Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
## 项目背景

大约在2017年的时候,我在做Android自动化相关的工作,当时的脚本是用的Python写的,所以去网上找了下相关的开源项目。

刚好找到了 https://github.com/xiaocong/uiautomator
原理是在手机上运行了一个http rpc服务,将uiautomator中的功能开放出来,然后再将这些http接口封装成Python库。这个库写的实在是太好了,爱不释手。
但是这个项目很久也没更新了,也联系不上作者,于是我就fork了一个版本
为了方便做区分我们就在后面加了个2,从uiautomator变成了uiautomator2

- [openatx/uiautomator2](https://github.com/openatx/uiautomator2)
- [openatx/android-uiautomator-server](https://github.com/openatx/android-uiautomator-server)

增加了各种各样的代码,对其中的bug做了修复。

期间也衍生出来的很多其他项目

- 自动化工具 https://github.com/NeteaseGame/ATX 废弃
- 设备管理平台(也支持iOS) [atxserver2](https://github.com/openatx/atxserver2) 废弃
- 纯Python的ADB客户端 https://github.com/openatx/adbutils 这个还健康的存活着
- https://github.com/openatx/weditor 不维护了,不过有开发了一个新的。 https://uiauto.dev
- [uiauto.dev](https://uiauto.dev) 用于查看UI层级结构,类似于uiautomatorviewer(用于替代之前写的weditor),用于查看UI层级结构

1,442 changes: 655 additions & 787 deletions README.md

Large diffs are not rendered by default.

947 changes: 351 additions & 596 deletions README_CN.md

Large diffs are not rendered by default.

177 changes: 2 additions & 175 deletions uiautomator2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@
from retry import retry
from PIL import Image

from uiautomator2.core import BasicUiautomatorServer

from uiautomator2 import xpath
from uiautomator2._proto import HTTP_TIMEOUT, SCROLL_STEPS, Direction
from uiautomator2._selector import Selector, UiObject
Expand All @@ -32,7 +30,7 @@
from uiautomator2.utils import image_convert, list2cmdline, deprecated
from uiautomator2.watcher import WatchContext, Watcher
from uiautomator2.abstract import AbstractShell, AbstractUiautomatorServer, ShellResponse

from uiautomator2.base import _BaseClient

WAIT_FOR_DEVICE_TIMEOUT = int(os.getenv("WAIT_FOR_DEVICE_TIMEOUT", 20))

Expand All @@ -48,177 +46,6 @@ def enable_pretty_logging(level=logging.DEBUG):

logger.setLevel(level)


class _BaseClient(BasicUiautomatorServer, AbstractUiautomatorServer, AbstractShell):
"""
提供最基础的控制类,这个类暂时先不公开吧
"""

def __init__(self, serial: Union[str, adbutils.AdbDevice] = None):
"""
Args:
serial: device serialno
"""
if isinstance(serial, adbutils.AdbDevice):
self._serial = serial.serial
self._dev = serial
else:
self._serial = serial
self._dev = self._wait_for_device()
self._debug = False
BasicUiautomatorServer.__init__(self, self._dev)

def _wait_for_device(self, timeout=10) -> adbutils.AdbDevice:
"""
wait for device came online, if device is remote, reconnect every 1s

Returns:
adbutils.AdbDevice

Raises:
ConnectError
"""
for d in adbutils.adb.device_list():
if d.serial == self._serial:
return d

_RE_remote_adb = re.compile(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$")
_is_remote = _RE_remote_adb.match(self._serial) is not None

adb = adbutils.adb
deadline = time.time() + timeout
while time.time() < deadline:
title = "device reconnecting" if _is_remote else "wait-for-device"
logger.debug("%s, time left(%.1fs)", title, deadline - time.time())
if _is_remote:
try:
adb.disconnect(self._serial)
adb.connect(self._serial, timeout=1)
except (adbutils.AdbError, adbutils.AdbTimeout) as e:
logger.debug("adb reconnect error: %s", str(e))
time.sleep(1.0)
continue
try:
adb.wait_for(self._serial, timeout=1)
except (adbutils.AdbError, adbutils.AdbTimeout):
continue
return adb.device(self._serial)
raise ConnectError(f"device {self._serial} not online")

@property
def adb_device(self) -> adbutils.AdbDevice:
return self._dev

@cached_property
def settings(self) -> Settings:
return Settings(self)

def sleep(self, seconds: float):
""" same as time.sleep """
time.sleep(seconds)

def shell(self, cmdargs: Union[str, List[str]], timeout=60) -> ShellResponse:
"""
Run shell command on device

Args:
cmdargs: str or list, example: "ls -l" or ["ls", "-l"]
timeout: seconds of command run, works on when stream is False

Returns:
ShellResponse

Raises:
AdbShellError
"""
try:
if self.debug:
print("shell:", list2cmdline(cmdargs))
logger.debug("shell: %s", list2cmdline(cmdargs))
ret = self._dev.shell2(cmdargs, timeout=timeout)
return ShellResponse(ret.output, ret.returncode)
except adbutils.AdbError as e:
raise AdbShellError(e)

@property
def info(self) -> Dict[str, Any]:
return self.jsonrpc.deviceInfo(http_timeout=10)

@property
def device_info(self) -> Dict[str, Any]:
serial = self._dev.getprop("ro.serialno")
sdk = self._dev.getprop("ro.build.version.sdk")
version = self._dev.getprop("ro.build.version.release")
brand = self._dev.getprop("ro.product.brand")
model = self._dev.getprop("ro.product.model")
arch = self._dev.getprop("ro.product.cpu.abi")
return {
"serial": serial,
"sdk": int(sdk) if sdk.isdigit() else None,
"brand": brand,
"model": model,
"arch": arch,
"version": int(version) if version.isdigit() else None,
}

@property
def wlan_ip(self) -> Optional[str]:
try:
return self._dev.wlan_ip()
except adbutils.AdbError:
return None

@property
def jsonrpc(self):
class JSONRpcWrapper():
def __init__(self, server: "Device"):
self.server = server
self.method = None

def __getattr__(self, method):
self.method = method # jsonrpc function name
return self

def __call__(self, *args, **kwargs):
http_timeout = kwargs.pop('http_timeout', HTTP_TIMEOUT)
params = args if args else kwargs
return self.server.jsonrpc_call(self.method, params, http_timeout)

return JSONRpcWrapper(self)

def reset_uiautomator(self):
"""
restart uiautomator service

Orders:
- stop uiautomator keeper
- am force-stop com.github.uiautomator
- start uiautomator keeper(am instrument -w ...)
- wait until uiautomator service is ready
"""
self.stop_uiautomator()
self.start_uiautomator()

def push(self, src, dst: str, mode=0o644):
"""
Push file into device

Args:
src (path or fileobj): source file
dst (str): destination can be folder or file path
mode (int): file mode
"""
self._dev.sync.push(src, dst, mode=mode)

def pull(self, src: str, dst: str):
"""
Pull file from device to local
"""
try:
self._dev.sync.pull(src, dst, exist_ok=True)
except TypeError:
self._dev.sync.pull(src, dst)

class _Device(_BaseClient):
__orientation = ( # device orientation
(0, "natural", "n", 0), (1, "left", "l", 90),
Expand Down Expand Up @@ -303,7 +130,7 @@ def _do_dump_hierarchy(self, compressed=False, max_depth=None) -> str:
raise HierarchyEmptyError("dump hierarchy is empty with no children")
return content

def implicitly_wait(self, seconds: float = None) -> float:
def implicitly_wait(self, seconds: Optional[float] = None) -> float:
"""set default wait timeout
Args:
seconds(float): to wait element show up
Expand Down
1 change: 1 addition & 0 deletions uiautomator2/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
d = u2.connect(dev)
logger.debug("install apk to %s", d.serial)
d._setup_jar()
d._setup_ime()

Check warning on line 32 in uiautomator2/__main__.py

View check run for this annotation

Codecov / codecov/patch

uiautomator2/__main__.py#L32

Added line #L32 was not covered by tests


def cmd_purge(args):
Expand Down
30 changes: 27 additions & 3 deletions uiautomator2/_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from typing import Dict, List, Optional, Union
import warnings

import adbutils
from retry import retry

from uiautomator2.abstract import AbstractShell
Expand Down Expand Up @@ -55,18 +56,27 @@
return
# prepare ime
if self.__ime_id not in self.__get_ime_list():
self.__setup_ime()
self._setup_ime()

Check warning on line 59 in uiautomator2/_input.py

View check run for this annotation

Codecov / codecov/patch

uiautomator2/_input.py#L59

Added line #L59 was not covered by tests
assert self.__ime_id in self.__get_ime_list()

self.shell(['ime', 'enable', self.__ime_id])
self.shell(['ime', 'set', self.__ime_id])
self.shell(['settings', 'put', 'secure', 'default_input_method', self.__ime_id])
self._wait_ime_ready()

Check warning on line 65 in uiautomator2/_input.py

View check run for this annotation

Codecov / codecov/patch

uiautomator2/_input.py#L65

Added line #L65 was not covered by tests

def __setup_ime(self):
def is_input_ime_installed(self) -> bool:
return self.__ime_id in self.__get_ime_list()

Check warning on line 68 in uiautomator2/_input.py

View check run for this annotation

Codecov / codecov/patch

uiautomator2/_input.py#L68

Added line #L68 was not covered by tests

def _setup_ime(self):
logger.debug("installing AdbKeyboard ime")
assets_dir = Path(__file__).parent / "assets"
ime_apk_path = assets_dir / 'app-uiautomator.apk'
self.adb_device.install(str(ime_apk_path), nolaunch=True, uninstall=True)
try:
self.adb_device.install(str(ime_apk_path), nolaunch=True, uninstall=True)

Check warning on line 75 in uiautomator2/_input.py

View check run for this annotation

Codecov / codecov/patch

uiautomator2/_input.py#L74-L75

Added lines #L74 - L75 were not covered by tests
except adbutils.AdbError as e:
self.adb_device.uninstall(self.__ime_id.split('/')[0])
self.adb_device.install(str(ime_apk_path), nolaunch=True, uninstall=True)

# wait for ime registered
for _ in range(10):
if self.__ime_id in self.__get_ime_list():
Expand Down Expand Up @@ -167,9 +177,23 @@
# shown = "mInputShown=true" in dim
# return (method_id, shown)

def _wait_ime_ready(self, timeout: float = 5.0) -> bool:
""" Wait for input method is ready """
deadline = time.time() + timeout

Check warning on line 182 in uiautomator2/_input.py

View check run for this annotation

Codecov / codecov/patch

uiautomator2/_input.py#L182

Added line #L182 was not covered by tests
while time.time() < deadline:
if self.current_ime() == self.__ime_id:
return True
time.sleep(0.1)
return False

Check warning on line 187 in uiautomator2/_input.py

View check run for this annotation

Codecov / codecov/patch

uiautomator2/_input.py#L185-L187

Added lines #L185 - L187 were not covered by tests

def __get_ime_list(self) -> List[str]:
ret = self.shell(['ime', 'list', '-s', '-a'])
return ret.output.strip().splitlines(keepends=False)

def hide_keyboard(self):
""" Hide keyboard """
self.set_input_ime()
self._must_broadcast('ADB_KEYBOARD_HIDE')

Check warning on line 196 in uiautomator2/_input.py

View check run for this annotation

Codecov / codecov/patch

uiautomator2/_input.py#L195-L196

Added lines #L195 - L196 were not covered by tests

@deprecated(reason="use set_input_ime instead")
def set_fastinput_ime(self, enable: bool = True):
Expand Down
7 changes: 1 addition & 6 deletions uiautomator2/_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,10 @@ def click(self, timeout=None, offset=None):
Raises:
UiObjectNotFoundError
"""
# self.jsonrpc.click(self.selector)
self.must_wait(timeout=timeout)
x, y = self.center(offset=offset)
# ext.htmlreport need to comment bellow code
# if info['clickable']:
# return self.jsonrpc.click(self.selector)
self.session.click(x, y)
# delay = self.session.click_post_delay
# if delay:
# time.sleep(delay)

def bounds(self) -> Tuple[int, int, int, int]:
"""
Expand Down
Loading
Loading