input.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. /**
  2. * adb method: input — 通过 ADB Keyboard 输入文本(先安装 static/ADBKeyboard.apk,切换 IME,发送后切回原输入法)
  3. */
  4. const path = require('path')
  5. const fs = require('fs')
  6. const { execSync, spawnSync } = require('child_process')
  7. const ADB_KEYBOARD_IME_ID = 'com.android.adbkeyboard/.AdbKeyboard'
  8. const ADB_KEYBOARD_PACKAGE = 'com.android.adbkeyboard'
  9. const B64_CHUNK_CHARS = 200
  10. function getProjectRoot(ctx) {
  11. const root = ctx.compilerConfig?.projectRoot
  12. if (root && fs.existsSync(root)) return root
  13. const defaultRoot = path.resolve(__dirname, '..', '..', '..', '..')
  14. return defaultRoot
  15. }
  16. function getAdbPath(projectRoot) {
  17. try {
  18. const configPath = path.join(projectRoot, 'configs', 'config.js')
  19. const config = fs.existsSync(configPath) ? require(configPath) : {}
  20. const p = config.adbPath?.path
  21. if (p) return path.isAbsolute(p) ? p : path.resolve(projectRoot, p)
  22. } catch (e) {}
  23. return path.join(projectRoot, 'lib', 'scrcpy-adb', process.platform === 'win32' ? 'adb.exe' : 'adb')
  24. }
  25. function getAdbKeyboardApkPath(projectRoot) {
  26. const p = path.join(projectRoot, 'static', 'ADBKeyboard.apk')
  27. return fs.existsSync(p) ? p : null
  28. }
  29. function deviceArgs(device) {
  30. return device && String(device).includes(':') ? ['-s', device] : []
  31. }
  32. function runShell(adbPath, device, shellArgs, timeout = 3000) {
  33. const args = [...deviceArgs(device), 'shell', shellArgs]
  34. const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout })
  35. if (r.status !== 0) throw new Error(r.stderr || r.stdout || `adb shell failed: ${r.status}`)
  36. return (r.stdout || '').trim()
  37. }
  38. function runShellQuiet(adbPath, device, shellArgs, timeout = 3000) {
  39. const args = [...deviceArgs(device), 'shell', shellArgs]
  40. const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout })
  41. return r.status === 0
  42. }
  43. function isPackageInstalled(adbPath, device, packageId) {
  44. try {
  45. const out = runShell(adbPath, device, `pm list packages ${packageId}`, 3000)
  46. return (out || '').indexOf('package:' + packageId) >= 0
  47. } catch (e) {
  48. return false
  49. }
  50. }
  51. function installApk(adbPath, device, apkPath) {
  52. const args = [...deviceArgs(device), 'install', '-r', apkPath]
  53. const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout: 60000 })
  54. if (r.status !== 0) throw new Error(r.stderr || r.stdout || `adb install failed: ${r.status}`)
  55. }
  56. function getCurrentIme(adbPath, device) {
  57. try {
  58. return runShell(adbPath, device, 'settings get secure default_input_method', 3000)
  59. } catch (e) {
  60. return ''
  61. }
  62. }
  63. function listEnabledImes(adbPath, device) {
  64. try {
  65. const out = runShell(adbPath, device, 'ime list -s', 3000)
  66. return (out || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)
  67. } catch (e) {
  68. return []
  69. }
  70. }
  71. function listAllImes(adbPath, device) {
  72. try {
  73. const out = runShell(adbPath, device, 'ime list -a', 3000)
  74. return (out || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)
  75. } catch (e) {
  76. return []
  77. }
  78. }
  79. /** 从 ime list -a 输出解析 IME id 列表(行格式多为 "com.pkg/.Class:") */
  80. function parseImeIdsFromListA(out) {
  81. const ids = []
  82. const lines = (out || '').split(/\r?\n/)
  83. for (const line of lines) {
  84. const t = line.trim()
  85. if (t.includes('/') && (t.endsWith(':') || !t.includes(' '))) {
  86. ids.push(t.replace(/:$/, ''))
  87. }
  88. }
  89. return ids
  90. }
  91. function findAdbKeyboardImeId(adbPath, device) {
  92. try {
  93. const out = runShell(adbPath, device, 'ime list -a', 3000)
  94. const ids = parseImeIdsFromListA(out)
  95. const lower = s => String(s).toLowerCase()
  96. const found = ids.find(id => lower(id).includes('adbkeyboard') || lower(id).includes('adb.keyboard'))
  97. return found || ADB_KEYBOARD_IME_ID
  98. } catch (e) {
  99. return ADB_KEYBOARD_IME_ID
  100. }
  101. }
  102. /** 从 dumpsys package 解析包内 BroadcastReceiver 组件名,用于 -n 显式发送 */
  103. function getPackageReceivers(adbPath, device, packageId) {
  104. try {
  105. const out = runShell(adbPath, device, `dumpsys package ${packageId}`, 8000)
  106. const receivers = []
  107. const re = /ComponentInfo\{([^}]+)\}/g
  108. let m
  109. while ((m = re.exec(out)) !== null) {
  110. const comp = m[1].trim()
  111. if (comp.startsWith(packageId + '/')) receivers.push(comp)
  112. }
  113. return [...new Set(receivers)]
  114. } catch (e) {
  115. return []
  116. }
  117. }
  118. function enableIme(adbPath, device, imeId) {
  119. if (!imeId) return
  120. const args = [...deviceArgs(device), 'shell', 'ime', 'enable', imeId]
  121. const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout: 5000 })
  122. if (r.status !== 0 && r.stderr) {
  123. throw new Error(`ime enable 失败: ${(r.stderr || r.stdout || '').trim() || r.status}`)
  124. }
  125. }
  126. function setIme(adbPath, device, imeId) {
  127. if (!imeId) return
  128. const args = [...deviceArgs(device), 'shell', 'ime', 'set', imeId]
  129. const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout: 5000 })
  130. if (r.status !== 0) {
  131. throw new Error(`ime set 失败: ${(r.stderr || r.stdout || '').trim() || r.status}`)
  132. }
  133. }
  134. function ensureAdbKeyboardInstalled(adbPath, device, projectRoot) {
  135. const apkPath = getAdbKeyboardApkPath(projectRoot)
  136. if (!apkPath) throw new Error('未找到 static/ADBKeyboard.apk,请将 ADB Keyboard 的 apk 放在该路径')
  137. if (!isPackageInstalled(adbPath, device, ADB_KEYBOARD_PACKAGE)) installApk(adbPath, device, apkPath)
  138. }
  139. /** 单次 broadcast(--es 字符串),返回是否成功 */
  140. function tryBroadcast(adbPath, device, action, key, value, component) {
  141. const args = [...deviceArgs(device), 'shell', 'am', 'broadcast', '-a', action, '--es', key, value]
  142. if (component) args.splice(args.indexOf(action) + 1, 0, '-n', component)
  143. const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout: 15000 })
  144. return r.status === 0
  145. }
  146. /** ADB_INPUT_CHARS:--eia chars 码点1 码点2 ...(部分设备 B64 异常时可用) */
  147. function tryBroadcastChars(adbPath, device, codePoints, component) {
  148. const args = [...deviceArgs(device), 'shell', 'am', 'broadcast', '-a', 'ADB_INPUT_CHARS', '--eia', 'chars', ...codePoints.map(String)]
  149. if (component) args.splice(args.indexOf('ADB_INPUT_CHARS') + 1, 0, '-n', component)
  150. const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout: 15000 })
  151. return r.status === 0
  152. }
  153. /** 依次尝试多种方式发送,直到一种成功。先试隐式(系统派发到注册的 Receiver),再试 -n */
  154. async function sendTextViaBroadcast(adbPath, device, str, adbKeyboardImeId, logMessage, folderPath) {
  155. const fromDumpsys = getPackageReceivers(adbPath, device, ADB_KEYBOARD_PACKAGE)
  156. const components = [
  157. null,
  158. adbKeyboardImeId,
  159. ...fromDumpsys,
  160. `${ADB_KEYBOARD_PACKAGE}/.AdbReceiver`,
  161. `${ADB_KEYBOARD_PACKAGE}/.Receiver`
  162. ]
  163. const dedup = [...new Set(components)]
  164. for (let i = 0; i < str.length; i += B64_CHUNK_CHARS) {
  165. const chunk = str.slice(i, i + B64_CHUNK_CHARS)
  166. const b64 = Buffer.from(chunk, 'utf8').toString('base64')
  167. let sent = false
  168. let used = null
  169. for (const comp of dedup) {
  170. if (tryBroadcast(adbPath, device, 'ADB_INPUT_B64', 'msg', b64, comp)) {
  171. sent = true
  172. used = comp ? comp : 'B64隐式'
  173. break
  174. }
  175. }
  176. if (!sent && chunk.length <= 100) {
  177. const codePoints = [...chunk].map(c => c.codePointAt(0))
  178. for (const comp of [adbKeyboardImeId, null]) {
  179. if (tryBroadcastChars(adbPath, device, codePoints, comp)) {
  180. sent = true
  181. used = 'ADB_INPUT_CHARS'
  182. break
  183. }
  184. }
  185. }
  186. if (!sent && tryBroadcast(adbPath, device, 'ADB_INPUT_TEXT', 'msg', chunk, null)) {
  187. sent = true
  188. used = 'ADB_INPUT_TEXT'
  189. }
  190. if (!sent) throw new Error('broadcast 所有方式均失败')
  191. if (i + B64_CHUNK_CHARS < str.length) await new Promise(r => setTimeout(r, 150))
  192. }
  193. }
  194. async function run(action, ctx) {
  195. const { device, folderPath, variableContext, api, extractVarName, resolveValue } = ctx
  196. let inputValue = null
  197. const inVars = action.inVars || []
  198. if (inVars.length > 0) {
  199. const raw = inVars[0]
  200. if (raw != null && typeof raw === 'string' && !(raw.startsWith('{') && raw.endsWith('}'))) {
  201. inputValue = raw
  202. } else {
  203. inputValue = variableContext[extractVarName(raw)]
  204. }
  205. }
  206. if (!inputValue && action.value) inputValue = resolveValue(action.value, variableContext)
  207. if (!inputValue) return { success: false, error: 'input 操作缺少输入内容' }
  208. if (action.clear) {
  209. if (!api?.sendKeyEvent) return { success: false, error: '清空需要 sendKeyEvent API' }
  210. for (let i = 0; i < 200; i++) {
  211. const clearResult = await api.sendKeyEvent(device, '67')
  212. if (!clearResult.success) break
  213. await new Promise((r) => setTimeout(r, 10))
  214. }
  215. await new Promise((r) => setTimeout(r, 200))
  216. }
  217. const projectRoot = getProjectRoot(ctx)
  218. const adbPath = getAdbPath(projectRoot)
  219. if (!fs.existsSync(adbPath)) return { success: false, error: `未找到 adb: ${adbPath}` }
  220. const logMessage = ctx.logMessage
  221. const needInstall = !isPackageInstalled(adbPath, device, ADB_KEYBOARD_PACKAGE)
  222. if (needInstall) {
  223. ensureAdbKeyboardInstalled(adbPath, device, projectRoot)
  224. }
  225. const adbKeyboardImeId = findAdbKeyboardImeId(adbPath, device)
  226. try {
  227. const prevIme = getCurrentIme(adbPath, device)
  228. try {
  229. try {
  230. enableIme(adbPath, device, adbKeyboardImeId)
  231. } catch (enableErr) {}
  232. await new Promise(r => setTimeout(r, 400))
  233. try {
  234. const sizeOut = runShell(adbPath, device, 'wm size', 3000)
  235. const m = (sizeOut || '').match(/(\d+)x(\d+)/)
  236. if (m) {
  237. const w = parseInt(m[1], 10)
  238. const h = parseInt(m[2], 10)
  239. const tapX = Math.floor(w / 2)
  240. const tapY = Math.floor(h * 0.28)
  241. runShellQuiet(adbPath, device, `input tap ${tapX} ${tapY}`, 2000)
  242. await new Promise(r => setTimeout(r, 500))
  243. }
  244. } catch (e) {}
  245. setIme(adbPath, device, adbKeyboardImeId)
  246. await new Promise(r => setTimeout(r, 1000))
  247. await sendTextViaBroadcast(adbPath, device, String(inputValue), adbKeyboardImeId, logMessage, folderPath)
  248. await new Promise(r => setTimeout(r, 1000))
  249. } finally {
  250. if (prevIme && prevIme !== adbKeyboardImeId) {
  251. setIme(adbPath, device, prevIme)
  252. } else {
  253. const enabled = listEnabledImes(adbPath, device)
  254. const other = enabled.find(id => id !== adbKeyboardImeId)
  255. if (other) setIme(adbPath, device, other)
  256. }
  257. }
  258. } catch (e) {
  259. const msg = e && (e.message || String(e)) || 'unknown'
  260. return { success: false, error: `输入失败: ${msg}` }
  261. }
  262. return { success: true }
  263. }
  264. module.exports = { run }