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 |
51 |
52 |
53 |
60 |
61 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_show_text.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
22 |
23 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v13/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
21 | 64dp
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 | 16dp
20 | 32dp
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 | UiAutomator sample
19 | Hello UiAutomator!
20 | Change text
21 | type something…
22 | Open activity and change text
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | maven {
6 | url mavenMirrorUrl
7 | }
8 | jcenter()
9 | google()
10 | }
11 | dependencies {
12 | classpath 'com.android.tools.build:gradle:7.1.3'
13 | }
14 | }
15 |
16 | allprojects {
17 | repositories {
18 | maven {
19 | url mavenMirrorUrl
20 | }
21 | jcenter()
22 | google()
23 | }
24 | }
25 |
26 | ext {
27 | buildToolsVersion = "29.0.2"
28 | supportLibVersion = "24.2.0"
29 | runnerVersion = "0.5"
30 | rulesVersion = "0.5"
31 | uiautomatorVersion = "2.1.2"
32 | nanohttpdVersion = "2.3.1"
33 | fastjsonVersion = "1.2.31"
34 | }
35 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | mavenMirrorUrl=
2 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macacajs/UIAutomatorWD/61866ae9c1c55865f2af091a46aabeeee96f9034/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Dec 16 11:23:51 CST 2020
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = require('./lib/uiautomator-client');
4 |
--------------------------------------------------------------------------------
/lib/helper.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const macacaUtils = require('xutil');
4 | const childProcess = require('child_process');
5 |
6 | var _ = macacaUtils.merge({}, macacaUtils);
7 |
8 | _.sleep = function(ms) {
9 | return new Promise(resolve => {
10 | setTimeout(resolve, ms);
11 | });
12 | };
13 |
14 | _.retry = function(func, interval, num) {
15 | return new Promise((resolve, reject) => {
16 | func().then(resolve, err => {
17 | if (num > 0 || typeof num === 'undefined') {
18 | _.sleep(interval).then(() => {
19 | resolve(_.retry(func, interval, num - 1));
20 | });
21 | } else {
22 | reject(err);
23 | }
24 | });
25 | });
26 | };
27 |
28 | _.waitForCondition = function(func, wait, interval) {
29 | wait = wait || 5000;
30 | interval = interval || 500;
31 | let start = Date.now();
32 | let end = start + wait;
33 |
34 | const fn = function() {
35 | return new Promise((resolve, reject) => {
36 | const continuation = (res, rej) => {
37 | let now = Date.now();
38 |
39 | if (now < end) {
40 | res(_.sleep(interval).then(fn));
41 | } else {
42 | rej(`Wait For Condition timeout ${wait}`);
43 | }
44 | };
45 | func().then(isOk => {
46 |
47 | if (isOk) {
48 | resolve();
49 | } else {
50 | continuation(resolve, reject);
51 | }
52 | }).catch(() => {
53 | continuation(resolve, reject);
54 | });
55 | });
56 | };
57 | return fn();
58 | };
59 |
60 | _.escapeString = function(str) {
61 | return str
62 | .replace(/[\\]/g, '\\\\')
63 | .replace(/[\/]/g, '\\/')
64 | .replace(/[\b]/g, '\\b')
65 | .replace(/[\f]/g, '\\f')
66 | .replace(/[\n]/g, '\\n')
67 | .replace(/[\r]/g, '\\r')
68 | .replace(/[\t]/g, '\\t')
69 | .replace(/[\"]/g, '\\"')
70 | .replace(/\\'/g, "\\'");
71 | };
72 |
73 | _.exec = function(cmd, opts) {
74 | return new Promise((resolve, reject) => {
75 | childProcess.exec(cmd, _.merge({
76 | maxBuffer: 1024 * 512,
77 | wrapArgs: false
78 | }, opts || {}), (err, stdout) => {
79 | if (err) {
80 | return reject(err);
81 | }
82 | resolve(_.trim(stdout));
83 | });
84 | });
85 | };
86 |
87 | _.spawn = function() {
88 | var args = Array.prototype.slice.call(arguments);
89 |
90 | return new Promise((resolve, reject) => {
91 | var stdout = '';
92 | var stderr = '';
93 | var child = childProcess.spawn.apply(childProcess, args);
94 |
95 | child.on('error', error => {
96 | reject(error);
97 | });
98 |
99 | child.stdout.on('data', data => {
100 | stdout += data;
101 | });
102 |
103 | child.stderr.on('data', data => {
104 | stderr += data;
105 | });
106 |
107 | child.on('close', code => {
108 | var error;
109 | if (code) {
110 | error = new Error(stderr);
111 | error.code = code;
112 | return reject(error);
113 | }
114 | resolve([stdout, stderr]);
115 | });
116 | });
117 | };
118 |
119 | var Defer = function() {
120 | this._resolve = null;
121 | this._reject = null;
122 | this.promise = new Promise((resolve, reject) => {
123 | this._resolve = resolve;
124 | this._reject = reject;
125 | });
126 | };
127 |
128 | Defer.prototype.resolve = function(data) {
129 | this._resolve(data);
130 | };
131 |
132 | Defer.prototype.reject = function(err) {
133 | this._reject(err);
134 | };
135 |
136 | _.Defer = Defer;
137 |
138 | module.exports = _;
139 |
140 |
--------------------------------------------------------------------------------
/lib/logger.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var logger = require('xlogger');
4 |
5 | module.exports = logger.Logger({
6 | closeFile: true
7 | });
8 |
--------------------------------------------------------------------------------
/lib/proxy.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const request = require('request');
4 |
5 | const _ = require('./helper');
6 | const logger = require('./logger');
7 |
8 | class WDProxy {
9 | constructor(options) {
10 | Object.assign(this, {
11 | scheme: 'http',
12 | proxyHost: '127.0.0.1',
13 | proxyPort: 9001,
14 | urlBase: 'wd/hub',
15 | sessionId: null,
16 | originSessionId: null
17 | }, options);
18 | }
19 |
20 | handleNewUrl(url) {
21 | const sessionReg = /\/session\/([^\/]+)/;
22 | const wdSessionReg = new RegExp(`${this.urlBase}\/session\/([^\/]+)`);
23 | url = `${this.scheme}://${this.proxyHost}:${this.proxyPort}${url}`;
24 |
25 | if (sessionReg.test(url) && this.sessionId) {
26 | this.originSessionId = url.match(sessionReg)[1];
27 | url = url.replace(wdSessionReg, `${this.urlBase}/session/${this.sessionId}`);
28 | }
29 | return url;
30 | }
31 |
32 | send(url, method, body) {
33 | return new Promise((resolve, reject) => {
34 | method = method.toUpperCase();
35 | const newUrl = this.handleNewUrl(url);
36 | const retryCount = 10;
37 | const retryInterval = 2000;
38 |
39 | const reqOpts = {
40 | url: newUrl,
41 | method: method,
42 | headers: {
43 | 'Content-type': 'application/json;charset=utf-8'
44 | },
45 | forever: true,
46 | resolveWithFullResponse: true
47 | };
48 |
49 | if (body && (method.toUpperCase() === 'POST' || method.toUpperCase() === 'PUT')) {
50 | if (typeof body !== 'object') {
51 | body = JSON.parse(body);
52 | }
53 | reqOpts.json = body;
54 | }
55 |
56 | logger.debug(`Proxy: ${url}:${method} to ${newUrl}:${method} with body: ${_.truncate(JSON.stringify(body), {
57 | length: 200
58 | })}`);
59 |
60 | _.retry(() => {
61 | return new Promise((_resolve, _reject) => {
62 | request(reqOpts, (error, res, body) => {
63 | if (error) {
64 | logger.debug(`UIAutomatorWD client proxy error with: ${error}`);
65 | return _reject(error);
66 | }
67 |
68 | if (!body) {
69 | logger.debug('UIAutomatorWD client proxy received no data.');
70 | return _reject('No data received from UIAutomatorWD Server.');
71 | }
72 |
73 | if (typeof body !== 'object') {
74 | try {
75 | body = JSON.parse(body);
76 | } catch (e) {
77 | logger.debug(`Fail to parse body: ${e}`);
78 | return _reject('Retry due to invalid body');
79 | }
80 | }
81 |
82 | if (body && body.sessionId) {
83 | this.sessionId = body.sessionId;
84 | body.sessionId = this.originSessionId;
85 | }
86 |
87 | logger.debug(`Got response with status ${res.statusCode}: ${_.truncate(JSON.stringify(body), {
88 | length: 200
89 | })}`);
90 | _resolve(body);
91 | });
92 |
93 | });
94 | }, retryInterval, retryCount).then(resolve, reject);
95 | });
96 | }
97 | }
98 |
99 | module.exports = WDProxy;
100 |
--------------------------------------------------------------------------------
/lib/uiautomator-client.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const _ = require('./helper');
4 | const logger = require('./logger');
5 | const WDProxy = require('./proxy');
6 | const UIAUTOMATORWD = require('./uiautomatorwd');
7 |
8 | const detectPort = _.detectPort;
9 |
10 | function UIAutomator(options) {
11 | this.adb = null;
12 | this.proxy = null;
13 | Object.assign(this, {
14 | proxyHost: '127.0.0.1',
15 | proxyPort: process.env.UIAUTOMATOR_PORT || 9001,
16 | urlBase: 'wd/hub'
17 | }, options || {});
18 | }
19 |
20 | UIAutomator.prototype.init = function *(adb, permissionPatterns) {
21 | this.adb = adb;
22 | this.permissionPatterns = permissionPatterns;
23 | this.proxyPort = yield detectPort(this.proxyPort);
24 | this.initProxy();
25 | yield this.adb.forward(this.proxyPort, this.proxyPort);
26 | const ANDROID_TMP_DIR = this.adb.getTmpDir();
27 |
28 | // install dirver pkg olny when pkg not exists
29 | try {
30 | let testPkgList = yield this.adb.shell(`pm list packages | grep ${UIAUTOMATORWD.TEST_PACKAGE}$`);
31 | if (testPkgList && testPkgList.split(':')[1] === UIAUTOMATORWD.TEST_PACKAGE) {
32 | logger.debug(`Package ${UIAUTOMATORWD.TEST_PACKAGE} already exists. No need reinstall.`);
33 | yield this.adb.shell(`pm clear "${UIAUTOMATORWD.TEST_PACKAGE}"`);
34 | } else {
35 | throw new Error('Package is not exists');
36 | }
37 | } catch (e) {
38 | logger.debug(`Package ${UIAUTOMATORWD.TEST_PACKAGE} is not exists,execute intsall action.`);
39 | if (_.isExistedFile(UIAUTOMATORWD.TEST_APK_BUILD_PATH)) {
40 | yield this.adb.push(UIAUTOMATORWD.TEST_APK_BUILD_PATH, `${ANDROID_TMP_DIR}/${UIAUTOMATORWD.TEST_PACKAGE}`);
41 | } else {
42 | logger.error(`${UIAUTOMATORWD.TEST_APK_BUILD_PATH} not found, please resolve and reinstall android driver`);
43 | }
44 | yield this.adb.shell(`pm install -r "${ANDROID_TMP_DIR}/${UIAUTOMATORWD.TEST_PACKAGE}"`);
45 | }
46 |
47 | // install dirver pkg olny when pkg not exists
48 | try {
49 | let extraTestPkgList = yield this.adb.shell(`pm list packages | grep ${UIAUTOMATORWD.TEST_PACKAGE}.test$`);
50 | if (extraTestPkgList && extraTestPkgList.split(':')[1] === 'com.macaca.android.testing.test') {
51 | logger.debug(`Package ${UIAUTOMATORWD.TEST_PACKAGE}.test already exists. No need reinstall.`);
52 | yield this.adb.shell(`pm clear "${UIAUTOMATORWD.TEST_PACKAGE}.test"`);
53 | } else {
54 | throw new Error('Package is not exists');
55 | }
56 | } catch (e) {
57 | logger.debug(`Package ${UIAUTOMATORWD.TEST_PACKAGE}.test is not exists,execute intsall action.`);
58 | if (_.isExistedFile(UIAUTOMATORWD.APK_BUILD_PATH)) {
59 | yield this.adb.push(UIAUTOMATORWD.APK_BUILD_PATH, `${ANDROID_TMP_DIR}/${UIAUTOMATORWD.TEST_PACKAGE}.test`);
60 | } else {
61 | logger.error(`${UIAUTOMATORWD.APK_BUILD_PATH} not found, please resolve and reinstall android driver`);
62 | }
63 | yield this.adb.shell(`pm install -r "${ANDROID_TMP_DIR}/${UIAUTOMATORWD.TEST_PACKAGE}.test"`);
64 | }
65 |
66 | yield this.adb.shell(`am force-stop ${UIAUTOMATORWD.TEST_PACKAGE}`);
67 | yield this.adb.shell(`am force-stop ${UIAUTOMATORWD.TEST_PACKAGE}.test`);
68 | yield this.startServer();
69 | };
70 |
71 | UIAutomator.prototype.initProxy = function() {
72 | this.proxy = new WDProxy({
73 | proxyHost: this.proxyHost,
74 | proxyPort: this.proxyPort,
75 | urlBase: this.urlBase
76 | });
77 | };
78 |
79 | UIAutomator.prototype.startServer = function() {
80 | return new Promise(resolve => {
81 | let permissionPatterns = this.permissionPatterns ? `-e permissionPattern ${this.permissionPatterns}` : '';
82 | let args = `shell am instrument -w -r ${permissionPatterns} -e port ${this.proxyPort} -e class ${UIAUTOMATORWD.PACKAGE_NAME} ${UIAUTOMATORWD.TEST_PACKAGE}.test/${UIAUTOMATORWD.RUNNER_CLASS}`.split(' ');
83 |
84 | var proc = this.adb.spawn(args, {
85 | path: process.cwd(),
86 | env: process.env
87 | });
88 |
89 | proc.stderr.setEncoding('utf8');
90 | proc.stdout.setEncoding('utf8');
91 | proc.stdout.on('data', data => {
92 | logger.debug(data);
93 | let match = UIAUTOMATORWD.SERVER_URL_REG.exec(data);
94 |
95 | if (match) {
96 | const url = match[1];
97 |
98 | if (url.startsWith('http://')) {
99 | logger.info('UIAutomatorWD http server ready');
100 | resolve();
101 | }
102 | }
103 | });
104 | proc.stderr.on('data', data => {
105 | logger.info(data);
106 | });
107 | });
108 | };
109 |
110 | UIAutomator.prototype.sendCommand = function *(url, method, body) {
111 | let isServerStillAlive = true;
112 | let ret;
113 | try {
114 | ret = yield this.proxy.send(url, method, body);
115 | if (ret && ret.hasOwnProperty('status')) {
116 | return ret;
117 | }
118 | } catch (e) {
119 | logger.warn('proxy.send except: %s', e);
120 | }
121 |
122 | try {
123 | let pids = yield this.adb.getPIds(UIAUTOMATORWD.TEST_PACKAGE);
124 | // If the pids array is empty, that means the WD server is killed
125 | isServerStillAlive = pids.length > 0;
126 | } catch (e) {
127 | // Unable to get the pid info, assume the server is still alive
128 | logger.warn(`get pids of ${UIAUTOMATORWD.TEST_PACKAGE} failed, ignore this log`);
129 | }
130 |
131 | if (!isServerStillAlive) {
132 | logger.info('restart UIAutomatorWD server');
133 | // restart the WD server
134 | yield this.startServer();
135 | ret = yield this.proxy.send(url, method, body);
136 | }
137 | return ret;
138 | };
139 |
140 | module.exports = UIAutomator;
141 | module.exports.UIAUTOMATORWD = UIAUTOMATORWD;
142 |
--------------------------------------------------------------------------------
/lib/uiautomatorwd.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 |
5 | exports.APK_BUILD_PATH = path.join(__dirname, '..', 'app', 'build', 'outputs', 'apk', 'androidTest', 'debug', 'app-debug-androidTest.apk');
6 | exports.TEST_APK_BUILD_PATH = path.join(__dirname, '..', 'app', 'build', 'outputs', 'apk', 'debug', 'app-debug.apk');
7 | exports.PACKAGE_NAME = 'com.macaca.android.testing.UIAutomatorWD';
8 | exports.TEST_PACKAGE = 'com.macaca.android.testing';
9 | exports.RUNNER_CLASS = 'android.support.test.runner.AndroidJUnitRunner';
10 | exports.SERVER_URL_REG = /UIAutomatorWD->(.*)<-UIAutomatorWD/;
11 |
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "uiautomatorwd",
3 | "version": "1.2.4",
4 | "description": "Node.js wrapper for UIAutomator2",
5 | "keywords": [
6 | "uiautomator",
7 | "Node.js"
8 | ],
9 | "main": "index.js",
10 | "repository": {
11 | "type": "git",
12 | "url": "https:github.com/macacajs/uiautomatorwd.git"
13 | },
14 | "dependencies": {
15 | "gradle": "1",
16 | "macaca-adb": "1",
17 | "request": "^2.81.0",
18 | "xlogger": "~1.0.0",
19 | "xutil": "~1.0.1"
20 | },
21 | "devDependencies": {
22 | "eslint": "^7.15.0",
23 | "eslint-plugin-mocha": "^8.0.0",
24 | "git-contributor": "1",
25 | "husky": "^1.3.1",
26 | "mocha": "*",
27 | "nyc": "^13.3.0"
28 | },
29 | "scripts": {
30 | "test": "nyc --reporter=lcov --reporter=text mocha",
31 | "lint": "eslint --fix lib test",
32 | "install": "node ./scripts/build.js",
33 | "contributor": "git-contributor"
34 | },
35 | "husky": {
36 | "hooks": {
37 | "pre-commit": "npm run lint"
38 | }
39 | },
40 | "homepage": "https://github.com/macacajs/uiautomatorwd",
41 | "license": "MIT"
42 | }
43 |
--------------------------------------------------------------------------------
/scripts/build.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | 'use strict';
4 |
5 | const path = require('path');
6 | const gradle = require('gradle');
7 |
8 | const cwd = path.join(__dirname, '..');
9 |
10 | var args = [
11 | 'assembleDebug',
12 | 'assembleDebugAndroidTest'
13 | ];
14 |
15 | args.push(`-PmavenMirrorUrl=${process.env.MAVEN_MIRROR_URL || ''}`);
16 |
17 | gradle({
18 | cwd: cwd,
19 | args: args
20 | });
21 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --reporter spec
2 |
--------------------------------------------------------------------------------
/test/uiautomator-client.test.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const _ = require('xutil');
4 | const assert = require('assert');
5 |
6 | const UIAutomator = require('..');
7 |
8 | describe('uiautomator', function() {
9 |
10 | it('should be ok', function() {
11 |
12 | assert(_.isExistedFile(UIAutomator.UIAUTOMATORWD.APK_BUILD_PATH), true);
13 | /*
14 | var adb = new ADB();
15 | var devices = yield ADB.getDevices();
16 |
17 | if (!devices.length) {
18 | done();
19 | }
20 |
21 | var device = devices[0];
22 | adb.setDeviceId(device.udid);
23 | yield client.init(adb);
24 | */
25 | });
26 | });
27 |
--------------------------------------------------------------------------------