From b1aa1df3c6335fa5e85881649a959b5182e2663c Mon Sep 17 00:00:00 2001 From: codeskyblue Date: Wed, 25 Dec 2019 19:38:30 +0800 Subject: [PATCH] change logic of start uiautomator, upgrade apk version --- examples/test_simple_example.py | 13 ++++++ requirements.txt | 2 +- tests/runtest.sh | 10 +++++ uiautomator2/__init__.py | 79 ++++++++++++++++++++++++++------- uiautomator2/image.py | 8 ++++ uiautomator2/init.py | 25 +++++++---- uiautomator2/session.py | 3 +- uiautomator2/version.py | 4 +- 8 files changed, 114 insertions(+), 30 deletions(-) create mode 100644 examples/test_simple_example.py create mode 100755 tests/runtest.sh diff --git a/examples/test_simple_example.py b/examples/test_simple_example.py new file mode 100644 index 00000000..5a4089a5 --- /dev/null +++ b/examples/test_simple_example.py @@ -0,0 +1,13 @@ +# coding: utf-8 +# + +import uiautomator2 as u2 + + +def test_simple(): + d = u2.connect() + print(d.info) + + +if __name__ == "__main__": + test_simple() diff --git a/requirements.txt b/requirements.txt index 2dabffb3..e4889a8f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ whichcraft logzero~=1.5 progress~=1.3 retry~=0.9 -adbutils>=0.6.4,<1.0 +adbutils>=0.7.0,<1.0 Deprecated~=1.2.6 Pillow # not support in QPython lxml>=4.3 # not support in QPython diff --git a/tests/runtest.sh b/tests/runtest.sh new file mode 100755 index 00000000..d8a26733 --- /dev/null +++ b/tests/runtest.sh @@ -0,0 +1,10 @@ +#!/bin/bash +# + +set -e + +if [[ $# -eq 0 ]] +then + python3 -m adbutils -i https://github.com/appium/java-client/raw/master/src/test/java/io/appium/java_client/ApiDemos-debug.apk +fi +py.test -v "$@" diff --git a/uiautomator2/__init__.py b/uiautomator2/__init__.py index daad3b83..a89b359b 100644 --- a/uiautomator2/__init__.py +++ b/uiautomator2/__init__.py @@ -624,6 +624,12 @@ def alive(self): except EnvironmentError: return False + def _kill_process_by_name(self, name): + for p in self._iter_process(): + if p.name == name and p.user == "shell": + logger.debug("kill uiautomator") + self.shell(["kill", "-9", str(p.pid)]) + def service(self, name): """ Manage service start or stop @@ -648,10 +654,20 @@ def raise_for_status(self, res): res.raise_for_status() def start(self): + """ + Manually run with the following command: + adb shell am instrument -w -r -e debug false -e class com.github.uiautomator.stub.Stub \ + com.github.uiautomator.test/android.support.test.runner.AndroidJUnitRunner + """ + # kill uiautomator res = u2obj._reqsess.post(self.service_url) self.raise_for_status(res) def stop(self): + """ + 1. stop command which launched with uiautomator 1.0 + Eg: adb shell uiautomator runtest androidUiAutomator.jar + """ res = u2obj._reqsess.delete(self.service_url) self.raise_for_status(res) @@ -694,6 +710,7 @@ def reset_uiautomator(self): with self.__uiautomator_lock: if self.alive: return + logger.debug("force reset uiautomator") success = self._force_reset_uiautomator_v2() # uiautomator 2.0 if not success: @@ -721,42 +738,47 @@ def _start_uiautomator_app(self): package_name = "com.github.uiautomator" if self.settings['uiautomator_runtest_app_background']: self.shell(f"pm grant {package_name} android.permission.READ_PHONE_STATE") - self.app_start(package_name, launch_timeout=10) + self.app_start(package_name, ".ToastActivity") else: self.shell(f'am startservice -n {package_name}/.Service') def _force_reset_uiautomator_v2(self): brand = self.shell("getprop ro.product.brand").output.strip() - logger.debug("Product-brand: %s", brand) + logger.debug("Device: %s, %s", brand, self.serial) package_name = "com.github.uiautomator" - first_killed = False - logger.debug("app-start com.github.uiautomator") - self._start_uiautomator_app() - self.uiautomator.start() - def is_infront(): - return self.app_current()['package'] == package_name + # logger.debug("app-start com.github.uiautomator") + + self.shell(["am", "force-stop", package_name]) + logger.debug("stop app: %s", package_name) + # self._start_uiautomator_app() + # self.uiautomator.start() + self.uiautomator.stop() + + # stop command which launched with uiautomator 1.0 + # eg: adb shell uiautomator runtest androidUiAutomator.jar + + logger.debug("kill process(ps): uiautomator") + self._kill_process_by_name("uiautomator") + self.uiautomator.start() - time.sleep(1) # wait until uiautomator2 service is working + time.sleep(.5) deadline = time.time() + 20.0 while time.time() < deadline: - logger.debug("uiautomator is starting ...") + logger.debug("uiautomator-v2 is starting ...") if not self.uiautomator.running(): break # apk might killed when call uiautomator runtest, so here launch again - if not first_killed and not is_infront(): - logger.debug("app-start com.github.uiautomator again") - self._start_uiautomator_app() + if not first_killed and package_name not in self.app_list_running(): first_killed = True + # self._start_uiautomator_app() if self.alive: - if is_infront(): - self.shell(['input', 'keyevent', 'BACK']) return True - time.sleep(1) + time.sleep(1.0) self.uiautomator.stop() return False @@ -1093,8 +1115,31 @@ def app_list_running(self) -> list: """ output, _ = self.shell(['pm', 'list', 'packages']) packages = re.findall(r'package:([^\s]+)', output) - process_names = re.findall(r'([^\s]+)$', self.shell('ps').output, re.M) + process_names = re.findall(r'([^\s]+)$', self.shell('ps; ps -A').output, re.M) return list(set(packages).intersection(process_names)) + + def _iter_process(self): + """ + List processes by cmd:ps + + Returns: + list of Process(pid, name) + """ + headers, pids = [], {} + Header = None + Process = namedtuple("Process", ["user", "pid", "name"]) + for line in self.shell("ps; ps -A").output.splitlines(): + # USER PID ..... NAME + fields = line.strip().split() + if fields[0] == "USER": + continue + if not fields[1].isdigit(): + continue + user, pid, name = fields[0], int(fields[1]), fields[-1] + if pid in pids: + continue + pids[pid] = True + yield Process(user, pid, name) def app_stop(self, pkg_name): """ Stop one application: am force-stop""" diff --git a/uiautomator2/image.py b/uiautomator2/image.py index 352b4b4a..6b34ebd5 100644 --- a/uiautomator2/image.py +++ b/uiautomator2/image.py @@ -208,6 +208,14 @@ def __init__(self, d: "uiautomator2.Device"): def send_click(self, x, y): return self._d.click(x, y) + + def getpixel(self, x, y): + """ + Returns: + (r, g, b) + """ + screenshot = self.screenshot() + return screenshot.convert("RGB").getpixel((x, y)) def match(self, imdata: Union[np.ndarray, str]): """ diff --git a/uiautomator2/init.py b/uiautomator2/init.py index a1d948f9..335372cc 100644 --- a/uiautomator2/init.py +++ b/uiautomator2/init.py @@ -1,6 +1,7 @@ # coding: utf-8 # +import datetime import hashlib import logging import os @@ -12,7 +13,9 @@ import requests from logzero import logger, setup_logger from retry import retry -from uiautomator2.version import __apk_version__, __atx_agent_version__, __jar_version__, __version__ + +from uiautomator2.version import (__apk_version__, __atx_agent_version__, + __jar_version__, __version__) appdir = os.path.join(os.path.expanduser("~"), '.uiautomator2') @@ -201,14 +204,21 @@ def is_apk_outdated(self): """ apk_debug = self._device.package_info("com.github.uiautomator") apk_debug_test = self._device.package_info("com.github.uiautomator.test") + self.logger.debug("apk-debug package-info: %s", apk_debug) + self.logger.debug("apk-debug-test package-info: %s", apk_debug_test) if not apk_debug or not apk_debug_test: return True if apk_debug['version_name'] != __apk_version__: + self.logger.info("package com.github.uiautomator version %s, latest %s", apk_debug['version_name'], __apk_version__) return True - self.logger.debug("signature: %s", apk_debug['signature']) + if apk_debug['signature'] != apk_debug_test['signature']: - self.logger.info("package com.github.uiautomator does not have a signature matching the target com.github.uiautomator") - return True + # On vivo-Y67 signature might not same, but signature matched. + # So here need to check first_install_time again + max_delta = datetime.timedelta(minutes=3) + if abs(apk_debug['first_install_time'] - apk_debug_test['first_install_time']) > max_delta: + self.logger.debug("package com.github.uiautomator does not have a signature matching the target com.github.uiautomator") + return True return False def is_atx_agent_outdated(self): @@ -245,11 +255,8 @@ def check_install(self): if self.is_atx_agent_outdated(): return False - - packages = d.list_packages() - if 'com.github.uiautomator' not in packages: - return False - if 'com.github.uiautomator.test' not in packages: + + if self.is_apk_outdated(): return False return True diff --git a/uiautomator2/session.py b/uiautomator2/session.py index 9fa2e2d6..3e8d9960 100644 --- a/uiautomator2/session.py +++ b/uiautomator2/session.py @@ -596,8 +596,7 @@ def set_clipboard(self, text, label=None): def __getattr__(self, key): if key in [ "wait_timeout", "window_size", "shell", "xpath", "widget", - "watcher", "settings", - "app_current", "app_start", "app_stop" + "watcher", "settings", "app_current", "app_start", "app_stop" ]: return getattr(self.server, key) raise AttributeError(f"Session object has no attribute '{key}'") diff --git a/uiautomator2/version.py b/uiautomator2/version.py index df8b37a3..62f36354 100644 --- a/uiautomator2/version.py +++ b/uiautomator2/version.py @@ -9,7 +9,9 @@ # See ChangeLog for details -__apk_version__ = '2.0.3' +__apk_version__ = '2.0.5' +# 2.0.5 add ToastActivity to show toast or just launch and quit +# 2.0.4 fix floatingWindow crash on Sumsung Android 9 # 2.0.3 use android.app.Service instead of android.app.intentService to simpfy logic # 2.0.2 fix error: AndroidQ Service must be explicit # 2.0.1 fix AndroidQ support