Appium 解决 Appium 使用 UiAutomator2 带来的 keyevent 无法识别问题

charles0427 · 2017年07月04日 · 最后由 Wumengjie1992 回复于 2019年08月26日 · 3353 次阅读

最近将 appium 更新到了 1.6.5,uiautomator2 确实能实现对 Android toast 内容的断言。但后续发现之前的wd.pressDeviceKey()方法不起作用。

更新

wd@1.3.0添加了 press_keycode 的路由,修复了这个 bug
Replace the deprecated sendKeyEvent API with pressKeycode for Appium

POST /session/:sessionId/appium/device/press_keycode
Send key event to device (mjsonWire).

pressKeycode(keycode, metastate, cb) -> cb(err)
metastate is optional.

问题

首先分析 appium server 的日志:

2017-07-04 10:10:14:838 - info: [HTTP] --> POST /wd/hub/session/744e508b-3f7c-446b-b75f-a4373b0da0c1/appium/device/keyevent {"keycode":4}
2017-07-04 10:10:14:838 - info: [MJSONWP] Driver proxy active, passing request on via HTTP proxy
2017-07-04 10:10:14:838 - info: [debug] [JSONWP Proxy] Proxying [POST /wd/hub/session/744e508b-3f7c-446b-b75f-a4373b0da0c1/appium/device/keyevent] to [POST http://localhost:8200/wd/hub/session/3705f215-26ac-409d-a89d-c9ab578af2b4/appium/device/keyevent] with body: {"keycode":4}
2017-07-04 10:10:14:947 - error: [MJSONWP] Encountered internal error running command: Error: Could not proxy. Proxy error: Could not proxy command to remote server. Original error: 404 - undefined
    at doJwpProxy$ (C:\Users\lichen2\AppData\Local\Programs\appium-desktop\resources\app\node_modules\appium\node_modules\appium-base-driver\lib\mjsonwp\mjsonwp.js:354:13)
    at tryCatch (C:\Users\lichen2\AppData\Local\Programs\appium-desktop\resources\app\node_modules\appium\node_modules\babel-runtime\regenerator\runtime.js:67:40)
    at GeneratorFunctionPrototype.invoke [as _invoke] (C:\Users\lichen2\AppData\Local\Programs\appium-desktop\resources\app\node_modules\appium\node_modules\babel-runtime\regenerator\runtime.js:315:22)
    at GeneratorFunctionPrototype.prototype.(anonymous function) [as throw] (C:\Users\lichen2\AppData\Local\Programs\appium-desktop\resources\app\node_modules\appium\node_modules\babel-runtime\regenerator\runtime.js:100:21)
    at GeneratorFunctionPrototype.invoke (C:\Users\lichen2\AppData\Local\Programs\appium-desktop\resources\app\node_modules\appium\node_modules\babel-runtime\regenerator\runtime.js:136:37)
2017-07-04 10:10:14:947 - info: [HTTP] <-- POST /wd/hub/session/744e508b-3f7c-446b-b75f-a4373b0da0c1/appium/device/keyevent 500 119 ms - 274 

日志显示,Appium server 接收到了keyevent的 POST 请求,但转发出去时: Error: Could not proxy. Proxy error: Could not proxy command to remote server. Original error: 404 - undefined,很明显,404 表示设备端的 sever 没有响应该请求。

原因

出了问题,第一反应是去 appium 上翻 Issue,确实有人提过同样的问题,但作者并没有给出 Answer:
Can't use sendKeyEvent
于是只能自己翻源码了,因为问题出在 appium server 发 POST 到 uiautomator2 server 时 404,所以直接去appium-uiautomator2-server项目里看:
找到文件AppiumServelt,该类看上去是注册路由的:

private void registerPostHandler() {
        register(postHandler, new NewSession("/wd/hub/session"));
        register(postHandler, new FindElement("/wd/hub/session/:sessionId/element"));
        register(postHandler, new FindElements("/wd/hub/session/:sessionId/elements"));
        register(postHandler, new Click("/wd/hub/session/:sessionId/element/:id/click"));
        register(postHandler, new Click("/wd/hub/session/:sessionId/appium/tap"));
        register(postHandler, new Clear("/wd/hub/session/:sessionId/element/:id/clear"));
        register(postHandler, new RotateScreen("/wd/hub/session/:sessionId/orientation"));
        register(postHandler, new RotateScreen("/wd/hub/session/:sessionId/rotation"));
        register(postHandler, new PressBack("/wd/hub/session/:sessionId/back"));
        register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/element/:id/value"));
        register(postHandler, new SendKeysToElement("/wd/hub/session/:sessionId/keys"));
        register(postHandler, new Swipe("/wd/hub/session/:sessionId/touch/perform"));
        register(postHandler, new TouchLongClick("/wd/hub/session/:sessionId/touch/longclick"));
        register(postHandler, new OpenNotification("/wd/hub/session/:sessionId/appium/device/open_notifications"));
        register(postHandler, new PressKeyCode("/wd/hub/session/:sessionId/appium/device/press_keycode"));
        register(postHandler, new LongPressKeyCode("/wd/hub/session/:sessionId/appium/device/long_press_keycode"));
        register(postHandler, new Drag("/wd/hub/session/:sessionId/touch/drag"));
        register(postHandler, new AppStrings("/wd/hub/session/:sessionId/appium/app/strings"));
        register(postHandler, new Flick("/wd/hub/session/:sessionId/touch/flick"));
        register(postHandler, new ScrollTo("/wd/hub/session/:sessionId/touch/scroll"));
        register(postHandler, new MultiPointerGesture("/wd/hub/session/:sessionId/touch/multi/perform"));
        register(postHandler, new TouchDown("/wd/hub/session/:sessionId/touch/down"));
        register(postHandler, new TouchUp("/wd/hub/session/:sessionId/touch/up"));
        register(postHandler, new TouchMove("/wd/hub/session/:sessionId/touch/move"));
        register(postHandler, new UpdateSettings("/wd/hub/session/:sessionId/appium/settings"));
        register(postHandler, new NetworkConnection("/wd/hub/session/:sessionId/network_connection"));
    }

发现上面代码里并没有对应 wd: POST /session/:sessionId/appium/device/keyevent,只有register(postHandler, new PressKeyCode("/wd/hub/session/:sessionId/appium/device/press_keycode"));
所以,404 的原因找到了,笔者也在上面的 Issue 里给作者提了,希望后面作者能更新下 uiautomator2 server。

解决

知道了问题原因,可以想到三种解决方案:

1.修改 uiautomator2 server
对 appium 编译安装 uiautomator2 server 的过程没深入研究,尝试卸载原手机里的 io.appium.uiautomator2.server 程序,修改 AppiumServlet,并没有起作用,所以这个解决方法我还是等作者吧。。

更新
感谢 Carl 的提示,原来appium-uiautomator2-server项目的 Readme 已经给出编译方法。

  • AS 打开 appium-uiautomator2-server 项目,完成上面提到的 AppiumServelt 代码的修改
  • 运行gradle clean assembleServerDebug assembleServerDebugAndroidTest,这里编译可能会遇到一些问题,我遇到的是:app 的 build.gradle 中,配置了对 sdk-manager-plugin 的依赖,而阿里云的 maven 库里并没有收录这个项目。解决方法: 将相关配置的代码注释掉
  • gradle 编译成功后,会在/app/build/outputs/apk/下生产两个 apk: 1.appium-uiautomator2-server-v0.1.5.APK,执行命令的 apk,(GitHub 上已经更新到 0.1.6) 2.appium-uiautomator2-server-debug-androidTest.apk,用于启动 Server 的 apk
  • 替换设备中的 apk,可以手动安装,也可以替换 appium 下默认的 apk。默认 apk 放在 appium-uiautomator2-driver 的uiautomator2目录下,替换之。还未结束,查看 driver 里的uiautomator2.js代码:
async installServerApk () {
    // Installs the apks on to the device or emulator
    let apkPackage = await this.getPackageName(apkPath);
    // appending .test to apkPackage name to get test apk package name
    let testApkPackage = apkPackage + '.test';
    let isApkInstalled = await this.adb.isAppInstalled(apkPackage);
    let isTestApkInstalled = await this.adb.isAppInstalled(testApkPackage);
    if (isApkInstalled || isTestApkInstalled) {
      //check server apk versionName
      let apkVersion = await this.getAPKVersion(apkPath);
      let pkgVersion = await this.getInstalledPackageVersion(apkPackage);
      if (apkVersion !== pkgVersion) {
        isApkInstalled = false;
        isTestApkInstalled = false;
        await this.adb.uninstallApk(apkPackage);
        await this.adb.uninstallApk(testApkPackage);
      }
    }
    if (!isApkInstalled) {
      await this.signAndInstall(apkPath, apkPackage);
    }
    if (!isTestApkInstalled) {
      await this.signAndInstall(testApkPath, testApkPackage);
    }
  }

uiautomator2-driver 会去校验设备是否已安装以及对版本号进行判断,所以想替换设备里的版本,还需修改你编译 apk 的 version,或者提前删除设备已经安装的。

2.修改 wd 代码
既然 uiautomator2-Server 路由中只有press_keycode,没有keyevent,那可以在 wd 里添加相应的 Post 方法。
找到你项目中 node_modules 里 wd 模块,在/lib/commands.js 中添加代码如下:

/**
 * pressKeyCode(keycode, metastate, cb) -> cb(err)
 * metastate is optional
 *
 * @jsonWire POST /session/:sessionId/appium/device/press_keycode
 */
commands.pressKeyCode = function() {
    var fargs = utils.varargs(arguments);
    var cb = fargs.callback,
        keycode = fargs.all[0],
        metastate = fargs.all[1];
    var data = {keycode: keycode};
    if(metastate) { data.metastate = metastate; }
    this._jsonWireCall({
        method: 'POST'
        , relPath: '/appium/device/press_keycode'
        , data: data
        , cb: simpleCallback(cb)
    });
};

重启项目后,driver 就可以调用 pressKeyCode 方法,appium server 会将请求转发到/press_keycode,uiautomator2 server 也就可以识别。

3.修改 uiautomator2-driver
我们在分析 appium-uiautomator2-driver 时,发现作者其实是有写 keyevent 方法的,只不过调用的是 adb:

// uiautomator2 doesn't support metastate for keyevents
commands.keyevent = async function (keycode, metastate) {
  log.debug(`Ignoring metastate ${metastate}`);
  await this.adb.keyevent(keycode);
};

那么怎么才能让 appium 直接调用这个方法呢,我们发现 uiautomator2-driver 的 driver.js 里有个数组:

// NO_PROXY contains the paths that we never want to proxy to UiAutomator2 server.
// TODO:  Add the list of paths that we never want to proxy to UiAutomator2 server.
// TODO: Need to segregate the paths better way using regular expressions wherever applicable.
// (Not segregating right away because more paths to be added in the NO_PROXY list)
const NO_PROXY = [
  ['POST', new RegExp('^/session/[^/]+/touch/multi/perform')],
  ['POST', new RegExp('^/session/[^/]+/touch/perform')],
  ['POST', new RegExp('^/session/[^/]+/element')],
  ['POST', new RegExp('^/session/[^/]+/appium/element/[^/]+/value')],
  ['POST', new RegExp('^/session/[^/]+/appium/element/[^/]+/replace_value')],
  ['GET', new RegExp('^/session/[^/]+/appium/[^/]+/current_activity')],
  ['POST', new RegExp('^/session/[^/]+/appium/[^/]+/start_activity')],
  ['POST', new RegExp('^/session/[^/]+/app/[^/]')],
  ['POST', new RegExp('^/session/[^/]+/location')],
  ['GET', new RegExp('^/session/[^/]+/appium/device/system_time')],
  ['POST', new RegExp('^/session/[^/]+/appium/settings')],
  ['GET', new RegExp('^/session/[^/]+/appium/settings')],
  ['POST', new RegExp('^/session/[^/]+/appium/device/app_installed')],
  ['POST', new RegExp('^/session/[^/]+/appium/device/lock')],
  ['POST', new RegExp('^/session/[^/]+/appium/app/close')],
  ['POST', new RegExp('^/session/[^/]+/appium/app/launch')],
  ['POST', new RegExp('^/session/[^/]+/appium/device/pull_file')],
  ['POST', new RegExp('^/session/[^/]+/appium/device/push_file')],
  ['POST', new RegExp('^/session/[^/]+/appium/app/reset')],
  ['POST', new RegExp('^/session/[^/]+/appium/app/background')],
  ['POST', new RegExp('^/session/[^/]+/appium/device/toggle_location_services')],
  ['POST', new RegExp('^/session/[^/]+/appium/device/is_locked')],
  ['POST', new RegExp('^/session/[^/]+/appium/device/unlock')],
  ['POST', new RegExp('^/session/[^/]+/appium/app/end_test_coverage')],
  ['GET', new RegExp('^/session/[^/]+/contexts')],
  ['POST', new RegExp('^/session/[^/]+/context')],
  ['GET', new RegExp('^/session/[^/]+/context')],
  ['POST', new RegExp('^/session/[^/]+/network_connection')],
  ['GET', new RegExp('^/session/[^/]+/network_connection')],
  ['POST', new RegExp('^/session/[^/]+/timeouts')],
  ['GET', new RegExp('^/session/[^/]+/screenshot')],
  ['GET', new RegExp('^/session/[^/]+/element/[^/]+/attribute')],
  ['GET', new RegExp('^/session/[^/]+/element/[^/]+/enabled')],
  ['GET', new RegExp('^/session/[^/]+/element/[^/]+/selected')],
  ['GET', new RegExp('^/session/[^/]+/element/[^/]+/displayed')],
  ['GET', new RegExp('^/session/[^/]+/element/[^/]+/name')],
  ['GET', new RegExp('^/session/(?!.*\/)')],
  ['POST', new RegExp('^/session/[^/]+/keys')],
  ['POST', new RegExp('^/session/[^/]+/appium/device/hide_keyboard')],
  ['POST', new RegExp('^/session/[^/]+/log')],
  ['POST', new RegExp('^/session/[^/]+/appium/device/remove_app')],
  ['GET', new RegExp('^/session/[^/]+/appium/device/is_keyboard_shown')]
];

注释里写了, 该数组维护的是不会转发到 uiautomator2 的路由,那么,我们只需要在该数组里添加:
['POST', new RegExp('^/session/[^/]+/appium/device/keyevent')]

p.s,该方法修改的是 appium server 依赖的 appium-uiatumator2-driver,修改后需通过命令行启动 appium,桌面程序似乎是带有缓存自制的,直接运行修改的代码不会起作用。

以上,对 appium 整个原理还不是很熟,上面的分析可能有误,敬请指出~

共收到 17 条回复 时间 点赞

不错噢,uiautomator2.0server 的部分我也修改过,不过没有修改 nodejs 的部分,学习了👍

我也想修改一下这个 server 部分,主要是在安装 uiautomator2.0 server 应用的时候,是判断手机上是否安装的,但没有判断这个应用是不是有更新的逻辑,如果我更新了这个应用,但手机上仍然是旧版本的

bauul 回复

Thanks😁
按照正常安卓应用的逻辑,如果 versionCode 增加了,会覆盖旧的 app。但我看 uiautomator2-server 的 manifest 没有配置这个值...

bauul 回复

hello,我修改了 uiautomator2-server,在尝试将重编译的 apk 放到 uiautomator2-driver 里时遇到了错误,driver 安装这两个 apk 会先签名再安装,然后就报appium could not sign with default certificate,请问你有遇到过么

charles0427 回复

你打包的方式导致的吧,确认文件名如下:
appium-uiautomator2-server-v0.1.0.apk
appium-uiautomator2-server-debug-androidTest.apk

在目录:
appium\node_modules\appium-uiautomator2-driver\uiautomator2

bauul 回复

打包方式就是用的 appium 给的 gradle 命令,生成的 apk 是放到那个目录下了。就是在创建 appium session 的时候,uiautomator2-driver 去安装这两个 apk 失败

charles0427 回复

apk 的名字和我提供的两个完全一致吗?另外可以先把手机中已有的应用 卸载掉,再试一下

bauul 回复

嗯,这些检查了没问题,我编译的时候讲 gradle 里配的 sdk-manager-plugin 注释掉了,不知道是不是跟这有关。
appium server 的错误日志:

Encountered internal error running command: Error: Could not sign with default ceritficate. Original error Command '/Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/bin/java -jar /usr/local/lib/node_modules/appium/node_modules/.2.23.4\@appium-adb/jars/sign.jar /usr/local/lib/node_modules/appium/node_modules/.0.3.4\@appium-uiautomator2-driver/uiautomator2/appium-uiautomator2-server-v0.1.5.apk --override' exited with code 1
    at Object.wrappedLogger.errorAndThrow (../../lib/logging.js:63:13)
    at ADB.callee$0$0$ (../../../lib/tools/apk-signing.js:20:9)
    at tryCatch (/usr/local/lib/node_modules/appium/node_modules/.5.8.24@babel-runtime/regenerator/runtime.js:67:40)
    at GeneratorFunctionPrototype.invoke [as _invoke] (/usr/local/lib/node_modules/appium/node_modules/.5.8.24@babel-runtime/regenerator/runtime.js:315:22)
    at GeneratorFunctionPrototype.prototype.(anonymous function) [as throw] (/usr/local/lib/node_modules/appium/node_modules/.5.8.24@babel-runtime/re[HTTP] <-- POST /wd/hub/session 500 95650 ms - 529 
generator/runtime.js:100:21)
    at GeneratorFunctionPrototype.invoke (/usr/local/lib/node_modules/appium/node_modules/.5.8.24@babel-runtime/regenerator/runtime.js:136:37)
    at process._tickCallback (internal/process/next_tick.js:103:7)
charles0427 回复

不是这个原因噢,

buildscript {
    repositories {
        jcenter()
        maven { url 'https://jitpack.io' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'
        //classpath 'com.github.JakeWharton:sdk-manager-plugin:0ce4cdf08009d79223850a59959d9d6e774d0f77'
    }
}

//apply plugin: 'android-sdk-manager'
apply plugin: 'com.android.application'

// optionally including an emulator
//sdkManager {
//    emulatorVersion 'android-23'
//    emulatorArchitecture 'x86' // optional, defaults to arm
//}

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "io.appium.uiautomator2"
        minSdkVersion 18
        targetSdkVersion 25
        versionCode 1
        archivesBaseName = "appium-uiautomator2"
        /**
         * versionName should be updated and inline with version in package.json for every npm release.
         */
        versionName "0.1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        debug {
            debuggable true
        }
        customDebuggableBuildType {
            debuggable true
        }
        applicationVariants.all { variant ->
            appendVersionNameVersionCode(variant, defaultConfig)
        }
    }

    lintOptions {
        abortOnError false
    }
    productFlavors {
        e2eTest {
            applicationId 'io.appium.uiautomator2.e2etest'
        }
        server {
            applicationId 'io.appium.uiautomator2.server'
        }
    }
    packagingOptions {
        exclude 'META-INF/maven/com.google.guava/guava/pom.properties'
        exclude 'META-INF/maven/com.google.guava/guava/pom.xml'
    }

    useLibrary 'org.apache.http.legacy'
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile 'io.netty:netty-all:4.0.41.Final'
    compile 'com.jayway.jsonpath:json-path:0.8.1'
    compile 'com.squareup.okhttp:okhttp:2.5.0'
    compile 'com.android.support.test:runner:0.5'
    compile 'com.android.support:support-annotations:25.1.0'
    compile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
    androidTestCompile 'junit:junit:4.12'
    androidTestCompile 'com.android.support:support-annotations:25.1.0'
    androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.2'
    androidTestCompile 'com.android.support.test.espresso:espresso-web:2.2.2'
}

task installAUT(type: Exec) {
    group = 'test'
    description = 'Install app under test'
    def adb = android.getAdbExe().toString()
    def apk = file('../app/src/androidTestE2eTest/java/io/appium/uiautomator2/unittest/resource/ApiDemos-debug.apk')
    commandLine "$adb install -rg $apk".split(' ')
    standardOutput = new ByteArrayOutputStream()
    ext.output = {
        return standardOutput.toString()
    }
    println ext.output.toString()
}

afterEvaluate {
    tasks.each { task ->
        if (task.name.startsWith('connectedE2eTestDebugAndroidTest')) {
            task.dependsOn installAUT
        }
    }
}

def appendVersionNameVersionCode(variant, defaultConfig) {
    variant.outputs.each { output ->
        def file = output.packageApplication.outputFile
        def fileName = file.name.replace("debug.apk", "v${defaultConfig.versionName}.apk")
        output.packageApplication.outputFile = new File(file.parent, fileName)
    }
}



bauul 回复

好吧,谢谢啦,我再研究研究~

大神。。可以给我一下你 QQ 吗?这个问题我一直没解决 或者你加一下我的 QQ 541347048 求大神解救

可以联系一下我吗?这个问题解决不了。。真的没办法了。。。

帝恨牧 回复

wd 已经修复了这个 bug,你可以看下更新

charles0427 回复

@charles_joker_lee 求助,使用 Appium UIAutomator2 pressKeyCode(66) 不生效,server 也没有报错。
问题详情见下方链接帖子,感谢~

Appium 中使用 pressKeyCode 方法不起作用也没有报错

KD 回复

请问此问题你解决了没?我也遇到相同的问题,appium 1.8 ,Uiautomator2 ,python3.6。求指点~~

小暖ljj 回复

你看下我的帖子中,关于我的问题的解决方案写在帖子最后。希望对你有帮助

charles0427 回复

请确认 appium 是否以管理员身份运行,可能是权限导致

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册