├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── build.gradle └── src │ ├── androidTest │ └── java │ │ ├── com │ │ └── macaca │ │ │ └── android │ │ │ └── testing │ │ │ ├── UIAutomatorWD.java │ │ │ ├── UIAutomatorWDServer.java │ │ │ └── server │ │ │ ├── Utils.java │ │ │ ├── common │ │ │ ├── Element.java │ │ │ └── Elements.java │ │ │ ├── controllers │ │ │ ├── ActionController.java │ │ │ ├── AlertController.java │ │ │ ├── ContextController.java │ │ │ ├── ElementController.java │ │ │ ├── ExecuteController.java │ │ │ ├── HomeController.java │ │ │ ├── KeysController.java │ │ │ ├── ScreenshotController.java │ │ │ ├── SessionController.java │ │ │ ├── SourceController.java │ │ │ ├── StatusController.java │ │ │ ├── TimeoutsController.java │ │ │ ├── TitleController.java │ │ │ ├── UrlController.java │ │ │ └── WindowController.java │ │ │ ├── models │ │ │ ├── Methods.java │ │ │ ├── Response.java │ │ │ └── Status.java │ │ │ └── xmlUtils │ │ │ ├── Attribute.java │ │ │ ├── InteractionController.java │ │ │ ├── MUiDevice.java │ │ │ ├── NodeInfoList.java │ │ │ ├── QueryController.java │ │ │ ├── ReflectionUtils.java │ │ │ ├── UiAutomationElement.java │ │ │ ├── UiAutomatorBridge.java │ │ │ ├── UiElement.java │ │ │ └── XPathSelector.java │ │ └── fi │ │ └── iki │ │ └── elonen │ │ └── router │ │ └── RouterNanoHTTPD.java │ └── main │ ├── AndroidManifest.xml │ └── res │ ├── drawable-hdpi │ └── ic_launcher.png │ ├── drawable-mdpi │ └── ic_launcher.png │ ├── drawable-xhdpi │ └── ic_launcher.png │ ├── drawable-xxhdpi │ └── ic_launcher.png │ ├── drawable-xxxhdpi │ └── ic_launcher.png │ ├── layout │ ├── activity_main.xml │ └── activity_show_text.xml │ ├── values-v13 │ └── styles.xml │ ├── values-v21 │ └── styles.xml │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── index.js ├── lib ├── helper.js ├── logger.js ├── proxy.js ├── uiautomator-client.js └── uiautomatorwd.js ├── package.json ├── scripts └── build.js ├── settings.gradle └── test ├── mocha.opts └── uiautomator-client.test.js /.eslintignore: -------------------------------------------------------------------------------- 1 | **/.* 2 | **/node_modules 3 | **/dist 4 | **/coverage 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true, 5 | "mocha": true 6 | }, 7 | "plugins": [ 8 | "mocha" 9 | ], 10 | // https://github.com/feross/eslint-config-standard 11 | "rules": { 12 | "accessor-pairs": 2, 13 | "block-scoped-var": 0, 14 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 15 | "camelcase": 0, 16 | "comma-dangle": [2, "never"], 17 | "comma-spacing": [2, { "before": false, "after": true }], 18 | "comma-style": [2, "last"], 19 | "complexity": 0, 20 | "consistent-return": 0, 21 | "consistent-this": 0, 22 | "curly": [2, "multi-line"], 23 | "default-case": 0, 24 | "dot-location": [2, "property"], 25 | "dot-notation": 0, 26 | "eol-last": 2, 27 | "eqeqeq": [2, "allow-null"], 28 | "func-names": 0, 29 | "func-style": 0, 30 | "generator-star-spacing": [2, "before"], 31 | "guard-for-in": 0, 32 | "handle-callback-err": [2, "^(err|error|anySpecificError)$" ], 33 | "indent": [2, 2], 34 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 35 | "linebreak-style": 0, 36 | "max-depth": 0, 37 | "max-len": 0, 38 | "max-nested-callbacks": 0, 39 | "max-params": 0, 40 | "max-statements": 0, 41 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 42 | "new-parens": 2, 43 | "no-alert": 0, 44 | "no-array-constructor": 2, 45 | "no-bitwise": 0, 46 | "no-caller": 0, 47 | "no-catch-shadow": 0, 48 | "no-cond-assign": 2, 49 | "no-console": 0, 50 | "no-constant-condition": 0, 51 | "no-continue": 0, 52 | "no-control-regex": 2, 53 | "no-debugger": 2, 54 | "no-delete-var": 2, 55 | "no-div-regex": 0, 56 | "no-dupe-args": 2, 57 | "no-dupe-keys": 2, 58 | "no-duplicate-case": 2, 59 | "no-else-return": 0, 60 | "no-empty": 0, 61 | "no-empty-character-class": 2, 62 | "no-eq-null": 0, 63 | "no-eval": 2, 64 | "no-ex-assign": 2, 65 | "no-extend-native": 2, 66 | "no-extra-bind": 2, 67 | "no-extra-boolean-cast": 2, 68 | "no-extra-semi": 0, 69 | "no-extra-strict": 0, 70 | "no-fallthrough": 2, 71 | "no-floating-decimal": 2, 72 | "no-func-assign": 2, 73 | "no-implied-eval": 2, 74 | "no-inline-comments": 0, 75 | "no-inner-declarations": [2, "functions"], 76 | "no-invalid-regexp": 2, 77 | "no-irregular-whitespace": 2, 78 | "no-iterator": 2, 79 | "no-label-var": 2, 80 | "no-labels": 2, 81 | "no-lone-blocks": 2, 82 | "no-lonely-if": 0, 83 | "no-loop-func": 0, 84 | "no-mixed-requires": 0, 85 | "no-mixed-spaces-and-tabs": [2, false], 86 | "no-multi-spaces": 2, 87 | "no-multi-str": 2, 88 | "no-multiple-empty-lines": [2, { "max": 1 }], 89 | "no-native-reassign": 2, 90 | "no-negated-in-lhs": 2, 91 | "no-nested-ternary": 0, 92 | "no-new": 2, 93 | "no-new-func": 2, 94 | "no-new-object": 2, 95 | "no-new-require": 2, 96 | "no-new-wrappers": 2, 97 | "no-obj-calls": 2, 98 | "no-octal": 2, 99 | "no-octal-escape": 2, 100 | "no-path-concat": 0, 101 | "no-plusplus": 0, 102 | "no-process-env": 0, 103 | "no-process-exit": 0, 104 | "no-proto": 2, 105 | "no-redeclare": 2, 106 | "no-regex-spaces": 2, 107 | "no-reserved-keys": 0, 108 | "no-restricted-modules": 0, 109 | "no-return-assign": 2, 110 | "no-script-url": 0, 111 | "no-self-compare": 2, 112 | "no-sequences": 2, 113 | "no-shadow": 0, 114 | "no-shadow-restricted-names": 2, 115 | "no-spaced-func": 2, 116 | "no-sparse-arrays": 2, 117 | "no-sync": 0, 118 | "no-ternary": 0, 119 | "no-throw-literal": 2, 120 | "no-trailing-spaces": 2, 121 | "no-undef": 2, 122 | "no-undef-init": 2, 123 | "no-undefined": 0, 124 | "no-underscore-dangle": 0, 125 | "no-unneeded-ternary": 2, 126 | "no-unreachable": 2, 127 | "no-unused-expressions": 0, 128 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 129 | "no-use-before-define": 0, 130 | "no-var": 0, 131 | "no-void": 0, 132 | "no-warning-comments": 0, 133 | "no-with": 2, 134 | "no-extra-parens": 2, 135 | "object-curly-spacing": 0, 136 | "one-var": [2, { "initialized": "never" }], 137 | "operator-assignment": 0, 138 | "operator-linebreak": [2, "after"], 139 | "padded-blocks": 0, 140 | "quote-props": 0, 141 | "quotes": [2, "single", "avoid-escape"], 142 | "radix": 2, 143 | "semi": [2, "always"], 144 | "semi-spacing": 0, 145 | "sort-vars": 0, 146 | "keyword-spacing": 2, 147 | "space-before-blocks": [2, "always"], 148 | "space-before-function-paren": 0, 149 | "space-in-parens": [2, "never"], 150 | "space-infix-ops": 2, 151 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 152 | "spaced-comment": [2, "always"], 153 | "strict": 0, 154 | "use-isnan": 2, 155 | "valid-jsdoc": 0, 156 | "valid-typeof": 2, 157 | "vars-on-top": 0, 158 | "wrap-iife": [2, "any"], 159 | "wrap-regex": 0, 160 | "yoda": [2, "never"] 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | .idea/* 5 | Thumbs.db 6 | coverage/ 7 | .project 8 | build/ 9 | bin/ 10 | gen/ 11 | app/build 12 | .gradle/ 13 | import-summary.txt 14 | local.properties 15 | *.iml 16 | *.un~ 17 | *.sw* 18 | .nyc_output/ 19 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .travis.yml 3 | .eslintrc 4 | .eslintignore 5 | coverage/ 6 | bin/ 7 | gen/ 8 | build/ 9 | app/*.iml 10 | app/build 11 | .gradle/ 12 | .idea/ 13 | import-summary.txt 14 | *.iml 15 | local.properties 16 | *.sw* 17 | *.un~ 18 | .nyc_output/ 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | sudo: required 3 | os: 4 | - linux 5 | jdk: 6 | - openjdk8 7 | - openjdk11 8 | android: 9 | components: 10 | - tools 11 | - build-tools-28.0.3 12 | - platform-tools 13 | - extra-android-m2repository 14 | - extra-google-android-support 15 | - android-26 16 | - android-28 17 | before_script: 18 | - wget https://services.gradle.org/distributions/gradle-5.6.4-all.zip 19 | - unzip gradle-5.6.4-all.zip > /dev/null 20 | - export GRADLE_HOME=$PWD/gradle-5.6.4 21 | - export PATH=$GRADLE_HOME/bin:$PATH 22 | - . $HOME/.nvm/nvm.sh 23 | - nvm install 10 24 | - nvm use 10 25 | script: 26 | - npm i 27 | - npm run lint 28 | - npm run test 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to UIAutomatorWD 2 | 3 | We love pull requests from everyone. 4 | 5 | ## Link Global To Local 6 | 7 | ``` bash 8 | $ cd path/to/macaca-android 9 | $ npm link path/to/UIAutomatorWD 10 | # now project UIAutomatorWD is linked to macaca-android 11 | ``` 12 | 13 | ## Run with Android Studio 14 | 15 | ## Restful Sample 16 | 17 | ``` bash 18 | $ adb forward tcp:9001 tcp:9001 19 | $ curl -l -H "Content-type: application/json" -X POST -d '{"value": "//*[@resource-id=\"android:id/tabs\"]/android.widget.LinearLayout[2]/android.widget.ImageView[1]","using":"xpath"}' http://localhost:9001/wd/hub/session/xxxxxxxx/element/1/click 20 | ``` 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | 3 | Copyright (c) 2017 Alibaba Group Holding Limited and other contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UIAutomatorWD 2 | 3 | --- 4 | 5 | [![NPM version][npm-image]][npm-url] 6 | [![build status][travis-image]][travis-url] 7 | [![node version][node-image]][node-url] 8 | [![npm download][download-image]][download-url] 9 | 10 | [npm-image]: https://img.shields.io/npm/v/uiautomatorwd.svg 11 | [npm-url]: https://npmjs.org/package/uiautomatorwd 12 | [travis-image]: https://img.shields.io/travis/macacajs/UIAutomatorWD.svg 13 | [travis-url]: https://travis-ci.org/macacajs/UIAutomatorWD 14 | [node-image]: https://img.shields.io/badge/node.js-%3E=_8-green.svg 15 | [node-url]: http://nodejs.org/download/ 16 | [download-image]: https://img.shields.io/npm/dm/uiautomatorwd.svg 17 | [download-url]: https://npmjs.org/package/uiautomatorwd 18 | 19 | Node.js wrapper for UIAutomator. 20 | 21 | 22 | ## Contributors 23 | 24 | |[
xudafeng](https://github.com/xudafeng)
|[
SamuelZhaoY](https://github.com/SamuelZhaoY)
|[
qichuan](https://github.com/qichuan)
|[
centy720](https://github.com/centy720)
|[
paradite](https://github.com/paradite)
|[
atomtong](https://github.com/atomtong)
| 25 | | :---: | :---: | :---: | :---: | :---: | :---: | 26 | |[
kyowang](https://github.com/kyowang)
|[
quxiaozha](https://github.com/quxiaozha)
|[
qddegtya](https://github.com/qddegtya)
|[
LynneXu](https://github.com/LynneXu)
|[
zhuyali](https://github.com/zhuyali)
|[
baozhida](https://github.com/baozhida)
| 27 | [
niaoshuai](https://github.com/niaoshuai)
28 | 29 | This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Sun Apr 26 2020 11:27:03 GMT+0800`. 30 | 31 | 32 | 33 | ## Installment 34 | 35 | ```bash 36 | $ npm i uiautomatorwd --save 37 | ``` 38 | 39 | [uiautomator source code](https://android.googlesource.com/platform/frameworks/testing/+/master/uiautomator/) 40 | 41 | ## Development 42 | 43 | - [CONTRIBUTING.md](./CONTRIBUTING.md) 44 | 45 | ## Report Issues 46 | 47 | Please report issues at the [macaca main repo](https://github.com/alibaba/macaca/issues/new), following the issue format. 48 | 49 | ## License 50 | 51 | The MIT License (MIT) 52 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | defaultConfig { 5 | compileSdkVersion 26 6 | minSdkVersion 18 7 | targetSdkVersion 26 8 | versionCode 1 9 | versionName "1.0" 10 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 11 | } 12 | buildTypes { 13 | release { 14 | minifyEnabled false 15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 16 | } 17 | } 18 | productFlavors { 19 | } 20 | testOptions { 21 | unitTests.all { 22 | testLogging { 23 | events "passed", "skipped", "failed", "standardOut", "standardError" 24 | outputs.upToDateWhen {false} 25 | showStandardStreams = true 26 | } 27 | } 28 | } 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | 35 | 36 | useLibrary 'org.apache.http.legacy' 37 | lint { 38 | abortOnError false 39 | } 40 | } 41 | 42 | dependencies { 43 | androidTestImplementation 'com.android.support.test:runner:' + rootProject.runnerVersion 44 | androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:' + rootProject.uiautomatorVersion 45 | implementation 'org.nanohttpd:nanohttpd:' + rootProject.nanohttpdVersion 46 | implementation 'com.alibaba:fastjson:' + rootProject.fastjsonVersion 47 | } 48 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/UIAutomatorWD.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing; 2 | 3 | import android.os.Bundle; 4 | import android.os.SystemClock; 5 | import android.support.test.InstrumentationRegistry; 6 | import android.support.test.runner.AndroidJUnit4; 7 | import android.support.test.uiautomator.UiDevice; 8 | import android.support.test.uiautomator.UiObject; 9 | import android.support.test.uiautomator.UiSelector; 10 | 11 | import com.alibaba.fastjson.JSON; 12 | import com.alibaba.fastjson.JSONArray; 13 | import com.macaca.android.testing.server.Utils; 14 | import com.macaca.android.testing.server.common.Elements; 15 | 16 | import org.junit.Test; 17 | import org.junit.runner.RunWith; 18 | 19 | @RunWith(AndroidJUnit4.class) 20 | public class UIAutomatorWD { 21 | @Test 22 | public void MacacaTestRunner() throws Exception { 23 | Bundle args = InstrumentationRegistry.getArguments(); 24 | 25 | int port = 9001; 26 | if (args.containsKey("port")) { 27 | port = Integer.parseInt(args.getString("port")); 28 | } 29 | 30 | UIAutomatorWDServer server = UIAutomatorWDServer.getInstance(port); 31 | Utils.print("UIAutomatorWD->" + "http://localhost:" + server.getListeningPort() + "<-UIAutomatorWD"); 32 | 33 | if (args.containsKey("permissionPattern")) { 34 | JSONArray permissionPatterns = JSON.parseArray(args.getString("permissionPattern")); 35 | skipPermission(permissionPatterns, 15); 36 | } 37 | 38 | while (true) { 39 | SystemClock.sleep(1000); 40 | } 41 | } 42 | 43 | public void skipPermission(JSONArray permissionPatterns, int scanningCount) { 44 | UiDevice mDevice = Elements.getGlobal().getmDevice(); 45 | 46 | // if permission list is empty, avoid execution 47 | if (permissionPatterns.size() == 0) { 48 | return; 49 | } 50 | 51 | // regular check for permission scanning 52 | try { 53 | for (int i = 0; i < scanningCount; i++) { 54 | inner: 55 | for (int j = 0; j < permissionPatterns.size(); j++) { 56 | String text = permissionPatterns.getString(j); 57 | UiObject object = mDevice.findObject(new UiSelector().text(text)); 58 | if (object.exists()) { 59 | object.click(); 60 | break inner; 61 | } 62 | } 63 | 64 | Thread.sleep(3000); 65 | } 66 | } catch (Exception e) { 67 | System.out.println(e.getMessage()); 68 | System.out.println(e.getCause().toString()); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/UIAutomatorWDServer.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing; 2 | 3 | import com.macaca.android.testing.server.controllers.*; 4 | import com.macaca.android.testing.server.models.Methods; 5 | 6 | import java.io.IOException; 7 | 8 | import com.macaca.android.testing.server.xmlUtils.ReflectionUtils; 9 | import fi.iki.elonen.NanoHTTPD; 10 | import fi.iki.elonen.router.RouterNanoHTTPD; 11 | 12 | /** 13 | * Created by xdf on 02/05/2017. 14 | */ 15 | 16 | public class UIAutomatorWDServer extends RouterNanoHTTPD { 17 | 18 | private static volatile UIAutomatorWDServer singleton; 19 | 20 | private String sessionRoutePrefix = "/wd/hub/session/:sessionId"; 21 | 22 | private UIAutomatorWDServer(int port) throws IOException { 23 | super(port); 24 | 25 | //Home 26 | //addRoute(sessionRoutePrefix + "/", Methods.GET, HomeController.home); 27 | 28 | //SessionRouter 29 | //addRoute("/wd/hub/session", Methods.POST, SessionController.createSession); 30 | //addRoute("/wd/hub/sessions", Methods.GET, SessionController.getSessions); 31 | //addRoute("/wd/hub/session/:sessionId", Methods.DELETE, SessionController.delSession); 32 | 33 | //Window Router 34 | addRoute(sessionRoutePrefix + "/window_handle", Methods.GET, WindowController.getWindow); 35 | addRoute(sessionRoutePrefix + "/window_handles", Methods.GET, WindowController.getWindows); 36 | addRoute(sessionRoutePrefix + "/window", Methods.POST, WindowController.setWindow); 37 | addRoute(sessionRoutePrefix + "/window", Methods.DELETE, WindowController.deleteWindow); 38 | addRoute(sessionRoutePrefix + "/window/:windowHandle/size", Methods.POST, WindowController.setWindowSize); 39 | addRoute(sessionRoutePrefix + "/window/:windowHandle/size", Methods.GET, WindowController.getWindowSize); 40 | addRoute(sessionRoutePrefix + "/window/:windowHandle/maximize", Methods.POST, WindowController.maximize); 41 | addRoute(sessionRoutePrefix + "/frame", Methods.POST, WindowController.setFrame); 42 | 43 | //ContextRouter 44 | addRoute(sessionRoutePrefix + "/context", Methods.GET, ContextController.getContext); 45 | addRoute(sessionRoutePrefix + "/context", Methods.POST, ContextController.setContext); 46 | addRoute(sessionRoutePrefix + "/contexts", Methods.GET, ContextController.getContexts); 47 | 48 | //AlertRouter 49 | addRoute(sessionRoutePrefix + "/accept_alert", Methods.POST, AlertController.acceptAlert); 50 | addRoute(sessionRoutePrefix + "/dismiss_alert", Methods.POST, AlertController.dismissAlert); 51 | addRoute(sessionRoutePrefix + "/alert_text", Methods.GET, AlertController.alertText); 52 | addRoute(sessionRoutePrefix + "/alert_text", Methods.POST, AlertController.alertKeys); 53 | 54 | //ElementRouter 55 | addRoute(sessionRoutePrefix + "/click", Methods.POST, ElementController.click); 56 | addRoute(sessionRoutePrefix + "/element", Methods.POST, ElementController.findElement); 57 | addRoute(sessionRoutePrefix + "/elements", Methods.POST, ElementController.findElements); 58 | addRoute(sessionRoutePrefix + "/element/:elementId/element", Methods.POST, ElementController.findElement); 59 | addRoute(sessionRoutePrefix + "/element/:elementId/elements", Methods.POST, ElementController.findElements); 60 | addRoute(sessionRoutePrefix + "/element/:elementId/value", Methods.POST, ElementController.setValue); 61 | addRoute(sessionRoutePrefix + "/element/:elementId/click", Methods.POST, ElementController.click); 62 | addRoute(sessionRoutePrefix + "/element/:elementId/text", Methods.GET, ElementController.getText); 63 | addRoute(sessionRoutePrefix + "/element/:elementId/clear", Methods.POST, ElementController.clearText); 64 | addRoute(sessionRoutePrefix + "/element/:elementId/displayed", Methods.GET, ElementController.isDisplayed); 65 | addRoute(sessionRoutePrefix + "/element/:elementId/attribute/:name", Methods.GET, ElementController.getAttribute); 66 | addRoute(sessionRoutePrefix + "/element/:elementId/property/:name", Methods.GET, ElementController.getAttribute); 67 | addRoute(sessionRoutePrefix + "/element/:elementId/css/:propertyName", Methods.GET, ElementController.getComputedCss); 68 | addRoute(sessionRoutePrefix + "/element/:elementId/rect", Methods.GET, ElementController.getRect); 69 | 70 | //ScreenshotRouter 71 | addRoute(sessionRoutePrefix + "/screenshot", Methods.GET, ScreenshotController.getScreenshot); 72 | 73 | //SourceRouter 74 | addRoute(sessionRoutePrefix + "/source", Methods.GET, SourceController.source); 75 | 76 | //KeysRouter 77 | addRoute(sessionRoutePrefix + "/keys", Methods.POST, KeysController.keys); 78 | 79 | //TimeoutsRouter 80 | addRoute(sessionRoutePrefix + "/timeouts/implicit_wait", Methods.POST, TimeoutsController.implicitWait); 81 | 82 | //UrlRouter 83 | addRoute(sessionRoutePrefix + "/url", Methods.POST, UrlController.getUrl); 84 | addRoute(sessionRoutePrefix + "/url", Methods.GET, UrlController.url); 85 | addRoute(sessionRoutePrefix + "/forward", Methods.POST, UrlController.forward); 86 | addRoute(sessionRoutePrefix + "/back", Methods.POST, UrlController.back); 87 | addRoute(sessionRoutePrefix + "/refresh", Methods.POST, UrlController.refresh); 88 | 89 | //ActionRouter 90 | addRoute(sessionRoutePrefix + "/actions", Methods.POST, ActionController.actions); 91 | 92 | start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); 93 | System.out.println("\nRunning! Point your browsers to http://localhost:8080/ \n"); 94 | } 95 | 96 | public static UIAutomatorWDServer getInstance(int port) { 97 | if (singleton == null) { 98 | synchronized (UIAutomatorWDServer.class) { 99 | if (singleton == null) { 100 | try { 101 | singleton = new UIAutomatorWDServer(port); 102 | } catch (IOException ioe) { 103 | System.err.println("Couldn't start server:\n" + ioe); 104 | } 105 | } 106 | } 107 | } 108 | return singleton; 109 | } 110 | 111 | @Override 112 | public Response serve(IHTTPSession session) { 113 | try { 114 | System.out.println("Try to clean up the Accessibility Node cache."); 115 | ReflectionUtils.clearAccessibilityCache(); 116 | } catch (Exception e) { 117 | System.err.println("Failed to clear Accessibility Node cache."); 118 | } 119 | return super.serve(session); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/Utils.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server; 2 | 3 | import android.app.Instrumentation; 4 | import android.os.Bundle; 5 | import android.support.test.InstrumentationRegistry; 6 | 7 | /** 8 | * Created by xdf on 07/05/2017. 9 | */ 10 | 11 | public class Utils { 12 | public static void print(String string) { 13 | Bundle b = new Bundle(); 14 | b.putString(Instrumentation.REPORT_KEY_STREAMRESULT, "\n" + string); 15 | InstrumentationRegistry.getInstrumentation().sendStatus(0, b); 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/common/Element.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.common; 2 | 3 | import android.graphics.Point; 4 | import android.support.test.uiautomator.Configurator; 5 | import android.support.test.uiautomator.UiObject2; 6 | import android.support.test.uiautomator.UiObjectNotFoundException; 7 | 8 | public class Element { 9 | 10 | public UiObject2 element; 11 | 12 | public String id; 13 | 14 | 15 | Element(String id, UiObject2 element) { 16 | this.element = element; 17 | this.id = id; 18 | } 19 | 20 | 21 | public void click() { 22 | element.click(); 23 | } 24 | 25 | 26 | public String getId() { 27 | return id; 28 | } 29 | 30 | 31 | public String getText() throws UiObjectNotFoundException { 32 | return element.getText(); 33 | } 34 | 35 | public void clearText() throws UiObjectNotFoundException { 36 | element.clear(); 37 | } 38 | 39 | public void setText(String text) throws UiObjectNotFoundException { 40 | Configurator config = Configurator.getInstance(); 41 | config.setKeyInjectionDelay(20); 42 | element.setText(text); 43 | config.setKeyInjectionDelay(0); 44 | } 45 | 46 | public boolean tap() throws UiObjectNotFoundException { 47 | element.click(); 48 | return true; 49 | } 50 | 51 | public boolean doubleTap() throws UiObjectNotFoundException, Exception { 52 | element.click(); 53 | Thread.sleep(100); 54 | element.click(); 55 | return true; 56 | } 57 | 58 | public boolean isDisplayed() throws UiObjectNotFoundException { 59 | return true; 60 | } 61 | 62 | public UiObject2 getUiObject() { 63 | return this.element; 64 | } 65 | 66 | public boolean pinch(String direction, float percent, int steps) throws UiObjectNotFoundException { 67 | if (direction.equals("in")) { 68 | element.pinchOpen(percent, steps); 69 | } else if (direction.equals("out")) { 70 | element.pinchClose(percent, steps); 71 | } 72 | return true; 73 | } 74 | 75 | public boolean drag(int x, int y, int steps) throws UiObjectNotFoundException { 76 | Point point = new Point(x, y); 77 | element.drag(point, steps); 78 | return true; 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/common/Elements.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.common; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Hashtable; 5 | import java.util.List; 6 | 7 | import android.support.test.uiautomator.BySelector; 8 | import android.support.test.uiautomator.UiDevice; 9 | import android.support.test.InstrumentationRegistry; 10 | import android.support.test.uiautomator.UiObject2; 11 | 12 | public class Elements { 13 | 14 | private static Elements global; 15 | private Hashtable elems; 16 | private UiDevice mDevice; 17 | private Integer counter; 18 | 19 | public Elements() { 20 | counter = 0; 21 | elems = new Hashtable(); 22 | mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()); 23 | } 24 | 25 | public static Elements getGlobal() { 26 | if (Elements.global == null) { 27 | Elements.global = new Elements(); 28 | } 29 | return Elements.global; 30 | } 31 | 32 | public Element addElement(UiObject2 element) { 33 | counter++; 34 | final String key = counter.toString(); 35 | Element elem = new Element(key, element); 36 | getElems().put(key, elem); 37 | return elem; 38 | } 39 | 40 | public List addElements(List elements) { 41 | List elems = new ArrayList(); 42 | for(int i = 0; i < elements.size(); i++) { 43 | counter++; 44 | Element elem = new Element(counter + "", elements.get(i)); 45 | getElems().put(counter + "", elem); 46 | elems.add(elem); 47 | } 48 | 49 | return elems; 50 | } 51 | 52 | public Element getElement(String key) { 53 | return getElems().get(key); 54 | } 55 | 56 | public Element getElement(BySelector sel) throws Exception { 57 | UiObject2 el = mDevice.findObject(sel); 58 | Element result = addElement(el); 59 | if (el != null) { 60 | return result; 61 | } else { 62 | throw new Exception("not found"); 63 | } 64 | } 65 | 66 | public List getMultiElement(BySelector sel) throws Exception { 67 | List el = mDevice.findObjects(sel); 68 | List result = addElements(el); 69 | if (result != null) { 70 | return result; 71 | } else { 72 | throw new Exception("not found"); 73 | } 74 | } 75 | 76 | public Hashtable getElems() { 77 | return elems; 78 | } 79 | 80 | public UiDevice getmDevice() { 81 | return this.mDevice; 82 | } 83 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/controllers/ActionController.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.controllers; 2 | 3 | import android.graphics.Point; 4 | import android.graphics.Rect; 5 | import android.support.test.uiautomator.UiDevice; 6 | 7 | import com.alibaba.fastjson.JSON; 8 | import com.alibaba.fastjson.JSONArray; 9 | import com.alibaba.fastjson.JSONObject; 10 | import com.macaca.android.testing.server.Utils; 11 | import com.macaca.android.testing.server.common.Element; 12 | import com.macaca.android.testing.server.common.Elements; 13 | import com.macaca.android.testing.server.models.Response; 14 | import com.macaca.android.testing.server.models.Status; 15 | 16 | import fi.iki.elonen.NanoHTTPD; 17 | import fi.iki.elonen.router.RouterNanoHTTPD; 18 | 19 | import java.util.ArrayList; 20 | import java.util.HashMap; 21 | import java.util.List; 22 | import java.util.Map; 23 | 24 | /** 25 | * Created by xdf on 02/05/2017. 26 | */ 27 | 28 | public class ActionController extends RouterNanoHTTPD.DefaultHandler { 29 | 30 | public static ActionController actions; 31 | 32 | private static UiDevice mDevice = Elements.getGlobal().getmDevice(); 33 | private static Elements elements = Elements.getGlobal(); 34 | 35 | static { 36 | actions = new ActionController() { 37 | @Override 38 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 39 | String sessionId = urlParams.get("sessionId"); 40 | Map body = new HashMap(); 41 | JSONObject response = null; 42 | try { 43 | session.parseBody(body); 44 | String postData = body.get("postData"); 45 | JSONObject jsonObj = JSON.parseObject(postData); 46 | JSONArray actions = (JSONArray)jsonObj.get("actions"); 47 | List queue = new ArrayList(); 48 | String lastGesture = ""; 49 | for (int i = 0; i < actions.size(); i++) { 50 | JSONObject action = actions.getJSONObject(i); 51 | String type = action.getString("type"); 52 | if (type.isEmpty()) { 53 | continue; 54 | } 55 | if (type.equals(lastGesture)) { 56 | if (queue.size() > 0) { 57 | JSONArray arr = queue.get(queue.size() - 1); 58 | arr.add(action); 59 | } 60 | } else { 61 | JSONArray arr = new JSONArray(); 62 | arr.add(action); 63 | queue.add(arr); 64 | } 65 | 66 | lastGesture = type; 67 | } 68 | for (JSONArray action: queue) { 69 | JSONObject first = action.getJSONObject(0); 70 | String type = first.getString("type"); 71 | boolean result = false; 72 | if (type.equals("tap")) { 73 | result = tap(action); 74 | } else if (type.equals("doubleTap")) { 75 | result = doubleTap(action); 76 | } else if (type.equals("press")) { 77 | result = press(action); 78 | } else if (type.equals("pinch")) { 79 | result = pinch(action); 80 | } else if (type.equals("drag")) { 81 | result = drag(action); 82 | } 83 | if(!result) { 84 | throw new Exception("Fail to execute action: " + type); 85 | } 86 | } 87 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(response, sessionId).toString()); 88 | } catch (final Exception e) { 89 | Utils.print(e.getMessage()); 90 | e.printStackTrace(); 91 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.UnknownError, sessionId).toString()); 92 | } 93 | } 94 | }; 95 | } 96 | 97 | @Override 98 | public String getMimeType() { 99 | return ""; 100 | } 101 | 102 | @Override 103 | public NanoHTTPD.Response.IStatus getStatus() { 104 | return NanoHTTPD.Response.Status.OK; 105 | } 106 | 107 | private static boolean tap(JSONObject args) throws Exception { 108 | String elementId = args.getString("element"); 109 | if (elementId != null) { 110 | Element el = elements.getElement(elementId); 111 | return el.tap(); 112 | } else { 113 | int x = args.getInteger("x"); 114 | int y = args.getInteger("y"); 115 | return mDevice.click(x, y); 116 | } 117 | } 118 | 119 | private static boolean tap(JSONArray actions) throws Exception { 120 | for (int i = 0; i < actions.size(); i++) { 121 | JSONObject action = actions.getJSONObject(i); 122 | if(!tap(action)) { 123 | return false; 124 | } 125 | } 126 | return true; 127 | } 128 | 129 | private static boolean doubleTap(JSONObject args) throws Exception { 130 | String elementId = args.getString("element"); 131 | if (elementId != null) { 132 | Element el = elements.getElement(elementId); 133 | return el.doubleTap(); 134 | } else { 135 | int x = args.getInteger("x"); 136 | int y = args.getInteger("y"); 137 | mDevice.click(x, y); 138 | Thread.sleep(100); 139 | mDevice.click(x, y); 140 | return true; 141 | } 142 | } 143 | 144 | private static boolean doubleTap(JSONArray actions) throws Exception { 145 | for (int i = 0; i < actions.size(); i++) { 146 | JSONObject action = actions.getJSONObject(i); 147 | if(!doubleTap(action)) { 148 | return false; 149 | } 150 | } 151 | return true; 152 | } 153 | 154 | private static boolean press(JSONObject args) throws Exception { 155 | String elementId = args.getString("element"); 156 | double duration = args.getDoubleValue("duration"); 157 | int steps = (int) Math.round(duration * 40); 158 | if (elementId != null) { 159 | Element el = elements.getElement(elementId); 160 | Rect elRect = el.getUiObject().getVisibleBounds(); 161 | return mDevice.swipe(elRect.centerX(), elRect.centerY(), elRect.centerX(), elRect.centerY(), steps); 162 | } else { 163 | int x = args.getInteger("x"); 164 | int y = args.getInteger("y"); 165 | return mDevice.swipe(x, y, x, y, steps); 166 | } 167 | } 168 | 169 | private static boolean press(JSONArray actions) throws Exception { 170 | for (int i = 0; i < actions.size(); i++) { 171 | JSONObject action = actions.getJSONObject(i); 172 | if(!press(action)) { 173 | return false; 174 | } 175 | } 176 | return true; 177 | } 178 | 179 | private static boolean pinch(JSONObject args) throws Exception { 180 | String elementId = args.getString("element"); 181 | Element el; 182 | if (elementId == null) { 183 | el = elements.getElement("1"); 184 | } else { 185 | el = elements.getElement(elementId); 186 | } 187 | String direction = args.getString("direction"); 188 | float percent = args.getFloat("percent"); 189 | int steps = args.getInteger("steps"); 190 | return el.pinch(direction, percent, steps); 191 | } 192 | 193 | private static boolean pinch(JSONArray actions) throws Exception { 194 | for (int i = 0; i < actions.size(); i++) { 195 | JSONObject action = actions.getJSONObject(i); 196 | if(!pinch(action)) { 197 | return false; 198 | } 199 | } 200 | return true; 201 | } 202 | 203 | private static boolean drag(JSONObject args) throws Exception { 204 | String elementId = args.getString("element"); 205 | Double fromX = args.getDouble("fromX"); 206 | Double fromY = args.getDouble("fromY"); 207 | Double toX = args.getDouble("toX"); 208 | Double toY = args.getDouble("toY"); 209 | double duration = args.getDoubleValue("duration"); 210 | int steps = (int) Math.round(duration * 40); 211 | if (elementId != null) { 212 | Element el = elements.getElement(elementId); 213 | return el.drag(toX.intValue(), toY.intValue(), steps); 214 | } else { 215 | boolean res = mDevice.drag(fromX.intValue(), fromY.intValue(), toX.intValue(), toY.intValue(), steps); 216 | Thread.sleep(steps * 100); 217 | return res; 218 | } 219 | } 220 | 221 | private static boolean drag(JSONArray actions) throws Exception { 222 | if (actions.size() == 1) { 223 | JSONObject action = actions.getJSONObject(0); 224 | return drag(action); 225 | } 226 | 227 | Point[] allPoint = new Point[actions.size() + 1]; 228 | int steps = 0; 229 | for (int i = 0; i < actions.size(); i++) { 230 | JSONObject action = actions.getJSONObject(i); 231 | if (i == 0) { 232 | String elementId = action.getString("element"); 233 | steps = action.getIntValue("steps"); 234 | if (steps == 0) { 235 | double duration = action.getDoubleValue("duration"); 236 | steps = (int) Math.round(duration * 40); 237 | } 238 | if (elementId != null) { 239 | Element el = elements.getElement(elementId); 240 | Rect elRect = el.getUiObject().getVisibleBounds(); 241 | Point p = new Point(elRect.centerX(), elRect.centerY()); 242 | allPoint[0] = p; 243 | } else { 244 | Double fromX = action.getDouble("fromX"); 245 | Double fromY = action.getDouble("fromY"); 246 | Point p = new Point(fromX.intValue(), fromY.intValue()); 247 | allPoint[0] = p; 248 | } 249 | } 250 | Double toX = action.getDouble("toX"); 251 | Double toY = action.getDouble("toY"); 252 | Point p = new Point(toX.intValue(), toY.intValue()); 253 | allPoint[i + 1] = p; 254 | } 255 | return mDevice.swipe(allPoint, steps); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/controllers/AlertController.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.controllers; 2 | 3 | import android.support.test.uiautomator.By; 4 | import android.support.test.uiautomator.UiDevice; 5 | import android.support.test.uiautomator.UiObject2; 6 | import android.support.test.uiautomator.UiObjectNotFoundException; 7 | 8 | import com.alibaba.fastjson.JSONObject; 9 | import com.macaca.android.testing.server.common.Elements; 10 | import com.macaca.android.testing.server.models.Response; 11 | import com.macaca.android.testing.server.models.Status; 12 | 13 | import fi.iki.elonen.NanoHTTPD; 14 | import fi.iki.elonen.router.RouterNanoHTTPD; 15 | 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | /** 20 | * Created by xdf on 02/05/2017. 21 | */ 22 | 23 | public class AlertController extends RouterNanoHTTPD.DefaultHandler { 24 | 25 | public static AlertController acceptAlert; 26 | public static AlertController dismissAlert; 27 | public static AlertController alertText; 28 | public static AlertController alertKeys; 29 | 30 | static { 31 | acceptAlert = new AlertController() { 32 | @Override 33 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 34 | String sessionId = urlParams.get("sessionId"); 35 | JSONObject result = null; 36 | try { 37 | acceptAlert(); 38 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(result, sessionId).toString()); 39 | } catch (final Exception e) { 40 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.UnknownError, sessionId).toString()); 41 | } 42 | } 43 | }; 44 | 45 | dismissAlert = new AlertController() { 46 | @Override 47 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 48 | String sessionId = urlParams.get("sessionId"); 49 | JSONObject result = null; 50 | try { 51 | dismissAlert(); 52 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(result, sessionId).toString()); 53 | } catch (final Exception e) { 54 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.UnknownError, sessionId).toString()); 55 | } 56 | } 57 | }; 58 | 59 | //TODO 60 | alertText = new AlertController() { 61 | @Override 62 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 63 | String sessionId = urlParams.get("sessionId"); 64 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 65 | } 66 | }; 67 | 68 | //TODO 69 | alertKeys = new AlertController() { 70 | @Override 71 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 72 | String sessionId = urlParams.get("sessionId"); 73 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 74 | } 75 | }; 76 | } 77 | 78 | @Override 79 | public String getMimeType() { 80 | return ""; 81 | } 82 | 83 | @Override 84 | public NanoHTTPD.Response.IStatus getStatus() { 85 | return NanoHTTPD.Response.Status.OK; 86 | } 87 | 88 | private static UiObject2 getAlertButton(String alertType) throws Exception { 89 | UiDevice mDevice = Elements.getGlobal().getmDevice(); 90 | int buttonIndex; 91 | if (alertType.equals("accept")) { 92 | buttonIndex = 1; 93 | } else if (alertType.equals("dismiss")) { 94 | buttonIndex = 0; 95 | } else { 96 | throw new Exception("alertType can only be 'accept' or 'dismiss'"); 97 | } 98 | 99 | List alertButtons = mDevice.findObjects(By.clazz("android.widget.Button").clickable(true).checkable(false)); 100 | if (alertButtons.size() == 0) { 101 | return null; 102 | } 103 | UiObject2 alertButton = alertButtons.get(buttonIndex); 104 | 105 | return alertButton; 106 | } 107 | 108 | private static void acceptAlert() throws Exception { 109 | UiObject2 alertButton = getAlertButton("accept"); 110 | if (alertButton != null) { 111 | alertButton.click(); 112 | } 113 | } 114 | 115 | private static void dismissAlert() throws Exception { 116 | UiObject2 alertButton = getAlertButton("dismiss"); 117 | if (alertButton != null) { 118 | alertButton.click(); 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/controllers/ContextController.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.controllers; 2 | 3 | import com.macaca.android.testing.server.models.Response; 4 | import com.macaca.android.testing.server.models.Status; 5 | 6 | import fi.iki.elonen.NanoHTTPD; 7 | import fi.iki.elonen.router.RouterNanoHTTPD; 8 | 9 | import java.util.Map; 10 | 11 | /** 12 | * Created by xdf on 02/05/2017. 13 | */ 14 | 15 | //TODO 16 | public class ContextController extends RouterNanoHTTPD.DefaultHandler { 17 | 18 | public static ContextController getContext; 19 | public static ContextController setContext; 20 | public static ContextController getContexts; 21 | 22 | static { 23 | getContext = new ContextController() { 24 | @Override 25 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 26 | String sessionId = urlParams.get("sessionId"); 27 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 28 | } 29 | }; 30 | 31 | setContext = new ContextController() { 32 | @Override 33 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 34 | String sessionId = urlParams.get("sessionId"); 35 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 36 | } 37 | }; 38 | 39 | getContexts = new ContextController() { 40 | @Override 41 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 42 | String sessionId = urlParams.get("sessionId"); 43 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 44 | } 45 | }; 46 | } 47 | 48 | @Override 49 | public String getMimeType() { 50 | return ""; 51 | } 52 | 53 | @Override 54 | public NanoHTTPD.Response.IStatus getStatus() { 55 | return NanoHTTPD.Response.Status.OK; 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/controllers/ElementController.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.controllers; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONArray; 5 | import com.alibaba.fastjson.JSONException; 6 | import com.alibaba.fastjson.JSONObject; 7 | 8 | import com.macaca.android.testing.server.models.Response; 9 | import com.macaca.android.testing.server.models.Status; 10 | import com.macaca.android.testing.server.common.Element; 11 | import com.macaca.android.testing.server.common.Elements; 12 | import com.macaca.android.testing.server.xmlUtils.InteractionController; 13 | import com.macaca.android.testing.server.xmlUtils.NodeInfoList; 14 | import com.macaca.android.testing.server.xmlUtils.ReflectionUtils; 15 | import com.macaca.android.testing.server.xmlUtils.UiAutomatorBridge; 16 | import com.macaca.android.testing.server.xmlUtils.XPathSelector; 17 | import com.macaca.android.testing.server.xmlUtils.MUiDevice; 18 | 19 | import android.graphics.Rect; 20 | import android.support.test.uiautomator.By; 21 | import android.support.test.uiautomator.BySelector; 22 | import android.support.test.uiautomator.InstrumentationUiAutomatorBridge; 23 | import android.support.test.uiautomator.UiDevice; 24 | import android.support.test.uiautomator.UiObject2; 25 | import android.support.test.uiautomator.UiObjectNotFoundException; 26 | import android.view.KeyEvent; 27 | import android.view.accessibility.AccessibilityNodeInfo; 28 | 29 | import fi.iki.elonen.NanoHTTPD; 30 | import fi.iki.elonen.router.RouterNanoHTTPD; 31 | 32 | import java.lang.reflect.Field; 33 | import java.lang.reflect.InvocationTargetException; 34 | import java.util.ArrayList; 35 | import java.util.HashMap; 36 | import java.util.Iterator; 37 | import java.util.List; 38 | import java.util.Map; 39 | 40 | /** 41 | * Created by xdf on 02/05/2017. 42 | */ 43 | 44 | public class ElementController extends RouterNanoHTTPD.DefaultHandler { 45 | 46 | public static ElementController click; 47 | public static ElementController findElement; 48 | public static ElementController findElements; 49 | public static ElementController setValue; 50 | public static ElementController getText; 51 | public static ElementController clearText; 52 | public static ElementController isDisplayed; 53 | public static ElementController getAttribute; 54 | public static ElementController getComputedCss; 55 | public static ElementController getRect; 56 | 57 | private static Elements elements = Elements.getGlobal(); 58 | 59 | static { 60 | click = new ElementController() { 61 | @Override 62 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 63 | String sessionId = urlParams.get("sessionId"); 64 | String elementId = urlParams.get("elementId"); 65 | JSONObject result = null; 66 | try { 67 | if (elementId != null) { 68 | Element el = getElements().getElement(elementId); 69 | el.click(); 70 | } else { 71 | Element el = getElements().getElement("1"); 72 | el.click(); 73 | } 74 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(result, sessionId).toString()); 75 | } catch (final Exception e) { 76 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 77 | } 78 | 79 | } 80 | }; 81 | 82 | findElement = new ElementController() { 83 | @Override 84 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 85 | String sessionId = urlParams.get("sessionId"); 86 | Map body = new HashMap(); 87 | JSONObject result = new JSONObject(); 88 | try { 89 | session.parseBody(body); 90 | String value = body.get("postData"); 91 | JSONObject postData = JSON.parseObject(value); 92 | String strategy = (String) postData.get("using"); 93 | String text = (String) postData.get("value"); 94 | strategy = strategy.trim().replace(" ", "_").toUpperCase(); 95 | if (strategy.equals("XPATH")) { 96 | try { 97 | UiObject2 uiObject2 = getXPathUiObject(text); 98 | Element element = getElements().addElement(uiObject2); 99 | result.put("ELEMENT", element.getId()); 100 | } catch (Exception e) { 101 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 102 | } 103 | } else { 104 | try { 105 | BySelector selector = getSelector(strategy, text); 106 | result = getOneElement(selector); 107 | } catch (Exception e) { 108 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.InvalidSelector, sessionId).toString()); 109 | } 110 | } 111 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(result, sessionId).toString()); 112 | 113 | } catch (Exception e) { 114 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 115 | } 116 | } 117 | }; 118 | 119 | findElements = new ElementController() { 120 | @Override 121 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 122 | String sessionId = urlParams.get("sessionId"); 123 | Map body = new HashMap(); 124 | JSONArray result = null; 125 | try { 126 | session.parseBody(body); 127 | String value = body.get("postData"); 128 | JSONObject postData = JSON.parseObject(value); 129 | String strategy = (String) postData.get("using"); 130 | String text = (String) postData.get("value"); 131 | strategy = strategy.trim().replace(" ", "_").toUpperCase(); 132 | if (strategy.equals("XPATH")) { 133 | List uiObject2s = getXPathUiObjects(text); 134 | List elements = getElements().addElements(uiObject2s); 135 | result = elementsToJSONArray(elements); 136 | } else { 137 | try { 138 | BySelector selector = getSelector(strategy, text); 139 | result = getMultiElements(selector); 140 | } catch (Exception e) { 141 | e.printStackTrace(); 142 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.InvalidSelector, sessionId).toString()); 143 | } 144 | } 145 | } catch (Exception e) { 146 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(new JSONArray(), sessionId).toString()); 147 | } 148 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(result, sessionId).toString()); 149 | } 150 | }; 151 | 152 | setValue = new ElementController() { 153 | @Override 154 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 155 | String sessionId = urlParams.get("sessionId"); 156 | String elementId = urlParams.get("elementId"); 157 | Map body = new HashMap<>(); 158 | JSONObject result = null; 159 | try { 160 | 161 | /** 162 | * Code in UIObject2 line 601, currentText cause npe: 163 | * if (!node.performAction(AccessibilityNodeInfo.ACTION_SET_SELECTION, args) && 164 | * currentText.length() > 0) { 165 | * */ 166 | Element element = getElements().getElement(elementId); 167 | AccessibilityNodeInfo info = (AccessibilityNodeInfo)ReflectionUtils.getField(UiObject2.class,"mCachedNode",element.element); 168 | if (info != null && info.getText() == null) { 169 | ReflectionUtils.setField(AccessibilityNodeInfo.class,"mText", info, ""); 170 | } 171 | 172 | // Conduct regular text set 173 | session.parseBody(body); 174 | String postData = body.get("postData"); 175 | JSONObject jsonObj = JSON.parseObject(postData); 176 | JSONArray values = (JSONArray) jsonObj.get("value"); 177 | for (Iterator iterator = values.iterator(); iterator.hasNext(); ) { 178 | String value = (String) iterator.next(); 179 | element.setText(value); 180 | } 181 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(result, sessionId).toString()); 182 | } catch (final UiObjectNotFoundException e) { 183 | e.printStackTrace(); 184 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 185 | } catch (final Exception e) { 186 | e.printStackTrace(); 187 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.UnknownError, sessionId).toString()); 188 | } 189 | } 190 | }; 191 | 192 | getText = new ElementController() { 193 | @Override 194 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 195 | String sessionId = urlParams.get("sessionId"); 196 | try { 197 | String elementId = urlParams.get("elementId"); 198 | Element element = Elements.getGlobal().getElement(elementId); 199 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(element.getText(), sessionId).toString()); 200 | } catch (final Exception e) { 201 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.UnknownError, sessionId).toString()); 202 | } 203 | } 204 | }; 205 | 206 | clearText = new ElementController() { 207 | @Override 208 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 209 | String sessionId = urlParams.get("sessionId"); 210 | String elementId = (String) urlParams.get("elementId"); 211 | Element el = Elements.getGlobal().getElement(elementId); 212 | JSONObject result = null; 213 | try { 214 | el.clearText(); 215 | if (el.getText() == null) { 216 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(result, sessionId).toString()); 217 | } 218 | if (el.getText().isEmpty()) { 219 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(result, sessionId).toString()); 220 | } 221 | if (hasHintText(el)) { 222 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(result, sessionId).toString()); 223 | } 224 | if (sendDeleteKeys(el)) { 225 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(result, sessionId).toString()); 226 | } 227 | if (!el.getText().isEmpty()) { 228 | if (hasHintText(el)) { 229 | System.out.println("The text should be the hint text"); 230 | } else if (!el.getText().isEmpty()) { 231 | System.out.println("oh my god. Can't clear the Text"); 232 | } 233 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(result, sessionId).toString()); 234 | } 235 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(result, sessionId).toString()); 236 | } catch (final UiObjectNotFoundException e) { 237 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 238 | } catch (final Exception e) { 239 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.UnknownError, sessionId).toString()); 240 | } 241 | } 242 | }; 243 | 244 | isDisplayed = new ElementController() { 245 | @Override 246 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 247 | String sessionId = urlParams.get("sessionId"); 248 | String elementId = urlParams.get("elementId"); 249 | try { 250 | Element el = Elements.getGlobal().getElement(elementId); 251 | boolean isDisplayed = el.isDisplayed(); 252 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(isDisplayed, sessionId).toString()); 253 | } catch (final UiObjectNotFoundException e) { 254 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 255 | } catch (final Exception e) { 256 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.UnknownError, sessionId).toString()); 257 | } 258 | } 259 | }; 260 | 261 | getAttribute = new ElementController() { 262 | @Override 263 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 264 | String sessionId = urlParams.get("sessionId"); 265 | String elementId = urlParams.get("elementId"); 266 | try { 267 | Element el = Elements.getGlobal().getElement(elementId); 268 | JSONObject props = new JSONObject(); 269 | props.put("text", el.element.getText()); 270 | props.put("description", el.element.getContentDescription()); 271 | props.put("enabled", el.element.isEnabled()); 272 | props.put("checkable", el.element.isCheckable()); 273 | props.put("checked", el.element.isChecked()); 274 | props.put("clickable", el.element.isClickable()); 275 | props.put("focusable", el.element.isFocusable()); 276 | props.put("focused", el.element.isFocused()); 277 | props.put("longClickable", el.element.isLongClickable()); 278 | props.put("scrollable", el.element.isScrollable()); 279 | props.put("selected", el.element.isSelected()); 280 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(props, sessionId).toString()); 281 | } catch (final Exception e) { 282 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.UnknownError, sessionId).toString()); 283 | } 284 | } 285 | }; 286 | 287 | getRect = new ElementController() { 288 | @Override 289 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 290 | String sessionId = urlParams.get("sessionId"); 291 | String elementId = urlParams.get("elementId"); 292 | try { 293 | Element el = Elements.getGlobal().getElement(elementId); 294 | final Rect rect = el.element.getVisibleBounds(); 295 | JSONObject res = new JSONObject(); 296 | res.put("x", rect.left); 297 | res.put("y", rect.top); 298 | res.put("height", rect.height()); 299 | res.put("width", rect.width()); 300 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(res, sessionId).toString()); 301 | } catch (final Exception e) { 302 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.UnknownError, sessionId).toString()); 303 | } 304 | } 305 | }; 306 | } 307 | 308 | @Override 309 | public String getMimeType() { 310 | return ""; 311 | } 312 | 313 | @Override 314 | public NanoHTTPD.Response.IStatus getStatus() { 315 | return NanoHTTPD.Response.Status.OK; 316 | } 317 | 318 | private static JSONObject getOneElement(final BySelector sel) throws Exception { 319 | final JSONObject res = new JSONObject(); 320 | final Element element = getElements().getElement(sel); 321 | res.put("ELEMENT", element.getId()); 322 | return res; 323 | } 324 | 325 | private static JSONArray getMultiElements(final BySelector sel) throws Exception { 326 | JSONArray res; 327 | List foundElements = new ArrayList(); 328 | final List elementsFromSelector = getElements().getMultiElement(sel); 329 | foundElements.addAll(elementsFromSelector); 330 | res = elementsToJSONArray(foundElements); 331 | return res; 332 | } 333 | 334 | private static UiObject2 getXPathUiObject(String expression) throws Exception { 335 | final NodeInfoList nodeList = XPathSelector.getNodesList(expression); 336 | if (nodeList.size() == 0) { 337 | throw new Exception(Status.XPathLookupError.getStatusDes()); 338 | } 339 | return MUiDevice.getInstance().findObject(nodeList); 340 | } 341 | 342 | private static List getXPathUiObjects(String expression) throws Exception { 343 | final NodeInfoList nodeList = XPathSelector.getNodesList(expression); 344 | if (nodeList.size() == 0) { 345 | throw new Exception(Status.XPathLookupError.getStatusDes()); 346 | } 347 | return MUiDevice.getInstance().findObjects(nodeList); 348 | } 349 | 350 | private static BySelector getSelector(String strategy, String text) throws Exception { 351 | BySelector selector = null; 352 | switch (strategy) { 353 | case "CLASS_NAME": 354 | selector = By.clazz(text); 355 | break; 356 | case "NAME": 357 | selector = By.desc(text); 358 | if(selector == null || elements.getmDevice().findObject(selector) == null){ 359 | selector = By.text(text); 360 | } 361 | break; 362 | case "ID": 363 | selector = By.res(text); 364 | break; 365 | case "TEXT_CONTAINS": 366 | selector = By.textContains(text); 367 | break; 368 | case "DESC_CONTAINS": 369 | selector = By.descContains(text); 370 | break; 371 | } 372 | return selector; 373 | } 374 | 375 | private static JSONArray elementsToJSONArray(final List elems) 376 | throws JSONException { 377 | JSONArray resArray = new JSONArray(); 378 | for (Element element : elems) { 379 | JSONObject jsonObject = new JSONObject(); 380 | jsonObject.put("ELEMENT", element.getId()); 381 | resArray.add(jsonObject); 382 | } 383 | return resArray; 384 | } 385 | 386 | private static boolean hasHintText(Element el) 387 | throws UiObjectNotFoundException, IllegalAccessException, 388 | InvocationTargetException, NoSuchMethodException { 389 | 390 | String currText = el.getText(); 391 | try { 392 | if (!el.getUiObject().isFocused()) { 393 | System.out.println("Could not check for hint text because the element is not focused!"); 394 | return false; 395 | } 396 | } catch (final Exception e) { 397 | System.out.println("Could not check for hint text: " + e.getMessage()); 398 | return false; 399 | } 400 | 401 | try { 402 | InteractionController interactionController = UiAutomatorBridge.getInstance().getInteractionController(); 403 | interactionController.sendKey(KeyEvent.KEYCODE_DEL, 0); 404 | interactionController.sendKey(KeyEvent.KEYCODE_FORWARD_DEL, 0); 405 | } catch (Exception e) { 406 | System.out.println("UiAutomatorBridge.getInteractionController error happen!"); 407 | } 408 | 409 | return currText.equals(el.getText()); 410 | } 411 | 412 | private static boolean sendDeleteKeys(Element el) 413 | throws UiObjectNotFoundException, IllegalAccessException, 414 | InvocationTargetException, NoSuchMethodException { 415 | String tempTextHolder = ""; 416 | 417 | while (!el.getText().isEmpty() && !tempTextHolder.equalsIgnoreCase(el.getText())) { 418 | el.click(); 419 | 420 | for (int key : new int[]{KeyEvent.KEYCODE_DEL, KeyEvent.KEYCODE_FORWARD_DEL}) { 421 | tempTextHolder = el.getText(); 422 | final int length = tempTextHolder.length(); 423 | for (int count = 0; count < length; count++) { 424 | try { 425 | InteractionController interactionController = UiAutomatorBridge.getInstance().getInteractionController(); 426 | interactionController.sendKey(key, 0); 427 | } catch (Exception e) { 428 | System.out.println("UiAutomatorBridge.getInteractionController error happen!"); 429 | } 430 | } 431 | } 432 | } 433 | return el.getText().isEmpty(); 434 | } 435 | public static Elements getElements() { 436 | return elements; 437 | } 438 | } 439 | 440 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/controllers/ExecuteController.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.controllers; 2 | 3 | import com.macaca.android.testing.server.models.Response; 4 | import com.macaca.android.testing.server.models.Status; 5 | 6 | import fi.iki.elonen.NanoHTTPD; 7 | import fi.iki.elonen.router.RouterNanoHTTPD; 8 | 9 | import java.util.Map; 10 | 11 | /** 12 | * Created by xdf on 02/05/2017. 13 | */ 14 | 15 | //TODO 16 | public class ExecuteController extends RouterNanoHTTPD.DefaultHandler { 17 | 18 | public static ExecuteController execute; 19 | 20 | static { 21 | execute = new ExecuteController() { 22 | @Override 23 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 24 | String sessionId = urlParams.get("sessionId"); 25 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 26 | } 27 | }; 28 | } 29 | 30 | @Override 31 | public String getMimeType() { 32 | return ""; 33 | } 34 | 35 | @Override 36 | public NanoHTTPD.Response.IStatus getStatus() { 37 | return NanoHTTPD.Response.Status.OK; 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/controllers/HomeController.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.controllers; 2 | 3 | import com.macaca.android.testing.server.models.Response; 4 | import com.macaca.android.testing.server.models.Status; 5 | 6 | import fi.iki.elonen.NanoHTTPD; 7 | import fi.iki.elonen.router.RouterNanoHTTPD; 8 | 9 | import java.util.Map; 10 | 11 | /** 12 | * Created by xdf on 02/05/2017. 13 | */ 14 | 15 | public class HomeController extends RouterNanoHTTPD.DefaultHandler { 16 | 17 | public static HomeController home; 18 | 19 | static { 20 | home = new HomeController() { 21 | @Override 22 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 23 | String sessionId = urlParams.get("sessionId"); 24 | 25 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 26 | } 27 | }; 28 | } 29 | 30 | @Override 31 | public String getMimeType() { 32 | return ""; 33 | } 34 | 35 | @Override 36 | public NanoHTTPD.Response.IStatus getStatus() { 37 | return NanoHTTPD.Response.Status.OK; 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/controllers/KeysController.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.controllers; 2 | 3 | import android.support.test.uiautomator.UiDevice; 4 | 5 | import com.alibaba.fastjson.JSON; 6 | import com.alibaba.fastjson.JSONArray; 7 | import com.alibaba.fastjson.JSONObject; 8 | import com.macaca.android.testing.server.common.Elements; 9 | import com.macaca.android.testing.server.models.Response; 10 | import com.macaca.android.testing.server.models.Status; 11 | 12 | import fi.iki.elonen.NanoHTTPD; 13 | import fi.iki.elonen.router.RouterNanoHTTPD; 14 | 15 | import java.util.HashMap; 16 | import java.util.Iterator; 17 | import java.util.Map; 18 | 19 | /** 20 | * Created by xdf on 02/05/2017. 21 | */ 22 | 23 | public class KeysController extends RouterNanoHTTPD.DefaultHandler { 24 | 25 | public static KeysController keys; 26 | 27 | static { 28 | keys = new KeysController() { 29 | @Override 30 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 31 | String sessionId = urlParams.get("sessionId"); 32 | Map body = new HashMap(); 33 | UiDevice mDevice = Elements.getGlobal().getmDevice(); 34 | JSONObject result = null; 35 | try { 36 | session.parseBody(body); 37 | String postData = body.get("postData"); 38 | JSONObject jsonObj = JSON.parseObject(postData); 39 | JSONArray keycodes = (JSONArray)jsonObj.get("value"); 40 | for (Iterator iterator = keycodes.iterator(); iterator.hasNext();) { 41 | int keycode = (int) iterator.next(); 42 | mDevice.pressKeyCode(keycode); 43 | } 44 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(result, sessionId).toString()); 45 | } catch (Exception e) { 46 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.UnknownError, sessionId).toString()); 47 | } 48 | } 49 | }; 50 | } 51 | 52 | @Override 53 | public String getMimeType() { 54 | return ""; 55 | } 56 | 57 | @Override 58 | public NanoHTTPD.Response.IStatus getStatus() { 59 | return NanoHTTPD.Response.Status.OK; 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/controllers/ScreenshotController.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.controllers; 2 | 3 | import com.macaca.android.testing.server.models.Response; 4 | import com.macaca.android.testing.server.models.Status; 5 | 6 | import fi.iki.elonen.NanoHTTPD; 7 | import fi.iki.elonen.router.RouterNanoHTTPD; 8 | 9 | import java.util.Map; 10 | 11 | /** 12 | * Created by xdf on 02/05/2017. 13 | */ 14 | 15 | //TODO 16 | public class ScreenshotController extends RouterNanoHTTPD.DefaultHandler { 17 | 18 | public static ScreenshotController getScreenshot; 19 | 20 | static { 21 | getScreenshot = new ScreenshotController() { 22 | @Override 23 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 24 | String sessionId = urlParams.get("sessionId"); 25 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 26 | } 27 | }; 28 | } 29 | 30 | @Override 31 | public String getMimeType() { 32 | return ""; 33 | } 34 | 35 | @Override 36 | public NanoHTTPD.Response.IStatus getStatus() { 37 | return NanoHTTPD.Response.Status.OK; 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/controllers/SessionController.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.controllers; 2 | 3 | import com.macaca.android.testing.server.models.Response; 4 | import com.macaca.android.testing.server.models.Status; 5 | 6 | import fi.iki.elonen.NanoHTTPD; 7 | import fi.iki.elonen.router.RouterNanoHTTPD; 8 | 9 | import java.util.Map; 10 | 11 | /** 12 | * Created by xdf on 02/05/2017. 13 | */ 14 | 15 | //TODO 16 | public class SessionController extends RouterNanoHTTPD.DefaultHandler { 17 | 18 | public static SessionController sessionAvailable; 19 | public static SessionController createSession; 20 | public static SessionController getSessions; 21 | public static SessionController delSession; 22 | 23 | static { 24 | sessionAvailable = new SessionController() { 25 | @Override 26 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 27 | String sessionId = urlParams.get("sessionId"); 28 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 29 | } 30 | }; 31 | 32 | createSession = new SessionController() { 33 | @Override 34 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 35 | String sessionId = urlParams.get("sessionId"); 36 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 37 | } 38 | }; 39 | 40 | getSessions = new SessionController() { 41 | @Override 42 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 43 | String sessionId = urlParams.get("sessionId"); 44 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 45 | } 46 | }; 47 | 48 | delSession = new SessionController() { 49 | @Override 50 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 51 | String sessionId = urlParams.get("sessionId"); 52 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 53 | } 54 | }; 55 | } 56 | 57 | @Override 58 | public String getMimeType() { 59 | return ""; 60 | } 61 | 62 | @Override 63 | public NanoHTTPD.Response.IStatus getStatus() { 64 | return NanoHTTPD.Response.Status.OK; 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/controllers/SourceController.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.controllers; 2 | 3 | import android.os.Environment; 4 | import android.support.test.uiautomator.UiDevice; 5 | 6 | import com.macaca.android.testing.server.common.Elements; 7 | import com.macaca.android.testing.server.models.Response; 8 | 9 | import org.apache.http.util.EncodingUtils; 10 | 11 | import fi.iki.elonen.NanoHTTPD; 12 | import fi.iki.elonen.router.RouterNanoHTTPD; 13 | 14 | import java.io.File; 15 | import java.io.FileInputStream; 16 | import java.io.IOException; 17 | import java.util.Map; 18 | 19 | 20 | /** 21 | * Created by xdf on 02/05/2017. 22 | */ 23 | 24 | public class SourceController extends RouterNanoHTTPD.DefaultHandler { 25 | 26 | public static SourceController source; 27 | UiDevice mDevice = Elements.getGlobal().getmDevice(); 28 | 29 | static { 30 | source = new SourceController() { 31 | private static final String dumpFileName = "macaca-dump.xml"; 32 | 33 | @Override 34 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 35 | String sessionId = urlParams.get("sessionId"); 36 | final File dump = new File(Environment.getDataDirectory() + File.separator + "local" + File.separator + "tmp" + File.separator + dumpFileName); 37 | try { 38 | if (!dump.delete()) { 39 | System.out.println("--> remove file failed<--"); 40 | } 41 | 42 | mDevice.dumpWindowHierarchy(dump); 43 | 44 | if (!dump.setReadable(true,false) && !dump.setWritable(true,false)) { 45 | System.out.println("--> permission update failure <--"); 46 | } else { 47 | System.out.println("--> permission update success <--"); 48 | } 49 | 50 | } catch (Exception e) { 51 | System.out.println("--> dump exception <--"); 52 | e.printStackTrace(); 53 | } 54 | String res = ""; 55 | try { 56 | FileInputStream fin = new FileInputStream(dump.getAbsolutePath()); 57 | int length = fin.available(); 58 | byte[] buffer = new byte[length]; 59 | fin.read(buffer); 60 | res = EncodingUtils.getString(buffer, "UTF-8"); 61 | fin.close(); 62 | } catch (Exception e) { 63 | System.out.println("--> file access exception <--"); 64 | e.printStackTrace(); 65 | } 66 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(res, sessionId).toString()); 67 | } 68 | }; 69 | } 70 | 71 | @Override 72 | public String getMimeType() { 73 | return ""; 74 | } 75 | 76 | @Override 77 | public NanoHTTPD.Response.IStatus getStatus() { 78 | return NanoHTTPD.Response.Status.OK; 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/controllers/StatusController.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.controllers; 2 | 3 | import com.macaca.android.testing.server.models.Response; 4 | import com.macaca.android.testing.server.models.Status; 5 | 6 | import fi.iki.elonen.NanoHTTPD; 7 | import fi.iki.elonen.router.RouterNanoHTTPD; 8 | 9 | import java.util.Map; 10 | 11 | /** 12 | * Created by xdf on 02/05/2017. 13 | */ 14 | 15 | //TODO 16 | public class StatusController extends RouterNanoHTTPD.DefaultHandler { 17 | 18 | public static StatusController status; 19 | 20 | static { 21 | status = new StatusController() { 22 | @Override 23 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 24 | String sessionId = urlParams.get("sessionId"); 25 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 26 | } 27 | }; 28 | } 29 | 30 | @Override 31 | public String getMimeType() { 32 | return ""; 33 | } 34 | 35 | @Override 36 | public NanoHTTPD.Response.IStatus getStatus() { 37 | return NanoHTTPD.Response.Status.OK; 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/controllers/TimeoutsController.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.controllers; 2 | 3 | import com.macaca.android.testing.server.models.Response; 4 | import com.macaca.android.testing.server.models.Status; 5 | 6 | import fi.iki.elonen.NanoHTTPD; 7 | import fi.iki.elonen.router.RouterNanoHTTPD; 8 | 9 | import java.util.Map; 10 | 11 | /** 12 | * Created by xdf on 02/05/2017. 13 | */ 14 | 15 | //TODO 16 | public class TimeoutsController extends RouterNanoHTTPD.DefaultHandler { 17 | 18 | public static TimeoutsController implicitWait; 19 | 20 | static { 21 | implicitWait = new TimeoutsController() { 22 | @Override 23 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 24 | String sessionId = urlParams.get("sessionId"); 25 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 26 | } 27 | }; 28 | } 29 | 30 | @Override 31 | public String getMimeType() { 32 | return ""; 33 | } 34 | 35 | @Override 36 | public NanoHTTPD.Response.IStatus getStatus() { 37 | return NanoHTTPD.Response.Status.OK; 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/controllers/TitleController.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.controllers; 2 | 3 | import com.macaca.android.testing.server.models.Response; 4 | import com.macaca.android.testing.server.models.Status; 5 | 6 | import fi.iki.elonen.NanoHTTPD; 7 | import fi.iki.elonen.router.RouterNanoHTTPD; 8 | 9 | import java.util.Map; 10 | 11 | /** 12 | * Created by xdf on 02/05/2017. 13 | */ 14 | 15 | //TODO 16 | public class TitleController extends RouterNanoHTTPD.DefaultHandler { 17 | 18 | public static TitleController title; 19 | 20 | static { 21 | title = new TitleController() { 22 | @Override 23 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 24 | String sessionId = urlParams.get("sessionId"); 25 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 26 | } 27 | }; 28 | } 29 | 30 | @Override 31 | public String getMimeType() { 32 | return ""; 33 | } 34 | 35 | @Override 36 | public NanoHTTPD.Response.IStatus getStatus() { 37 | return NanoHTTPD.Response.Status.OK; 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/controllers/UrlController.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.controllers; 2 | 3 | import com.macaca.android.testing.server.models.Response; 4 | import com.macaca.android.testing.server.models.Status; 5 | 6 | import fi.iki.elonen.NanoHTTPD; 7 | import fi.iki.elonen.router.RouterNanoHTTPD; 8 | 9 | import java.util.Map; 10 | 11 | /** 12 | * Created by xdf on 02/05/2017. 13 | */ 14 | 15 | //TODO 16 | public class UrlController extends RouterNanoHTTPD.DefaultHandler { 17 | 18 | public static UrlController url; 19 | public static UrlController getUrl; 20 | public static UrlController forward; 21 | public static UrlController back; 22 | public static UrlController refresh; 23 | 24 | static { 25 | url = new UrlController() { 26 | @Override 27 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 28 | String sessionId = urlParams.get("sessionId"); 29 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 30 | } 31 | }; 32 | 33 | getUrl = new UrlController() { 34 | @Override 35 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 36 | String sessionId = urlParams.get("sessionId"); 37 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 38 | } 39 | }; 40 | 41 | forward = new UrlController() { 42 | @Override 43 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 44 | String sessionId = urlParams.get("sessionId"); 45 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 46 | } 47 | }; 48 | 49 | back = new UrlController() { 50 | @Override 51 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 52 | String sessionId = urlParams.get("sessionId"); 53 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 54 | } 55 | }; 56 | 57 | refresh = new UrlController() { 58 | @Override 59 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 60 | String sessionId = urlParams.get("sessionId"); 61 | 62 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 63 | } 64 | }; 65 | } 66 | 67 | @Override 68 | public String getMimeType() { 69 | return ""; 70 | } 71 | 72 | @Override 73 | public NanoHTTPD.Response.IStatus getStatus() { 74 | return NanoHTTPD.Response.Status.OK; 75 | } 76 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/controllers/WindowController.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.controllers; 2 | 3 | import android.support.test.uiautomator.UiDevice; 4 | 5 | import com.macaca.android.testing.server.common.Elements; 6 | import com.macaca.android.testing.server.models.Response; 7 | import com.macaca.android.testing.server.models.Status; 8 | 9 | import com.alibaba.fastjson.JSONObject; 10 | 11 | import fi.iki.elonen.NanoHTTPD; 12 | import fi.iki.elonen.router.RouterNanoHTTPD; 13 | 14 | import java.util.Map; 15 | 16 | /** 17 | * Created by xdf on 02/05/2017. 18 | */ 19 | 20 | public class WindowController extends RouterNanoHTTPD.DefaultHandler { 21 | 22 | public static WindowController getWindow; 23 | public static WindowController getWindows; 24 | public static WindowController setWindow; 25 | public static WindowController deleteWindow; 26 | public static WindowController getWindowSize; 27 | public static WindowController setWindowSize; 28 | public static WindowController maximize; 29 | public static WindowController setFrame; 30 | 31 | static { 32 | //TODO 33 | getWindow = new WindowController() { 34 | @Override 35 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 36 | String sessionId = urlParams.get("sessionId"); 37 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(new JSONObject(), sessionId).toString()); 38 | } 39 | }; 40 | 41 | //TODO 42 | getWindows = new WindowController() { 43 | @Override 44 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 45 | String sessionId = urlParams.get("sessionId"); 46 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 47 | } 48 | }; 49 | 50 | //TODO 51 | setWindow = new WindowController() { 52 | @Override 53 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 54 | String sessionId = urlParams.get("sessionId"); 55 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 56 | } 57 | }; 58 | 59 | //TODO 60 | deleteWindow = new WindowController() { 61 | @Override 62 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 63 | String sessionId = urlParams.get("sessionId"); 64 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 65 | } 66 | }; 67 | 68 | getWindowSize = new WindowController() { 69 | @Override 70 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 71 | String sessionId = urlParams.get("sessionId"); 72 | try { 73 | UiDevice mDevice = Elements.getGlobal().getmDevice(); 74 | Integer width = mDevice.getDisplayWidth(); 75 | Integer height = mDevice.getDisplayHeight(); 76 | JSONObject size = new JSONObject(); 77 | size.put("width", width); 78 | size.put("height", height); 79 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(size, sessionId).toString()); 80 | } catch(Exception e) { 81 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.UnknownError, sessionId).toString()); 82 | } 83 | } 84 | }; 85 | 86 | //TODO 87 | setWindowSize = new WindowController() { 88 | @Override 89 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 90 | String sessionId = urlParams.get("sessionId"); 91 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 92 | } 93 | }; 94 | 95 | //TODO 96 | maximize = new WindowController() { 97 | @Override 98 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 99 | String sessionId = urlParams.get("sessionId"); 100 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 101 | } 102 | }; 103 | 104 | //TODO 105 | setFrame = new WindowController() { 106 | @Override 107 | public NanoHTTPD.Response get(RouterNanoHTTPD.UriResource uriResource, Map urlParams, NanoHTTPD.IHTTPSession session) { 108 | String sessionId = urlParams.get("sessionId"); 109 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), new Response(Status.NoSuchElement, sessionId).toString()); 110 | } 111 | }; 112 | } 113 | 114 | @Override 115 | public String getMimeType() { 116 | return ""; 117 | } 118 | 119 | @Override 120 | public NanoHTTPD.Response.IStatus getStatus() { 121 | return NanoHTTPD.Response.Status.OK; 122 | } 123 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/models/Methods.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.models; 2 | 3 | /** 4 | * Created by xdf on 03/05/2017. 5 | */ 6 | 7 | public interface Methods { 8 | String GET = "get"; 9 | String POST = "post"; 10 | String PUT = "put"; 11 | String DELETE = "delete"; 12 | } 13 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/models/Response.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.models; 2 | 3 | import com.alibaba.fastjson.JSONArray; 4 | import com.alibaba.fastjson.JSONException; 5 | import com.alibaba.fastjson.JSONObject; 6 | 7 | /** 8 | * Created by xdf on 03/05/2017. 9 | */ 10 | 11 | public class Response { 12 | 13 | private int status; 14 | 15 | private Object value; 16 | 17 | private String sessionId; 18 | 19 | public Response(Status status, String sessionId) { 20 | this.status = status.getStatusCode(); 21 | this.value = status.getStatusDes(); 22 | this.sessionId = sessionId; 23 | } 24 | 25 | public Response(JSONObject value, String sessionId) { 26 | this.status = 0; 27 | this.value = value; 28 | this.sessionId = sessionId; 29 | } 30 | 31 | public Response(JSONArray value, String sessionId) { 32 | this.status = 0; 33 | this.value = value; 34 | this.sessionId = sessionId; 35 | } 36 | 37 | public Response(String value, String sessionId) { 38 | this.status = 0; 39 | this.value = value; 40 | this.sessionId = sessionId; 41 | } 42 | 43 | public Response(boolean value, String sessionId) { 44 | this.status = 0; 45 | this.value = value; 46 | this.sessionId = sessionId; 47 | } 48 | 49 | public int getStatus() { 50 | return status; 51 | } 52 | 53 | public void setStatus(int status) { 54 | this.status = status; 55 | } 56 | 57 | public Object getValue() { 58 | return value; 59 | } 60 | 61 | public String getSessionId() { 62 | return sessionId; 63 | } 64 | 65 | public void setSessionId(String sessionId) { 66 | this.sessionId = sessionId; 67 | } 68 | 69 | @Override 70 | public String toString() { 71 | JSONObject res = new JSONObject(); 72 | try { 73 | res.put("status", this.getStatus()); 74 | res.put("value", this.getValue()); 75 | res.put("sessionId", this.getSessionId()); 76 | return res.toString(); 77 | } catch (JSONException e) { 78 | e.printStackTrace(); 79 | } 80 | return null; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/models/Status.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.models; 2 | 3 | /** 4 | * Created by xdf on 03/05/2017. 5 | */ 6 | 7 | public enum Status { 8 | Success(0, "The command executed successfully."), 9 | NoSuchElement(7, "An element could not be located on the page using the given search parameters."), 10 | NoSuchFrame(8, "A request to switch to a frame could not be satisfied because the frame could not be found."), 11 | UnknownCommand(9, "The requested resource could not be found, or a request was received using an HTTP method that is not supported by the mapped resource."), 12 | StaleElementReference(10, "An element command failed because the referenced element is no longer attached to the DOM."), 13 | ElementNotVisible(11, "An element command could not be completed because the element is not visible on the page."), 14 | InvalidElementState(12, "An element command could not be completed because the element is in an invalid state (e.g. attempting to click a disabled element)."), 15 | UnknownError(13, "An unknown server-side error occurred while processing the command."), 16 | ElementIsNotSelectable(15, "An attempt was made to select an element that cannot be selected."), 17 | JavaScriptError(17, "An error occurred while executing user supplied JavaScript."), 18 | XPathLookupError(19, "An error occurred while searching for an element by XPath."), 19 | Timeout(21, "An operation did not complete before its timeout expired."), 20 | NoSuchWindow(23, "A request to switch to a different window could not be satisfied because the window could not be found."), 21 | InvalidCookieDomain(24, "An illegal attempt was made to set a cookie under a different domain than the current page."), 22 | UnableToSetCookie(25, "A request to set a cookie's value could not be satisfied."), 23 | UnexpectedAlertOpen(26, "A modal dialog was open, blocking this operation."), 24 | NoAlertOpenError(27, "An attempt was made to operate on a modal dialog when one was not open."), 25 | ScriptTimeout(28, "A script did not complete before its timeout expired."), 26 | InvalidElementCoordinates(29, "The coordinates provided to an interactions operation are invalid."), 27 | IMENotAvailable(30, "IME was not available."), 28 | IMEEngineActivationFailed(31, "An IME engine could not be started."), 29 | InvalidSelector(32, "Argument was an invalid selector (e.g. XPath/CSS)."), 30 | SessionNotCreatedException(33, "Session Not Created Exception"), 31 | MoveTargetOutOfBounds(34, "Move Target Out Of Bounds"); 32 | 33 | private int statusCode; 34 | private String statusDes; 35 | 36 | private Status(int statusCode, String statusDes) { 37 | this.statusCode = statusCode; 38 | this.statusDes = statusDes; 39 | } 40 | 41 | public int getStatusCode() { 42 | return this.statusCode; 43 | } 44 | 45 | public String getStatusDes() { 46 | return this.statusDes; 47 | } 48 | } 49 | 50 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/xmlUtils/Attribute.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.xmlUtils; 2 | 3 | /** 4 | * Created by xdf on 09/05/2017. 5 | */ 6 | public enum Attribute { 7 | CHECKABLE("checkable"), 8 | CHECKED("checked"), 9 | CLASS("class"), 10 | CLICKABLE("clickable"), 11 | CONTENT_DESC("content-desc"), 12 | ENABLED("enabled"), 13 | FOCUSABLE("focusable"), 14 | FOCUSED("focused"), 15 | LONG_CLICKABLE("long-clickable"), 16 | PACKAGE("package"), 17 | PASSWORD("password"), 18 | RESOURCE_ID("resource-id"), 19 | SCROLLABLE("scrollable"), 20 | SELECTION_START("selection-start"), 21 | SELECTION_END("selection-end"), 22 | SELECTED("selected"), 23 | TEXT("text"), 24 | BOUNDS("bounds"), 25 | INDEX("index"); 26 | 27 | private final String name; 28 | 29 | private Attribute(String name) { 30 | this.name = name; 31 | } 32 | 33 | public String getName() { 34 | return name; 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return name; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/xmlUtils/InteractionController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.macaca.android.testing.server.xmlUtils; 17 | 18 | import static com.macaca.android.testing.server.xmlUtils.ReflectionUtils.invoke; 19 | import static com.macaca.android.testing.server.xmlUtils.ReflectionUtils.method; 20 | 21 | public class InteractionController { 22 | 23 | private static final String CLASS_INTERACTION_CONTROLLER = "com.android.uiautomator.core.InteractionController"; 24 | private static final String METHOD_SEND_KEY = "sendKey"; 25 | 26 | private final Object interactionController; 27 | 28 | public InteractionController(Object interactionController) { 29 | this.interactionController = interactionController; 30 | } 31 | 32 | public boolean sendKey(int keyCode, int metaState) throws Exception { 33 | return (Boolean) invoke(method(CLASS_INTERACTION_CONTROLLER, METHOD_SEND_KEY, int.class, int.class), interactionController, keyCode, metaState); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/xmlUtils/MUiDevice.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.xmlUtils; 2 | 3 | /** 4 | * Created by xdf on 09/05/2017. 5 | */ 6 | 7 | import android.app.Instrumentation; 8 | import android.os.SystemClock; 9 | import android.support.test.uiautomator.By; 10 | import android.support.test.uiautomator.BySelector; 11 | import android.support.test.uiautomator.UiDevice; 12 | import android.support.test.uiautomator.UiObject2; 13 | import android.view.accessibility.AccessibilityNodeInfo; 14 | 15 | import com.macaca.android.testing.server.common.Elements; 16 | 17 | import java.lang.reflect.Constructor; 18 | import java.lang.reflect.Method; 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | 22 | public class MUiDevice { 23 | 24 | private static final String FIELD_M_INSTRUMENTATION = "mInstrumentation"; 25 | private static final String FIELD_API_LEVEL_ACTUAL = "API_LEVEL_ACTUAL"; 26 | 27 | private static MUiDevice INSTANCE = new MUiDevice(); 28 | private Method METHOD_FIND_MATCH; 29 | private Method METHOD_FIND_MATCHS; 30 | private Class ByMatcher; 31 | private Instrumentation mInstrumentation; 32 | private Object API_LEVEL_ACTUAL; 33 | 34 | private UiDevice uiDevice = Elements.getGlobal().getmDevice(); 35 | 36 | /** 37 | * UiDevice in android open source project will Support multi-window searches for API level 21, 38 | * which has not been implemented in UiAutomatorViewer capture layout hierarchy, to be in sync 39 | * with UiAutomatorViewer customizing getWindowRoots() method to skip the multi-window search 40 | * based user passed property 41 | */ 42 | public MUiDevice() { 43 | try { 44 | this.mInstrumentation = (Instrumentation) ReflectionUtils.getField(UiDevice.class, FIELD_M_INSTRUMENTATION, uiDevice); 45 | this.API_LEVEL_ACTUAL = ReflectionUtils.getField(UiDevice.class, FIELD_API_LEVEL_ACTUAL, uiDevice); 46 | METHOD_FIND_MATCH = ReflectionUtils.method("android.support.test.uiautomator.ByMatcher", "findMatch", UiDevice.class, BySelector.class, AccessibilityNodeInfo[].class); 47 | METHOD_FIND_MATCHS = ReflectionUtils.method("android.support.test.uiautomator.ByMatcher", "findMatches", UiDevice.class, BySelector.class, AccessibilityNodeInfo[].class); 48 | ByMatcher = ReflectionUtils.getClass("android.support.test.uiautomator.ByMatcher"); 49 | } catch (Error error) { 50 | throw error; 51 | } catch (Exception error) { 52 | throw new Error(error); 53 | } 54 | } 55 | 56 | public static MUiDevice getInstance() { 57 | return INSTANCE; 58 | } 59 | 60 | private UiObject2 doFindObject(Object selector, AccessibilityNodeInfo node) throws Exception { 61 | 62 | Class uiObject2 = Class.forName("android.support.test.uiautomator.UiObject2"); 63 | Constructor cons = uiObject2.getDeclaredConstructors()[0]; 64 | cons.setAccessible(true); 65 | Object[] constructorParams = {uiDevice, selector, node}; 66 | 67 | final long timeoutMillis = 1000; 68 | long end = SystemClock.uptimeMillis() + timeoutMillis; 69 | while (true) { 70 | UiObject2 object2 = (UiObject2) cons.newInstance(constructorParams); 71 | if (object2 != null) { 72 | return object2; 73 | } 74 | long remainingMillis = end - SystemClock.uptimeMillis(); 75 | if (remainingMillis < 0) { 76 | return null; 77 | } 78 | SystemClock.sleep(Math.min(200, remainingMillis)); 79 | } 80 | } 81 | 82 | 83 | /** 84 | * Returns the first object to match the {@code selector} criteria. 85 | */ 86 | public UiObject2 findObject(Object selector) throws Exception { 87 | AccessibilityNodeInfo node; 88 | uiDevice.waitForIdle(); 89 | node = ((NodeInfoList) selector).getNodeList().size() > 0 ? ((NodeInfoList) selector).getNodeList().get(0) : null; 90 | selector = By.clazz(node.getClassName().toString()); 91 | if (node == null) { 92 | return null; 93 | } 94 | return doFindObject(selector, node); 95 | } 96 | 97 | public List findObjects(Object selector) throws Exception { 98 | uiDevice.waitForIdle(); 99 | ArrayList accessibilityNodeInfos = ((NodeInfoList) selector).getNodeList(); 100 | int size = accessibilityNodeInfos.size(); 101 | List list = new ArrayList(); 102 | if (size > 0) { 103 | for (int i = 0; i < size; i++) { 104 | AccessibilityNodeInfo node = accessibilityNodeInfos.get(i); 105 | if (node == null) { 106 | continue; 107 | } 108 | selector = By.clazz(node.getClassName().toString()); 109 | UiObject2 uiObject2 = doFindObject(selector, node); 110 | list.add(uiObject2); 111 | } 112 | } else { 113 | return null; 114 | } 115 | return list; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/xmlUtils/NodeInfoList.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.xmlUtils; 2 | 3 | /** 4 | * Created by xdf on 09/05/2017. 5 | */ 6 | 7 | import android.view.accessibility.AccessibilityNodeInfo; 8 | 9 | import java.util.ArrayList; 10 | 11 | public class NodeInfoList { 12 | 13 | private ArrayList nodeList = new ArrayList(); 14 | 15 | public void addToList(AccessibilityNodeInfo node){ 16 | nodeList.add(node); 17 | } 18 | 19 | public ArrayList getNodeList(){ 20 | return nodeList; 21 | } 22 | 23 | public int size(){ 24 | return nodeList.size(); 25 | } 26 | 27 | } 28 | 29 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/xmlUtils/QueryController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.macaca.android.testing.server.xmlUtils; 17 | 18 | import android.view.accessibility.AccessibilityNodeInfo; 19 | 20 | import static com.macaca.android.testing.server.xmlUtils.ReflectionUtils.invoke; 21 | import static com.macaca.android.testing.server.xmlUtils.ReflectionUtils.method; 22 | 23 | public class QueryController { 24 | 25 | private static final String CLASS_QUERY_CONTROLLER = "android.support.test.uiautomator.QueryController"; 26 | private static final String METHOD_GET_ACCESSIBILITY_ROOT_NODE = "getRootNode"; 27 | 28 | private final Object queryController; 29 | 30 | public QueryController(Object queryController) { 31 | this.queryController = queryController; 32 | } 33 | 34 | public AccessibilityNodeInfo getAccessibilityRootNode() throws Exception { 35 | return (AccessibilityNodeInfo) invoke(method(CLASS_QUERY_CONTROLLER, METHOD_GET_ACCESSIBILITY_ROOT_NODE), queryController); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/xmlUtils/ReflectionUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * See the NOTICE file distributed with this work for additional 5 | * information regarding copyright ownership. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.macaca.android.testing.server.xmlUtils; 17 | 18 | import java.lang.reflect.Field; 19 | import java.lang.reflect.Method; 20 | 21 | public class ReflectionUtils { 22 | 23 | /** 24 | * Clears the in-process Accessibility cache, removing any stale references. 25 | * Because the AccessibilityInteractionClient singleton stores copies of 26 | * AccessibilityNodeInfo instances, calls to public APIs such as `recycle` do 27 | * not guarantee cached references get updated. See the 28 | * android.view.accessibility AIC and ANI source code for more information. 29 | * https://github.com/appium/appium/issues/4200 30 | */ 31 | public static boolean clearAccessibilityCache() throws Exception { 32 | boolean success; 33 | 34 | final Class c = Class.forName("android.view.accessibility.AccessibilityInteractionClient"); 35 | final Method getInstance = ReflectionUtils.method(c, "getInstance"); 36 | final Object instance = getInstance.invoke(null); 37 | final Method clearCache = ReflectionUtils.method(instance.getClass(), "clearCache"); 38 | clearCache.invoke(instance); 39 | success = true; 40 | 41 | return success; 42 | } 43 | 44 | 45 | public static Class getClass(final String name) throws Exception { 46 | return Class.forName(name); 47 | } 48 | 49 | public static Object getField(final Class clazz, final String fieldName, final Object object) throws Exception { 50 | final Field field = clazz.getDeclaredField(fieldName); 51 | field.setAccessible(true); 52 | return field.get(object); 53 | } 54 | 55 | public static Object getField(final String className, final String field, final Object object) throws Exception { 56 | Class a = getClass(className); 57 | return getField(a, field, object); 58 | } 59 | 60 | public static void setField(final Class clazz, final String fieldName, final Object object, final Object value) throws Exception { 61 | final Field field = clazz.getDeclaredField(fieldName); 62 | field.setAccessible(true); 63 | field.set(object, value); 64 | } 65 | 66 | public static Object invoke(final Method method, final Object object, final Object... parameters) throws Exception { 67 | return method.invoke(object, parameters); 68 | } 69 | 70 | public static Method method(final Class clazz, final String methodName, final Class... parameterTypes) throws Exception { 71 | final Method method = clazz.getDeclaredMethod(methodName, parameterTypes); 72 | method.setAccessible(true); 73 | return method; 74 | } 75 | 76 | public static Method method(final String className, final String method, final Class... parameterTypes) throws Exception { 77 | return method(getClass(className), method, parameterTypes); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/xmlUtils/UiAutomationElement.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.xmlUtils; 2 | 3 | /** 4 | * Created by xdf on 09/05/2017. 5 | */ 6 | 7 | import android.annotation.TargetApi; 8 | import android.graphics.Rect; 9 | import android.view.accessibility.AccessibilityNodeInfo; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Collections; 13 | import java.util.EnumMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.WeakHashMap; 17 | 18 | /** 19 | * A UiElement that gets attributes via the Accessibility API. 20 | */ 21 | @TargetApi(18) 22 | public class UiAutomationElement extends UiElement { 23 | 24 | private final Map attributes; 25 | private final boolean visible; 26 | private final Rect visibleBounds; 27 | private final UiAutomationElement parent; 28 | private final List children; 29 | public final static Map map = new WeakHashMap(); 30 | 31 | /** 32 | * A snapshot of all attributes is taken at construction. The attributes of a 33 | * {@code UiAutomationElement} instance are immutable. If the underlying 34 | * {@link AccessibilityNodeInfo} is updated, a new {@code UiAutomationElement} 35 | * instance will be created in 36 | */ 37 | protected UiAutomationElement( AccessibilityNodeInfo node, 38 | UiAutomationElement parent, int index) { 39 | this.node = node; 40 | this.parent = parent; 41 | 42 | Map attribs = new EnumMap(Attribute.class); 43 | 44 | put(attribs, Attribute.INDEX, index); 45 | put(attribs, Attribute.PACKAGE, charSequenceToString(node.getPackageName())); 46 | put(attribs, Attribute.CLASS, charSequenceToString(node.getClassName())); 47 | put(attribs, Attribute.TEXT, charSequenceToString(node.getText())); 48 | put(attribs, Attribute.CONTENT_DESC, charSequenceToString(node.getContentDescription())); 49 | put(attribs, Attribute.RESOURCE_ID, charSequenceToString(node.getViewIdResourceName())); 50 | put(attribs, Attribute.CHECKABLE, node.isCheckable()); 51 | put(attribs, Attribute.CHECKED, node.isChecked()); 52 | put(attribs, Attribute.CLICKABLE, node.isClickable()); 53 | put(attribs, Attribute.ENABLED, node.isEnabled()); 54 | put(attribs, Attribute.FOCUSABLE, node.isFocusable()); 55 | put(attribs, Attribute.FOCUSED, node.isFocused()); 56 | put(attribs, Attribute.LONG_CLICKABLE, node.isLongClickable()); 57 | put(attribs, Attribute.PASSWORD, node.isPassword()); 58 | put(attribs, Attribute.SCROLLABLE, node.isScrollable()); 59 | if (node.getTextSelectionStart() >= 0 60 | && node.getTextSelectionStart() != node.getTextSelectionEnd()) { 61 | attribs.put(Attribute.SELECTION_START, node.getTextSelectionStart()); 62 | attribs.put(Attribute.SELECTION_END, node.getTextSelectionEnd()); 63 | } 64 | put(attribs, Attribute.SELECTED, node.isSelected()); 65 | put(attribs, Attribute.BOUNDS, getBounds(node)); 66 | attributes = Collections.unmodifiableMap(attribs); 67 | 68 | // Order matters as getVisibleBounds depends on visible 69 | visible = node.isVisibleToUser(); 70 | visibleBounds = getVisibleBounds(node); 71 | List mutableChildren = buildChildren(node); 72 | this.children = mutableChildren == null ? null : Collections.unmodifiableList(mutableChildren); 73 | } 74 | 75 | protected UiAutomationElement(String hierarchyClassName, 76 | AccessibilityNodeInfo childNode, int index){ 77 | this.parent = null; 78 | Map attribs = new EnumMap(Attribute.class); 79 | 80 | put(attribs, Attribute.INDEX, index); 81 | put(attribs, Attribute.CLASS, charSequenceToString(hierarchyClassName)); 82 | put(attribs, Attribute.CHECKABLE, false); 83 | put(attribs, Attribute.CHECKED, false); 84 | put(attribs, Attribute.CLICKABLE, false); 85 | put(attribs, Attribute.ENABLED, false); 86 | put(attribs, Attribute.FOCUSABLE, false); 87 | put(attribs, Attribute.FOCUSED, false); 88 | put(attribs, Attribute.LONG_CLICKABLE, false); 89 | put(attribs, Attribute.PASSWORD, false); 90 | put(attribs, Attribute.SCROLLABLE, false); 91 | put(attribs, Attribute.SELECTED, false); 92 | 93 | this.attributes = Collections.unmodifiableMap(attribs); 94 | this.visible= true; 95 | this.visibleBounds = null; 96 | List mutableChildren =new ArrayList(); 97 | mutableChildren.add(new UiAutomationElement(childNode, this /* parent UiAutomationElement*/, 0/* index */)); 98 | this.children = mutableChildren; 99 | } 100 | 101 | private void put(Map attribs, Attribute key, Object value) { 102 | if (value != null) { 103 | attribs.put(key, value); 104 | } 105 | } 106 | 107 | private List buildChildren(AccessibilityNodeInfo node) { 108 | List children; 109 | int childCount = node.getChildCount(); 110 | if (childCount == 0) { 111 | children = null; 112 | } else { 113 | children = new ArrayList(childCount); 114 | for (int i = 0; i < childCount; i++) { 115 | AccessibilityNodeInfo child = node.getChild(i); 116 | if (child != null && child.isVisibleToUser()) { 117 | children.add(this.getElement(child, this, i)); 118 | } 119 | } 120 | } 121 | return children; 122 | } 123 | 124 | public static UiAutomationElement newRootElement(AccessibilityNodeInfo rawElement) { 125 | clearData(); 126 | /** 127 | * Injecting root element as hierarchy and adding rawElement as a child. 128 | */ 129 | UiAutomationElement rootElement = new UiAutomationElement("hierarchy" /*root element*/, rawElement /* child nodInfo */, 0 /* index */); 130 | return rootElement; 131 | } 132 | 133 | private static void clearData() { 134 | map.clear(); 135 | XPathSelector.clearData(); 136 | } 137 | 138 | public static UiAutomationElement getElement(AccessibilityNodeInfo rawElement, UiAutomationElement parent, int index) { 139 | UiAutomationElement element = map.get(rawElement); 140 | if (element == null) { 141 | element = new UiAutomationElement(rawElement, parent, index); 142 | map.put(rawElement, element); 143 | } 144 | return element; 145 | } 146 | 147 | private Rect getBounds(AccessibilityNodeInfo node) { 148 | Rect rect = new Rect(); 149 | node.getBoundsInScreen(rect); 150 | return rect; 151 | } 152 | 153 | private Rect getVisibleBounds(AccessibilityNodeInfo node) { 154 | if (!visible) { 155 | return new Rect(); 156 | } 157 | Rect visibleBounds = getBounds(this.node); 158 | UiAutomationElement parent = getParent(); 159 | Rect parentBounds; 160 | while (parent != null && parent.node != null) { 161 | parentBounds = parent.getBounds(this.parent.node); 162 | visibleBounds.intersect(parentBounds); 163 | parent = parent.getParent(); 164 | } 165 | return visibleBounds; 166 | } 167 | 168 | public UiAutomationElement getParent() { 169 | return parent; 170 | } 171 | 172 | @Override 173 | protected List getChildren() { 174 | if (children == null) { 175 | return Collections.emptyList(); 176 | } 177 | return children; 178 | } 179 | 180 | @Override 181 | protected Map getAttributes() { 182 | return attributes; 183 | } 184 | 185 | public static String charSequenceToString(CharSequence input) { 186 | return input == null ? null : input.toString(); 187 | } 188 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/xmlUtils/UiAutomatorBridge.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.macaca.android.testing.server.xmlUtils; 17 | 18 | import android.view.Display; 19 | import android.support.test.uiautomator.UiDevice; 20 | 21 | import static com.macaca.android.testing.server.xmlUtils.ReflectionUtils.*; 22 | 23 | public class UiAutomatorBridge { 24 | 25 | private static final String CLASS_UI_AUTOMATOR_BRIDGE = "android.support.test.uiautomator.UiAutomatorBridge"; 26 | private static final String FIELD_UI_AUTOMATOR_BRIDGE = "mUiAutomationBridge"; 27 | private static final String FIELD_QUERY_CONTROLLER = "mQueryController"; 28 | private static final String FIELD_INTERACTION_CONTROLLER = "mInteractionController"; 29 | private static final String METHOD_GET_DEFAULT_DISPLAY = "getDefaultDisplay"; 30 | 31 | private final Object uiAutomatorBridge; 32 | private static UiDevice uiDevice; 33 | 34 | private static UiAutomatorBridge INSTANCE = new UiAutomatorBridge(); 35 | 36 | public UiAutomatorBridge() { 37 | try { 38 | final UiDevice device = this.getUiDevice(); 39 | this.uiAutomatorBridge = getField(UiDevice.class, FIELD_UI_AUTOMATOR_BRIDGE, device); 40 | } catch (Error error) { 41 | throw error; 42 | } catch (Exception error) { 43 | throw new Error(error); 44 | } 45 | } 46 | 47 | public static final UiDevice getUiDevice() { 48 | if (uiDevice == null) { 49 | uiDevice = UiDevice.getInstance(); 50 | } 51 | return uiDevice; 52 | } 53 | 54 | public static UiAutomatorBridge getInstance() { 55 | return INSTANCE; 56 | } 57 | 58 | public InteractionController getInteractionController() throws Exception { 59 | return new InteractionController(getField(CLASS_UI_AUTOMATOR_BRIDGE, FIELD_INTERACTION_CONTROLLER, uiAutomatorBridge)); 60 | } 61 | 62 | public QueryController getQueryController() throws Exception { 63 | return new QueryController(getField(CLASS_UI_AUTOMATOR_BRIDGE, FIELD_QUERY_CONTROLLER, uiAutomatorBridge)); 64 | } 65 | 66 | public Display getDefaultDisplay() throws Exception { 67 | return (Display) invoke(method(CLASS_UI_AUTOMATOR_BRIDGE, METHOD_GET_DEFAULT_DISPLAY), uiAutomatorBridge); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/xmlUtils/UiElement.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.xmlUtils; 2 | 3 | /** 4 | * Created by xdf on 09/05/2017. 5 | */ 6 | 7 | import android.graphics.Rect; 8 | import android.view.accessibility.AccessibilityNodeInfo; 9 | 10 | import java.util.List; 11 | import java.util.Map; 12 | 13 | 14 | /** 15 | * UiElement that implements the common operations. 16 | * 17 | * @param the type of the raw element this class wraps, for example, View or 18 | * AccessibilityNodeInfo 19 | * @param the type of the concrete subclass of UiElement 20 | */ 21 | public abstract class UiElement> { 22 | public AccessibilityNodeInfo node; 23 | 24 | 25 | @SuppressWarnings("unchecked") 26 | public T get(Attribute attribute) { 27 | return (T) getAttributes().get(attribute); 28 | } 29 | 30 | public String getText() { 31 | return get(Attribute.TEXT); 32 | } 33 | 34 | public String getContentDescription() { 35 | return get(Attribute.CONTENT_DESC); 36 | } 37 | 38 | public String getClassName() { 39 | return get(Attribute.CLASS); 40 | } 41 | 42 | public String getResourceId() { 43 | return get(Attribute.RESOURCE_ID); 44 | } 45 | 46 | public String getPackageName() { 47 | return get(Attribute.PACKAGE); 48 | } 49 | 50 | public boolean isCheckable() { 51 | return (Boolean) get(Attribute.CHECKABLE); 52 | } 53 | 54 | public boolean isChecked() { 55 | return (Boolean) get(Attribute.CHECKED); 56 | } 57 | 58 | public boolean isClickable() { 59 | return (Boolean) get(Attribute.CLICKABLE); 60 | } 61 | 62 | public boolean isEnabled() { 63 | return (Boolean) get(Attribute.ENABLED); 64 | } 65 | 66 | public boolean isFocusable() { 67 | return (Boolean) get(Attribute.FOCUSABLE); 68 | } 69 | 70 | public boolean isFocused() { 71 | return (Boolean) get(Attribute.FOCUSED); 72 | } 73 | 74 | public boolean isScrollable() { 75 | return (Boolean) get(Attribute.SCROLLABLE); 76 | } 77 | 78 | public boolean isLongClickable() { 79 | return (Boolean) get(Attribute.LONG_CLICKABLE); 80 | } 81 | 82 | public boolean isPassword() { 83 | return (Boolean) get(Attribute.PASSWORD); 84 | } 85 | 86 | public boolean isSelected() { 87 | return (Boolean) get(Attribute.SELECTED); 88 | } 89 | 90 | public int getIndex() { return (Integer)get(Attribute.INDEX); } 91 | 92 | protected abstract List getChildren(); 93 | 94 | public Rect getBounds() { 95 | return get(Attribute.BOUNDS); 96 | } 97 | 98 | public int getSelectionStart() { 99 | Integer value = get(Attribute.SELECTION_START); 100 | return value == null ? 0 : value; 101 | } 102 | 103 | public int getSelectionEnd() { 104 | Integer value = get(Attribute.SELECTION_END); 105 | return value == null ? 0 : value; 106 | } 107 | 108 | public boolean hasSelection() { 109 | final int selectionStart = getSelectionStart(); 110 | final int selectionEnd = getSelectionEnd(); 111 | 112 | return selectionStart >= 0 && selectionStart != selectionEnd; 113 | } 114 | 115 | protected abstract Map getAttributes(); 116 | } -------------------------------------------------------------------------------- /app/src/androidTest/java/com/macaca/android/testing/server/xmlUtils/XPathSelector.java: -------------------------------------------------------------------------------- 1 | package com.macaca.android.testing.server.xmlUtils; 2 | 3 | /** 4 | * Created by xdf on 09/05/2017. 5 | */ 6 | 7 | import android.os.SystemClock; 8 | import android.support.test.uiautomator.UiDevice; 9 | import android.view.accessibility.AccessibilityNodeInfo; 10 | 11 | import com.macaca.android.testing.server.common.Elements; 12 | 13 | import org.w3c.dom.DOMException; 14 | import org.w3c.dom.Document; 15 | import org.w3c.dom.Element; 16 | import org.w3c.dom.Node; 17 | import org.w3c.dom.NodeList; 18 | 19 | import java.lang.reflect.Field; 20 | import java.util.HashMap; 21 | import java.util.Map; 22 | 23 | import javax.xml.parsers.DocumentBuilderFactory; 24 | import javax.xml.xpath.XPath; 25 | import javax.xml.xpath.XPathConstants; 26 | import javax.xml.xpath.XPathExpression; 27 | import javax.xml.xpath.XPathFactory; 28 | 29 | /** 30 | * Find matching UiElement by XPath. 31 | */ 32 | public class XPathSelector { 33 | private static final XPath XPATH_COMPILER = XPathFactory.newInstance().newXPath(); 34 | // document needs to be static so that when buildDomNode is called recursively 35 | // on children they are in the same document to be appended. 36 | private static Document document; 37 | // The two maps should be kept in sync 38 | private static final Map, Element> TO_DOM_MAP = 39 | new HashMap, Element>(); 40 | private static final Map> FROM_DOM_MAP = 41 | new HashMap>(); 42 | private static UiDevice uiDevice = Elements.getGlobal().getmDevice(); 43 | 44 | public static void clearData() { 45 | TO_DOM_MAP.clear(); 46 | FROM_DOM_MAP.clear(); 47 | document = null; 48 | } 49 | 50 | private final String xPathString; 51 | private XPathExpression xPathExpression; 52 | private static UiAutomationElement rootElement; 53 | 54 | public XPathSelector(String xPathString) throws Exception { 55 | this.xPathString = xPathString; 56 | xPathExpression = XPATH_COMPILER.compile(xPathString); 57 | } 58 | 59 | public NodeInfoList find(UiElement context) throws Exception { 60 | Element domNode = getDomNode((UiElement) context); 61 | NodeInfoList list = new NodeInfoList(); 62 | getDocument().appendChild(domNode); 63 | NodeList nodes = (NodeList) xPathExpression.evaluate(domNode, XPathConstants.NODESET); 64 | int nodesLength = nodes.getLength(); 65 | for (int i = 0; i < nodesLength; i++) { 66 | if (nodes.item(i).getNodeType() == Node.ELEMENT_NODE && !FROM_DOM_MAP.get(nodes.item(i)).getClassName().equals("hierarchy")) { 67 | list.addToList(FROM_DOM_MAP.get(nodes.item(i)).node); 68 | } 69 | } 70 | try { 71 | getDocument().removeChild(domNode); 72 | } catch (DOMException e) { 73 | document = null; 74 | } 75 | return list; 76 | } 77 | 78 | public static NodeInfoList getNodesList(String xpathExpression) throws Exception { 79 | XPathSelector.refreshUiElementTree(); 80 | XPathSelector finder = new XPathSelector(xpathExpression); 81 | return finder.find(finder.getRootElement()); 82 | } 83 | 84 | private static Document getDocument() throws Exception { 85 | if (document == null) { 86 | document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); 87 | } 88 | return document; 89 | } 90 | 91 | /** 92 | * Returns the DOM node representing this UiElement. 93 | */ 94 | private static Element getDomNode(UiElement uiElement) throws Exception { 95 | Element domNode = TO_DOM_MAP.get(uiElement); 96 | if (domNode == null) { 97 | domNode = buildDomNode(uiElement); 98 | } 99 | return domNode; 100 | } 101 | 102 | private static void setNodeLocalName(Element element, String className) throws Exception { 103 | Field localName = element.getClass().getDeclaredField("localName"); 104 | localName.setAccessible(true); 105 | localName.set(element, tag(className)); 106 | } 107 | 108 | private static Element buildDomNode(UiElement uiElement) throws Exception { 109 | String className = uiElement.getClassName(); 110 | if (className == null) { 111 | className = "UNKNOWN"; 112 | } 113 | Element element = getDocument().createElement(simpleClassName(className)); 114 | TO_DOM_MAP.put(uiElement, element); 115 | FROM_DOM_MAP.put(element, uiElement); 116 | 117 | setNodeLocalName(element, className); 118 | 119 | setAttribute(element, Attribute.INDEX, String.valueOf(uiElement.getIndex())); 120 | setAttribute(element, Attribute.CLASS, className); 121 | setAttribute(element, Attribute.RESOURCE_ID, uiElement.getResourceId()); 122 | setAttribute(element, Attribute.PACKAGE, uiElement.getPackageName()); 123 | setAttribute(element, Attribute.CONTENT_DESC, uiElement.getContentDescription()); 124 | setAttribute(element, Attribute.TEXT, uiElement.getText()); 125 | setAttribute(element, Attribute.CHECKABLE, uiElement.isCheckable()); 126 | setAttribute(element, Attribute.CHECKED, uiElement.isChecked()); 127 | setAttribute(element, Attribute.CLICKABLE, uiElement.isClickable()); 128 | setAttribute(element, Attribute.ENABLED, uiElement.isEnabled()); 129 | setAttribute(element, Attribute.FOCUSABLE, uiElement.isFocusable()); 130 | setAttribute(element, Attribute.FOCUSED, uiElement.isFocused()); 131 | setAttribute(element, Attribute.SCROLLABLE, uiElement.isScrollable()); 132 | setAttribute(element, Attribute.LONG_CLICKABLE, uiElement.isLongClickable()); 133 | setAttribute(element, Attribute.PASSWORD, uiElement.isPassword()); 134 | if (uiElement.hasSelection()) { 135 | element.setAttribute(Attribute.SELECTION_START.getName(), 136 | Integer.toString(uiElement.getSelectionStart())); 137 | element.setAttribute(Attribute.SELECTION_END.getName(), 138 | Integer.toString(uiElement.getSelectionEnd())); 139 | } 140 | setAttribute(element, Attribute.SELECTED, uiElement.isSelected()); 141 | element.setAttribute(Attribute.BOUNDS.getName(), uiElement.getBounds() == null ? null : uiElement.getBounds().toShortString()); 142 | 143 | for (UiElement child : uiElement.getChildren()) { 144 | element.appendChild(getDomNode(child)); 145 | } 146 | return element; 147 | } 148 | 149 | private static void setAttribute(Element element, Attribute attr, String value) { 150 | if (value != null) { 151 | element.setAttribute(attr.getName(), value); 152 | } 153 | } 154 | 155 | private static void setAttribute(Element element, Attribute attr, boolean value) { 156 | element.setAttribute(attr.getName(), String.valueOf(value)); 157 | } 158 | 159 | public UiAutomationElement getRootElement() throws Exception { 160 | if (rootElement == null) { 161 | refreshUiElementTree(); 162 | } 163 | return rootElement; 164 | } 165 | 166 | public static void refreshUiElementTree() throws Exception { 167 | rootElement = UiAutomationElement.newRootElement(getRootAccessibilityNode()); 168 | } 169 | 170 | 171 | public static AccessibilityNodeInfo getRootAccessibilityNode() throws Exception { 172 | final long timeoutMillis = 10000; 173 | uiDevice.waitForIdle(timeoutMillis); 174 | long end = SystemClock.uptimeMillis() + timeoutMillis; 175 | while (true) { 176 | AccessibilityNodeInfo root = UiAutomatorBridge.getInstance().getQueryController().getAccessibilityRootNode(); 177 | if (root != null) { 178 | return root; 179 | } 180 | long remainingMillis = end - SystemClock.uptimeMillis(); 181 | SystemClock.sleep(Math.min(250, remainingMillis)); 182 | } 183 | } 184 | 185 | /** 186 | * @return The tag name used to build UiElement DOM. It is preferable to use 187 | * this to build XPath instead of String literals. 188 | */ 189 | public static String tag(String className) { 190 | // the nth anonymous class has a class name ending in "Outer$n" 191 | // and local inner classes have names ending in "Outer.$1Inner" 192 | className = className.replaceAll("\\$[0-9]+", "\\$"); 193 | return className; 194 | } 195 | 196 | /** 197 | * returns by excluding inner class name. 198 | */ 199 | private static String simpleClassName(String name) { 200 | name = name.replaceAll("\\$[0-9]+", "\\$"); 201 | int start = name.lastIndexOf('$'); 202 | if (start == -1) { 203 | return name; 204 | } 205 | return name.substring(0, start); 206 | } 207 | } 208 | 209 | -------------------------------------------------------------------------------- /app/src/androidTest/java/fi/iki/elonen/router/RouterNanoHTTPD.java: -------------------------------------------------------------------------------- 1 | package fi.iki.elonen.router; 2 | 3 | /* 4 | * 5 | * NanoHttpd-Samples 6 | * 7 | * Copyright (C) 2012 - 2015 nanohttpd 8 | * 9 | * Redistribution and use in source and binary forms, with or without modification, 10 | * are permitted provided that the following conditions are met: 11 | * 12 | * 1. Redistributions of source code must retain the above copyright notice, this 13 | * list of conditions and the following disclaimer. 14 | * 15 | * 2. Redistributions in binary form must reproduce the above copyright notice, 16 | * this list of conditions and the following disclaimer in the documentation 17 | * and/or other materials provided with the distribution. 18 | * 19 | * 3. Neither the name of the nanohttpd nor the names of its contributors 20 | * may be used to endorse or promote products derived from this software without 21 | * specific prior written permission. 22 | * 23 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 24 | * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 25 | * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 26 | * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 27 | * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 30 | * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 31 | * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 32 | * OF THE POSSIBILITY OF SUCH DAMAGE. 33 | * #L% 34 | */ 35 | 36 | import com.macaca.android.testing.server.models.Methods; 37 | 38 | import java.io.BufferedInputStream; 39 | import java.io.File; 40 | import java.io.FileInputStream; 41 | import java.io.IOException; 42 | import java.io.InputStream; 43 | import java.util.ArrayList; 44 | import java.util.Collections; 45 | import java.util.Comparator; 46 | import java.util.HashMap; 47 | import java.util.Iterator; 48 | import java.util.List; 49 | import java.util.Map; 50 | import java.util.logging.Level; 51 | import java.util.logging.Logger; 52 | import java.util.regex.Matcher; 53 | import java.util.regex.Pattern; 54 | 55 | import fi.iki.elonen.NanoHTTPD; 56 | import fi.iki.elonen.NanoHTTPD.Response.IStatus; 57 | import fi.iki.elonen.NanoHTTPD.Response.Status; 58 | 59 | /** 60 | * @author vnnv 61 | * @author ritchieGitHub 62 | */ 63 | public class RouterNanoHTTPD extends NanoHTTPD { 64 | 65 | /** 66 | * logger to log to. 67 | */ 68 | private static final Logger LOG = Logger.getLogger(RouterNanoHTTPD.class.getName()); 69 | 70 | public interface UriResponder { 71 | 72 | public Response get(UriResource uriResource, Map urlParams, IHTTPSession session); 73 | 74 | public Response put(UriResource uriResource, Map urlParams, IHTTPSession session); 75 | 76 | public Response post(UriResource uriResource, Map urlParams, IHTTPSession session); 77 | 78 | public Response delete(UriResource uriResource, Map urlParams, IHTTPSession session); 79 | 80 | public Response other(String method, UriResource uriResource, Map urlParams, IHTTPSession session); 81 | } 82 | 83 | /** 84 | * General nanolet to inherit from if you provide stream data, only chucked 85 | * responses will be generated. 86 | */ 87 | public static abstract class DefaultStreamHandler implements UriResponder { 88 | 89 | public abstract String getMimeType(); 90 | 91 | public abstract IStatus getStatus(); 92 | 93 | public abstract InputStream getData(); 94 | 95 | public Response get(UriResource uriResource, Map urlParams, IHTTPSession session) { 96 | return NanoHTTPD.newChunkedResponse(getStatus(), getMimeType(), getData()); 97 | } 98 | 99 | public Response post(UriResource uriResource, Map urlParams, IHTTPSession session) { 100 | return get(uriResource, urlParams, session); 101 | } 102 | 103 | public Response put(UriResource uriResource, Map urlParams, IHTTPSession session) { 104 | return get(uriResource, urlParams, session); 105 | } 106 | 107 | public Response delete(UriResource uriResource, Map urlParams, IHTTPSession session) { 108 | return get(uriResource, urlParams, session); 109 | } 110 | 111 | public Response other(String method, UriResource uriResource, Map urlParams, IHTTPSession session) { 112 | return get(uriResource, urlParams, session); 113 | } 114 | } 115 | 116 | /** 117 | * General nanolet to inherit from if you provide text or html data, only 118 | * fixed size responses will be generated. 119 | */ 120 | public static abstract class DefaultHandler extends DefaultStreamHandler { 121 | 122 | public abstract IStatus getStatus(); 123 | 124 | public Response get(UriResource uriResource, Map urlParams, IHTTPSession session) { 125 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), ""); 126 | } 127 | 128 | @Override 129 | public InputStream getData() { 130 | throw new IllegalStateException("this method should not be called in a text based nanolet"); 131 | } 132 | } 133 | 134 | /** 135 | * General nanolet to print debug info's as a html page. 136 | */ 137 | public static class GeneralHandler extends DefaultHandler { 138 | 139 | @Override 140 | public String getMimeType() { 141 | return "text/html"; 142 | } 143 | 144 | @Override 145 | public IStatus getStatus() { 146 | return Status.OK; 147 | } 148 | 149 | public Response get(UriResource uriResource, Map urlParams, IHTTPSession session) { 150 | StringBuilder text = new StringBuilder(""); 151 | text.append("

Url: "); 152 | text.append(session.getUri()); 153 | text.append("


"); 154 | Map queryParams = session.getParms(); 155 | if (queryParams.size() > 0) { 156 | for (Map.Entry entry : queryParams.entrySet()) { 157 | String key = entry.getKey(); 158 | String value = entry.getValue(); 159 | text.append("

Param '"); 160 | text.append(key); 161 | text.append("' = "); 162 | text.append(value); 163 | text.append("

"); 164 | } 165 | } else { 166 | text.append("

no params in url


"); 167 | } 168 | return NanoHTTPD.newFixedLengthResponse(getStatus(), getMimeType(), text.toString()); 169 | } 170 | } 171 | 172 | /** 173 | * General nanolet to print debug info's as a html page. 174 | */ 175 | public static class StaticPageHandler extends DefaultHandler { 176 | 177 | private static String[] getPathArray(String uri) { 178 | String array[] = uri.split("/"); 179 | ArrayList pathArray = new ArrayList(); 180 | 181 | for (String s : array) { 182 | if (s.length() > 0) 183 | pathArray.add(s); 184 | } 185 | 186 | return pathArray.toArray(new String[]{}); 187 | 188 | } 189 | 190 | @Override 191 | public String getMimeType() { 192 | throw new IllegalStateException("this method should not be called"); 193 | } 194 | 195 | @Override 196 | public IStatus getStatus() { 197 | return Status.OK; 198 | } 199 | 200 | public Response get(UriResource uriResource, Map urlParams, IHTTPSession session) { 201 | String baseUri = uriResource.getUri(); 202 | String realUri = normalizeUri(session.getUri()); 203 | for (int index = 0; index < Math.min(baseUri.length(), realUri.length()); index++) { 204 | if (baseUri.charAt(index) != realUri.charAt(index)) { 205 | realUri = normalizeUri(realUri.substring(index)); 206 | break; 207 | } 208 | } 209 | File fileOrdirectory = uriResource.initParameter(File.class); 210 | for (String pathPart : getPathArray(realUri)) { 211 | fileOrdirectory = new File(fileOrdirectory, pathPart); 212 | } 213 | if (fileOrdirectory.isDirectory()) { 214 | fileOrdirectory = new File(fileOrdirectory, "index.html"); 215 | if (!fileOrdirectory.exists()) { 216 | fileOrdirectory = new File(fileOrdirectory.getParentFile(), "index.htm"); 217 | } 218 | } 219 | if (!fileOrdirectory.exists() || !fileOrdirectory.isFile()) { 220 | return new Error404UriHandler().get(uriResource, urlParams, session); 221 | } else { 222 | try { 223 | return NanoHTTPD.newChunkedResponse(getStatus(), getMimeTypeForFile(fileOrdirectory.getName()), fileToInputStream(fileOrdirectory)); 224 | } catch (IOException ioe) { 225 | return NanoHTTPD.newFixedLengthResponse(NanoHTTPD.Response.Status.REQUEST_TIMEOUT, "text/plain", null); 226 | } 227 | } 228 | } 229 | 230 | protected BufferedInputStream fileToInputStream(File fileOrdirectory) throws IOException { 231 | return new BufferedInputStream(new FileInputStream(fileOrdirectory)); 232 | } 233 | } 234 | 235 | /** 236 | * Handling error 404 - unrecognized urls 237 | */ 238 | public static class Error404UriHandler extends DefaultHandler { 239 | 240 | public String getText() { 241 | return "

Error 404: the requested page doesn't exist.

"; 242 | } 243 | 244 | @Override 245 | public String getMimeType() { 246 | return "text/html"; 247 | } 248 | 249 | @Override 250 | public IStatus getStatus() { 251 | return Status.NOT_FOUND; 252 | } 253 | } 254 | 255 | /** 256 | * Handling index 257 | */ 258 | public static class IndexHandler extends DefaultHandler { 259 | 260 | public String getText() { 261 | return "

Hello world!

"; 262 | } 263 | 264 | @Override 265 | public String getMimeType() { 266 | return "text/html"; 267 | } 268 | 269 | @Override 270 | public IStatus getStatus() { 271 | return Status.OK; 272 | } 273 | 274 | } 275 | 276 | public static class NotImplementedHandler extends DefaultHandler { 277 | 278 | public String getText() { 279 | return "

The uri is mapped in the router, but no handler is specified.
Status: Not implemented!

"; 280 | } 281 | 282 | @Override 283 | public String getMimeType() { 284 | return "text/html"; 285 | } 286 | 287 | @Override 288 | public IStatus getStatus() { 289 | return Status.OK; 290 | } 291 | } 292 | 293 | public static String normalizeUri(String value) { 294 | if (value == null) { 295 | return value; 296 | } 297 | if (value.startsWith("/")) { 298 | value = value.substring(1); 299 | } 300 | if (value.endsWith("/")) { 301 | value = value.substring(0, value.length() - 1); 302 | } 303 | return value; 304 | 305 | } 306 | 307 | public static class UriResource { 308 | 309 | private static final Pattern PARAM_PATTERN = Pattern.compile("(?<=(^|/)):[a-zA-Z0-9_-]+(?=(/|$))"); 310 | 311 | private static final String PARAM_MATCHER = "([A-Za-z0-9\\-\\._~:/?#\\[\\]@!\\$&'\\(\\)\\*\\+,;=\\s]+)"; 312 | 313 | private static final Map EMPTY = Collections.unmodifiableMap(new HashMap()); 314 | 315 | private final String uri; 316 | 317 | private final Pattern uriPattern; 318 | 319 | private final int priority; 320 | 321 | private final Class handler; 322 | 323 | private final Object handlerObject; 324 | 325 | private final Object[] initParameter; 326 | 327 | private List uriParams = new ArrayList(); 328 | 329 | private final String method; 330 | 331 | public UriResource(String uri, String method, int priority, Object handlerObject, Object... initParameter) { 332 | this.handler = null; 333 | this.handlerObject = handlerObject; 334 | this.initParameter = initParameter; 335 | if (uri != null) { 336 | this.uri = normalizeUri(uri); 337 | this.uriPattern = createUriPattern(); 338 | } else { 339 | this.uriPattern = null; 340 | this.uri = null; 341 | } 342 | this.method = method; 343 | this.priority = priority + uriParams.size() * 1000; 344 | } 345 | 346 | private Pattern createUriPattern() { 347 | String patternUri = uri; 348 | 349 | Matcher matcher = PARAM_PATTERN.matcher(patternUri); 350 | int start = 0; 351 | while (matcher.find(start)) { 352 | uriParams.add(patternUri.substring(matcher.start() + 1, matcher.end())); 353 | patternUri = new StringBuilder(patternUri.substring(0, matcher.start())) 354 | .append(PARAM_MATCHER) 355 | .append(patternUri.substring(matcher.end())).toString(); 356 | start = matcher.start() + PARAM_MATCHER.length(); 357 | matcher = PARAM_PATTERN.matcher(patternUri); 358 | } 359 | return Pattern.compile(patternUri); 360 | } 361 | 362 | public Response process(Map urlParams, IHTTPSession session) { 363 | String error = "General error!"; 364 | if (handlerObject != null || handler != null) { 365 | try { 366 | Object object = ((handlerObject != null) ? handlerObject : handler.newInstance()); 367 | if (object instanceof UriResponder) { 368 | UriResponder responder = (UriResponder) object; 369 | switch (this.method) { 370 | case Methods.GET: 371 | return responder.get(this, urlParams, session); 372 | case Methods.POST: 373 | return responder.post(this, urlParams, session); 374 | case Methods.PUT: 375 | return responder.put(this, urlParams, session); 376 | case Methods.DELETE: 377 | return responder.delete(this, urlParams, session); 378 | default: 379 | return responder.other(session.getMethod().toString(), this, urlParams, session); 380 | } 381 | } else { 382 | return NanoHTTPD.newFixedLengthResponse(Status.OK, "text/plain", // 383 | new StringBuilder("Return: ")// 384 | .append(handler.getCanonicalName())// 385 | .append(".toString() -> ")// 386 | .append(object)// 387 | .toString()); 388 | } 389 | } catch (Exception e) { 390 | error = "Error: " + e.getClass().getName() + " : " + e.getMessage(); 391 | LOG.log(Level.SEVERE, error, e); 392 | } 393 | } 394 | return NanoHTTPD.newFixedLengthResponse(Status.INTERNAL_ERROR, "text/plain", error); 395 | } 396 | 397 | @Override 398 | public String toString() { 399 | return new StringBuilder("UrlResource{uri='").append((uri == null ? "/" : uri))// 400 | .append("', urlParts=").append(uriParams)// 401 | .append('}')// 402 | .toString(); 403 | } 404 | 405 | public String getUri() { 406 | return uri; 407 | } 408 | 409 | public T initParameter(Class paramClazz) { 410 | return initParameter(0, paramClazz); 411 | } 412 | 413 | public T initParameter(int parameterIndex, Class paramClazz) { 414 | if (initParameter.length > parameterIndex) { 415 | return paramClazz.cast(initParameter[parameterIndex]); 416 | } 417 | LOG.severe("init parameter index not available " + parameterIndex); 418 | return null; 419 | } 420 | 421 | public Map match(String url) { 422 | Matcher matcher = uriPattern.matcher(url); 423 | if (matcher.matches()) { 424 | if (uriParams.size() > 0) { 425 | Map result = new HashMap(); 426 | for (int i = 1; i <= matcher.groupCount(); i++) { 427 | result.put(uriParams.get(i - 1), matcher.group(i)); 428 | } 429 | return result; 430 | } else { 431 | return EMPTY; 432 | } 433 | } 434 | return null; 435 | } 436 | 437 | } 438 | 439 | public static class UriRouter { 440 | 441 | private List mappings; 442 | 443 | private UriResource error404Url; 444 | 445 | private Class notImplemented; 446 | 447 | public UriRouter() { 448 | mappings = new ArrayList(); 449 | } 450 | 451 | /** 452 | * Search in the mappings if the given url matches some of the rules If 453 | * there are more than one marches returns the rule with less parameters 454 | * e.g. mapping 1 = /user/:id mapping 2 = /user/help if the incoming uri 455 | * is www.example.com/user/help - mapping 2 is returned if the incoming 456 | * uri is www.example.com/user/3232 - mapping 1 is returned 457 | * 458 | * @param session 459 | * @return 460 | */ 461 | public Response process(IHTTPSession session) { 462 | String work = normalizeUri(session.getUri()); 463 | Map params = null; 464 | UriResource uriResource = error404Url; 465 | for (UriResource u : mappings) { 466 | params = u.match(work); 467 | if (params != null) { 468 | uriResource = u; 469 | break; 470 | } 471 | } 472 | 473 | if (uriResource == null) { 474 | return null; 475 | } 476 | return uriResource.process(params, session); 477 | } 478 | 479 | private void addRoute(String url, String method, int priority, Object handlerObject, Object... initParameter) { 480 | if (url != null) { 481 | if (handlerObject != null) { 482 | mappings.add(new UriResource(url, method, priority + mappings.size(), handlerObject, initParameter)); 483 | } else { 484 | mappings.add(new UriResource(url, method, priority + mappings.size(), notImplemented)); 485 | } 486 | sortMappings(); 487 | } 488 | } 489 | 490 | private void sortMappings() { 491 | Collections.sort(mappings, new Comparator() { 492 | 493 | @Override 494 | public int compare(UriResource o1, UriResource o2) { 495 | return o2.priority - o1.priority; 496 | } 497 | }); 498 | } 499 | 500 | private void removeRoute(String url) { 501 | String uriToDelete = normalizeUri(url); 502 | Iterator iter = mappings.iterator(); 503 | while (iter.hasNext()) { 504 | UriResource uriResource = iter.next(); 505 | if (uriToDelete.equals(uriResource.getUri())) { 506 | iter.remove(); 507 | break; 508 | } 509 | } 510 | } 511 | 512 | public void setNotFoundHandler(Class handler) { 513 | error404Url = new UriResource(null, Methods.GET, 100, handler); 514 | } 515 | 516 | public void setNotFoundHandler(Object handlerObject) { 517 | error404Url = new UriResource(null, Methods.GET, 100, handlerObject); 518 | } 519 | 520 | public void setNotImplemented(Class handler) { 521 | notImplemented = handler; 522 | } 523 | 524 | } 525 | 526 | private UriRouter router; 527 | 528 | public RouterNanoHTTPD(int port) { 529 | super(port); 530 | router = new UriRouter(); 531 | } 532 | 533 | public RouterNanoHTTPD(String hostname, int port) { 534 | super(hostname, port); 535 | router = new UriRouter(); 536 | } 537 | 538 | /** 539 | * default routings, they are over writable. 540 | * 541 | *
542 |      * router.setNotFoundHandler(GeneralHandler.class);
543 |      * 
544 | */ 545 | 546 | public void addMappings() { 547 | router.setNotImplemented(NotImplementedHandler.class); 548 | router.setNotFoundHandler(Error404UriHandler.class); 549 | router.addRoute("/", Methods.GET, Integer.MAX_VALUE / 2, IndexHandler.class); 550 | router.addRoute("/index.html", Methods.GET, Integer.MAX_VALUE / 2, IndexHandler.class); 551 | } 552 | 553 | public void addRoute(String url, String method, Object handlerObject, Object... initParameter) { 554 | router.addRoute(url, method, 100, handlerObject, initParameter); 555 | } 556 | 557 | public void removeRoute(String url) { 558 | router.removeRoute(url); 559 | } 560 | 561 | @Override 562 | public Response serve(IHTTPSession session) { 563 | long bodySize = ((HTTPSession)session).getBodySize(); 564 | session.getInputStream().mark(HTTPSession.BUFSIZE); 565 | 566 | // Try to find match 567 | Response r = router.process(session); 568 | 569 | //clear remain body 570 | try{ 571 | session.getInputStream().reset(); 572 | session.getInputStream().skip(bodySize); 573 | } 574 | catch (Exception e){ 575 | String error = "Error: " + e.getClass().getName() + " : " + e.getMessage(); 576 | LOG.log(Level.SEVERE, error, e); 577 | } 578 | return r; 579 | } 580 | } 581 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 24 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macacajs/UIAutomatorWD/61866ae9c1c55865f2af091a46aabeeee96f9034/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macacajs/UIAutomatorWD/61866ae9c1c55865f2af091a46aabeeee96f9034/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macacajs/UIAutomatorWD/61866ae9c1c55865f2af091a46aabeeee96f9034/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macacajs/UIAutomatorWD/61866ae9c1c55865f2af091a46aabeeee96f9034/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/macacajs/UIAutomatorWD/61866ae9c1c55865f2af091a46aabeeee96f9034/app/src/main/res/drawable-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 24 | 25 | 26 | 35 | 36 | 37 | 43 | 44 |