| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- /**
- * adb method: input — 通过 ADB Keyboard 输入文本(先安装 static/ADBKeyboard.apk,切换 IME,发送后切回原输入法)
- */
- const path = require('path')
- const fs = require('fs')
- const { execSync, spawnSync } = require('child_process')
- const ADB_KEYBOARD_IME_ID = 'com.android.adbkeyboard/.AdbKeyboard'
- const ADB_KEYBOARD_PACKAGE = 'com.android.adbkeyboard'
- const B64_CHUNK_CHARS = 200
- function getProjectRoot(ctx) {
- const root = ctx.compilerConfig?.projectRoot
- if (root && fs.existsSync(root)) return root
- const defaultRoot = path.resolve(__dirname, '..', '..', '..', '..')
- return defaultRoot
- }
- function getAdbPath(projectRoot) {
- try {
- const configPath = path.join(projectRoot, 'configs', 'config.js')
- const config = fs.existsSync(configPath) ? require(configPath) : {}
- const p = config.adbPath?.path
- if (p) return path.isAbsolute(p) ? p : path.resolve(projectRoot, p)
- } catch (e) {}
- return path.join(projectRoot, 'lib', 'scrcpy-adb', process.platform === 'win32' ? 'adb.exe' : 'adb')
- }
- function getAdbKeyboardApkPath(projectRoot) {
- const p = path.join(projectRoot, 'static', 'ADBKeyboard.apk')
- return fs.existsSync(p) ? p : null
- }
- function deviceArgs(device) {
- return device && String(device).includes(':') ? ['-s', device] : []
- }
- function runShell(adbPath, device, shellArgs, timeout = 3000) {
- const args = [...deviceArgs(device), 'shell', shellArgs]
- const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout })
- if (r.status !== 0) throw new Error(r.stderr || r.stdout || `adb shell failed: ${r.status}`)
- return (r.stdout || '').trim()
- }
- function runShellQuiet(adbPath, device, shellArgs, timeout = 3000) {
- const args = [...deviceArgs(device), 'shell', shellArgs]
- const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout })
- return r.status === 0
- }
- function isPackageInstalled(adbPath, device, packageId) {
- try {
- const out = runShell(adbPath, device, `pm list packages ${packageId}`, 3000)
- return (out || '').indexOf('package:' + packageId) >= 0
- } catch (e) {
- return false
- }
- }
- function installApk(adbPath, device, apkPath) {
- const args = [...deviceArgs(device), 'install', '-r', apkPath]
- const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout: 60000 })
- if (r.status !== 0) throw new Error(r.stderr || r.stdout || `adb install failed: ${r.status}`)
- }
- function getCurrentIme(adbPath, device) {
- try {
- return runShell(adbPath, device, 'settings get secure default_input_method', 3000)
- } catch (e) {
- return ''
- }
- }
- function listEnabledImes(adbPath, device) {
- try {
- const out = runShell(adbPath, device, 'ime list -s', 3000)
- return (out || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)
- } catch (e) {
- return []
- }
- }
- function listAllImes(adbPath, device) {
- try {
- const out = runShell(adbPath, device, 'ime list -a', 3000)
- return (out || '').split(/\r?\n/).map(s => s.trim()).filter(Boolean)
- } catch (e) {
- return []
- }
- }
- /** 从 ime list -a 输出解析 IME id 列表(行格式多为 "com.pkg/.Class:") */
- function parseImeIdsFromListA(out) {
- const ids = []
- const lines = (out || '').split(/\r?\n/)
- for (const line of lines) {
- const t = line.trim()
- if (t.includes('/') && (t.endsWith(':') || !t.includes(' '))) {
- ids.push(t.replace(/:$/, ''))
- }
- }
- return ids
- }
- function findAdbKeyboardImeId(adbPath, device) {
- try {
- const out = runShell(adbPath, device, 'ime list -a', 3000)
- const ids = parseImeIdsFromListA(out)
- const lower = s => String(s).toLowerCase()
- const found = ids.find(id => lower(id).includes('adbkeyboard') || lower(id).includes('adb.keyboard'))
- return found || ADB_KEYBOARD_IME_ID
- } catch (e) {
- return ADB_KEYBOARD_IME_ID
- }
- }
- /** 从 dumpsys package 解析包内 BroadcastReceiver 组件名,用于 -n 显式发送 */
- function getPackageReceivers(adbPath, device, packageId) {
- try {
- const out = runShell(adbPath, device, `dumpsys package ${packageId}`, 8000)
- const receivers = []
- const re = /ComponentInfo\{([^}]+)\}/g
- let m
- while ((m = re.exec(out)) !== null) {
- const comp = m[1].trim()
- if (comp.startsWith(packageId + '/')) receivers.push(comp)
- }
- return [...new Set(receivers)]
- } catch (e) {
- return []
- }
- }
- function enableIme(adbPath, device, imeId) {
- if (!imeId) return
- const args = [...deviceArgs(device), 'shell', 'ime', 'enable', imeId]
- const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout: 5000 })
- if (r.status !== 0 && r.stderr) {
- throw new Error(`ime enable 失败: ${(r.stderr || r.stdout || '').trim() || r.status}`)
- }
- }
- function setIme(adbPath, device, imeId) {
- if (!imeId) return
- const args = [...deviceArgs(device), 'shell', 'ime', 'set', imeId]
- const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout: 5000 })
- if (r.status !== 0) {
- throw new Error(`ime set 失败: ${(r.stderr || r.stdout || '').trim() || r.status}`)
- }
- }
- function ensureAdbKeyboardInstalled(adbPath, device, projectRoot) {
- const apkPath = getAdbKeyboardApkPath(projectRoot)
- if (!apkPath) throw new Error('未找到 static/ADBKeyboard.apk,请将 ADB Keyboard 的 apk 放在该路径')
- if (!isPackageInstalled(adbPath, device, ADB_KEYBOARD_PACKAGE)) installApk(adbPath, device, apkPath)
- }
- /** 单次 broadcast(--es 字符串),返回是否成功 */
- function tryBroadcast(adbPath, device, action, key, value, component) {
- const args = [...deviceArgs(device), 'shell', 'am', 'broadcast', '-a', action, '--es', key, value]
- if (component) args.splice(args.indexOf(action) + 1, 0, '-n', component)
- const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout: 15000 })
- return r.status === 0
- }
- /** ADB_INPUT_CHARS:--eia chars 码点1 码点2 ...(部分设备 B64 异常时可用) */
- function tryBroadcastChars(adbPath, device, codePoints, component) {
- const args = [...deviceArgs(device), 'shell', 'am', 'broadcast', '-a', 'ADB_INPUT_CHARS', '--eia', 'chars', ...codePoints.map(String)]
- if (component) args.splice(args.indexOf('ADB_INPUT_CHARS') + 1, 0, '-n', component)
- const r = spawnSync(adbPath, args, { encoding: 'utf-8', timeout: 15000 })
- return r.status === 0
- }
- /** 依次尝试多种方式发送,直到一种成功。先试隐式(系统派发到注册的 Receiver),再试 -n */
- async function sendTextViaBroadcast(adbPath, device, str, adbKeyboardImeId, logMessage, folderPath) {
- const fromDumpsys = getPackageReceivers(adbPath, device, ADB_KEYBOARD_PACKAGE)
- const components = [
- null,
- adbKeyboardImeId,
- ...fromDumpsys,
- `${ADB_KEYBOARD_PACKAGE}/.AdbReceiver`,
- `${ADB_KEYBOARD_PACKAGE}/.Receiver`
- ]
- const dedup = [...new Set(components)]
- for (let i = 0; i < str.length; i += B64_CHUNK_CHARS) {
- const chunk = str.slice(i, i + B64_CHUNK_CHARS)
- const b64 = Buffer.from(chunk, 'utf8').toString('base64')
- let sent = false
- let used = null
- for (const comp of dedup) {
- if (tryBroadcast(adbPath, device, 'ADB_INPUT_B64', 'msg', b64, comp)) {
- sent = true
- used = comp ? comp : 'B64隐式'
- break
- }
- }
- if (!sent && chunk.length <= 100) {
- const codePoints = [...chunk].map(c => c.codePointAt(0))
- for (const comp of [adbKeyboardImeId, null]) {
- if (tryBroadcastChars(adbPath, device, codePoints, comp)) {
- sent = true
- used = 'ADB_INPUT_CHARS'
- break
- }
- }
- }
- if (!sent && tryBroadcast(adbPath, device, 'ADB_INPUT_TEXT', 'msg', chunk, null)) {
- sent = true
- used = 'ADB_INPUT_TEXT'
- }
- if (!sent) throw new Error('broadcast 所有方式均失败')
- if (i + B64_CHUNK_CHARS < str.length) await new Promise(r => setTimeout(r, 150))
- }
- }
- async function run(action, ctx) {
- const { device, folderPath, variableContext, api, extractVarName, resolveValue } = ctx
- let inputValue = null
- const inVars = action.inVars || []
- if (inVars.length > 0) {
- const raw = inVars[0]
- if (raw != null && typeof raw === 'string' && !(raw.startsWith('{') && raw.endsWith('}'))) {
- inputValue = raw
- } else {
- inputValue = variableContext[extractVarName(raw)]
- }
- }
- if (!inputValue && action.value) inputValue = resolveValue(action.value, variableContext)
- if (!inputValue) return { success: false, error: 'input 操作缺少输入内容' }
- if (action.clear) {
- if (!api?.sendKeyEvent) return { success: false, error: '清空需要 sendKeyEvent API' }
- for (let i = 0; i < 200; i++) {
- const clearResult = await api.sendKeyEvent(device, '67')
- if (!clearResult.success) break
- await new Promise((r) => setTimeout(r, 10))
- }
- await new Promise((r) => setTimeout(r, 200))
- }
- const projectRoot = getProjectRoot(ctx)
- const adbPath = getAdbPath(projectRoot)
- if (!fs.existsSync(adbPath)) return { success: false, error: `未找到 adb: ${adbPath}` }
- const logMessage = ctx.logMessage
- const needInstall = !isPackageInstalled(adbPath, device, ADB_KEYBOARD_PACKAGE)
- if (needInstall) {
- ensureAdbKeyboardInstalled(adbPath, device, projectRoot)
- }
- const adbKeyboardImeId = findAdbKeyboardImeId(adbPath, device)
- try {
- const prevIme = getCurrentIme(adbPath, device)
- try {
- try {
- enableIme(adbPath, device, adbKeyboardImeId)
- } catch (enableErr) {}
- await new Promise(r => setTimeout(r, 400))
- try {
- const sizeOut = runShell(adbPath, device, 'wm size', 3000)
- const m = (sizeOut || '').match(/(\d+)x(\d+)/)
- if (m) {
- const w = parseInt(m[1], 10)
- const h = parseInt(m[2], 10)
- const tapX = Math.floor(w / 2)
- const tapY = Math.floor(h * 0.28)
- runShellQuiet(adbPath, device, `input tap ${tapX} ${tapY}`, 2000)
- await new Promise(r => setTimeout(r, 500))
- }
- } catch (e) {}
- setIme(adbPath, device, adbKeyboardImeId)
- await new Promise(r => setTimeout(r, 1000))
- await sendTextViaBroadcast(adbPath, device, String(inputValue), adbKeyboardImeId, logMessage, folderPath)
- await new Promise(r => setTimeout(r, 1000))
- } finally {
- if (prevIme && prevIme !== adbKeyboardImeId) {
- setIme(adbPath, device, prevIme)
- } else {
- const enabled = listEnabledImes(adbPath, device)
- const other = enabled.find(id => id !== adbKeyboardImeId)
- if (other) setIme(adbPath, device, other)
- }
- }
- } catch (e) {
- const msg = e && (e.message || String(e)) || 'unknown'
- return { success: false, error: `输入失败: ${msg}` }
- }
- return { success: true }
- }
- module.exports = { run }
|