diff --git a/README.md b/README.md index a76742e..917479e 100644 --- a/README.md +++ b/README.md @@ -1270,22 +1270,23 @@ Refs: [Google uiautomator Configurator](https://developer.android.com/reference/ 这种方法通常用于不知道控件的情况下的输入。第一步需要切换输入法,然后发送adb广播命令,具体使用方法如下 ```python -d.set_fastinput_ime(True) # 切换成FastInputIME输入法 d.send_keys("你好123abcEFG") # adb广播输入 -d.clear_text() # 清除输入框所有内容(Require android-uiautomator.apk version >= 1.0.7) -d.set_fastinput_ime(False) # 切换成正常的输入法 -d.send_action("search") # 模拟输入法的搜索 +d.send_keys("你好123abcEFG", clear=True) # adb广播输入 + +d.clear_text() # 清除输入框所有内容 + +d.send_action() # 根据输入框的需求,自动执行回车、搜索等指令, Added in version 3.1 +# 也可以指定发送的输入法action, eg: d.send_action("search") 支持 go, search, send, next, done, previous ``` -**send_action** 说明 -该函数可以使用的参数有 `go search send next done previous` -_什么时候该使用这个函数呢?_ +```python +print(d.current_ime()) # 获取当前输入法ID + +``` -有些时候在EditText中输入完内容之后,调用`press("search")` or `press("enter")`发现并没有什么反应。 -这个时候就需要`send_action`函数了,这里用到了只有输入法才能用的[IME_ACTION_CODE](https://developer.android.com/reference/android/view/inputmethod/EditorInfo)。 -`send_action`先broadcast命令发送给输入法操作`IME_ACTION_CODE`,由输入法完成后续跟EditText的通信。(原理我不太清楚,有了解的,提issue告诉我) +> 更多参考: [IME_ACTION_CODE](https://developer.android.com/reference/android/view/inputmethod/EditorInfo) ### Toast (2.2版本之后有添加回来) Show Toast (好像有点bug) diff --git a/docs/2to3.md b/docs/2to3.md index 41971cc..45ce326 100644 --- a/docs/2to3.md +++ b/docs/2to3.md @@ -65,6 +65,10 @@ XPath (d.xpath) methods - remove when, run_watchers, watch_background, watch_stop, watch_clear, sleep_watch - remove position method, usage like d.xpath(...).position(0.2, 0.2) +InputMethod +- deprecated wait_fastinput_ime +- deprecated set_fastinput_ime use set_input_ime instead + ### Command remove - Remove "uiautomator2 healthcheck" - Remove "uiautomator2 identify" @@ -169,6 +173,10 @@ print(d.device_info) 'version': 12} ``` - ### app_current +### app_current - 2.x raise `OSError` if couldn't get focused app - 3.x raise `DeviceError` if couldn't get focused app + +### current_ime +- 2.x return (ime_method_name, bool), e.g. ("com.github.uiautomator/.FastInputIME", True) +- 3.x return ime_method_name, e.g. "com.github.uiautomator/.FastInputIME" \ No newline at end of file diff --git a/uiautomator2/__init__.py b/uiautomator2/__init__.py index dd64d81..0548081 100644 --- a/uiautomator2/__init__.py +++ b/uiautomator2/__init__.py @@ -24,6 +24,7 @@ from uiautomator2 import xpath from uiautomator2._proto import HTTP_TIMEOUT, SCROLL_STEPS, Direction from uiautomator2._selector import Selector, UiObject +from uiautomator2._input import InputMethodMixIn from uiautomator2.exceptions import AdbShellError, BaseException, ConnectError, DeviceError, HierarchyEmptyError, SessionBrokenError from uiautomator2.settings import Settings from uiautomator2.swipe import SwipeExt @@ -911,119 +912,6 @@ def unlock(self): self.swipe(0.1, 0.9, 0.9, 0.1) -class _InputMethodMixIn(AbstractShell): - def set_fastinput_ime(self, enable: bool = True): - """ Enable of Disable FastInputIME """ - fast_ime = 'com.github.uiautomator/.FastInputIME' - if enable: - self.shell(['ime', 'enable', fast_ime]) - self.shell(['ime', 'set', fast_ime]) - else: - self.shell(['ime', 'disable', fast_ime]) - - def send_keys(self, text: str, clear: bool = False): - """ - Args: - text (str): text to set - clear (bool): clear before set text - - Raises: - EnvironmentError - """ - try: - self.wait_fastinput_ime() - btext = text.encode('utf-8') - base64text = base64.b64encode(btext).decode() - cmd = "ADB_SET_TEXT" if clear else "ADB_INPUT_TEXT" - self.shell( - ['am', 'broadcast', '-a', cmd, '--es', 'text', base64text]) - return True - except EnvironmentError: - warnings.warn( - "set FastInputIME failed. use \"d(focused=True).set_text instead\"", - Warning) - return self(focused=True).set_text(text) - # warnings.warn("set FastInputIME failed. use \"adb shell input text\" instead", Warning) - # self.shell(["input", "text", text.replace(" ", "%s")]) - - def send_action(self, code): - """ - Simulate input method edito code - - Args: - code (str or int): input method editor code - - Examples: - send_action("search"), send_action(3) - - Refs: - https://developer.android.com/reference/android/view/inputmethod/EditorInfo - """ - self.wait_fastinput_ime() - __alias = { - "go": 2, - "search": 3, - "send": 4, - "next": 5, - "done": 6, - "previous": 7, - } - if isinstance(code, str): - code = __alias.get(code, code) - self.shell([ - 'am', 'broadcast', '-a', 'ADB_EDITOR_CODE', '--ei', 'code', - str(code) - ]) - - def clear_text(self): - """ clear text - Raises: - EnvironmentError - """ - try: - self.wait_fastinput_ime() - self.shell(['am', 'broadcast', '-a', 'ADB_CLEAR_TEXT']) - except EnvironmentError: - # for Android simulator - self(focused=True).clear_text() - - def wait_fastinput_ime(self, timeout=5.0): - """ wait FastInputIME is ready - Args: - timeout(float): maxium wait time - - Raises: - EnvironmentError - """ - # TODO: 模拟器待兼容 eg. Genymotion, 海马玩, Mumu - - deadline = time.time() + timeout - while time.time() < deadline: - ime_id, shown = self.current_ime() - if ime_id != "com.github.uiautomator/.FastInputIME": - self.set_fastinput_ime(True) - time.sleep(0.5) - continue - if shown: - return True - time.sleep(0.2) - raise EnvironmentError("FastInputIME started failed") - - def current_ime(self): - """ Current input method - Returns: - (method_id(str), shown(bool) - - Example output: - ("com.github.uiautomator/.FastInputIME", True) - """ - _INPUT_METHOD_RE = re.compile(r'mCurMethodId=([-_./\w]+)') - dim, _ = self.shell(['dumpsys', 'input_method']) - m = _INPUT_METHOD_RE.search(dim) - method_id = None if not m else m.group(1) - shown = "mInputShown=true" in dim - return (method_id, shown) - class _PluginMixIn: def watch_context(self, autostart: bool = True, builtin: bool = False) -> WatchContext: @@ -1054,32 +942,11 @@ def screenrecord(self): def swipe_ext(self) -> SwipeExt: return SwipeExt(self) -class Device(_Device, _AppMixIn, _PluginMixIn, _InputMethodMixIn, _DeprecatedMixIn): + +class Device(_Device, _AppMixIn, _PluginMixIn, InputMethodMixIn, _DeprecatedMixIn): """ Device object """ + pass - @property - def info(self) -> Dict[str, Any]: - """ return device info, make sure currentPackageName is set - - Return example: - {'currentPackageName': 'io.appium.android.apis', - 'displayHeight': 720, - 'displayRotation': 3, - 'displaySizeDpX': 780, - 'displaySizeDpY': 360, - 'displayWidth': 1560, - 'productName': 'ELE-AL00', - 'screenOn': True, - 'sdkInt': 29, - 'naturalOrientation': False} - """ - _info = super().info - if _info.get('currentPackageName') is None: - try: - _info['currentPackageName'] = self.app_current().get('package') - except DeviceError: - pass - return _info class Session(Device): """Session keeps watch the app status diff --git a/uiautomator2/_input.py b/uiautomator2/_input.py new file mode 100644 index 0000000..7660055 --- /dev/null +++ b/uiautomator2/_input.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +"""Created on Wed May 22 2024 16:23:56 by codeskyblue +""" + +import base64 +from dataclasses import dataclass +import re +from typing import Dict, Optional, Union +import warnings + +from retry import retry + +from uiautomator2.abstract import AbstractShell +from uiautomator2.exceptions import AdbBroadcastError, DeviceError +from uiautomator2.utils import deprecated + + +@dataclass +class BroadcastResult: + code: Optional[int] + data: Optional[str] + + +BORADCAST_RESULT_OK = -1 +BROADCAST_RESULT_CANCELED = 0 + + + +class InputMethodMixIn(AbstractShell): + @deprecated(reason="use set_input_ime instead") + def set_fastinput_ime(self, enable: bool = True): + return self.set_input_ime(enable) + + def set_input_ime(self, enable: bool = True): + """ Enable of Disable InputIME """ + ime_id = 'com.github.uiautomator/.AdbKeyboard' + if not enable: + self.shell(['ime', 'disable', ime_id]) + return + + if self.current_ime() == ime_id: + return + self.shell(['ime', 'enable', ime_id]) + self.shell(['ime', 'set', ime_id]) + self.shell(['settings', 'put', 'secure', 'default_input_method', ime_id]) + + def _broadcast(self, action: str, extras: Dict[str, str] = {}) -> BroadcastResult: + # requires ATX 2.4.0+ + args = ['am', 'broadcast', '-a', action] + for k, v in extras.items(): + if isinstance(v, int): + args.extend(['--ei', k, str(v)]) + else: + args.extend(['--es', k, v]) + # Example output: result=-1 data="success" + output = self.shell(args).output + m_result = re.search(r'result=(-?\d+)', output) + m_data = re.search(r'data="([^"]+)"', output) + result = int(m_result.group(1)) if m_result else None + data = m_data.group(1) if m_data else None + return BroadcastResult(result, data) + + @retry(AdbBroadcastError, tries=3, delay=1, jitter=0.5) + def _must_broadcast(self, action: str, extras: Dict[str, str] = {}): + result = self._broadcast(action, extras) + if result.code != BORADCAST_RESULT_OK: + raise AdbBroadcastError(f"broadcast {action} failed: {result.data}") + + def send_keys(self, text: str, clear: bool = False): + """ + Args: + text (str): text to set + clear (bool): clear before set text + """ + try: + self.set_input_ime() + btext = text.encode('utf-8') + base64text = base64.b64encode(btext).decode() + cmd = "ADB_KEYBOARD_SET_TEXT" if clear else "ADB_KEYBOARD_INPUT_TEXT" + self._must_broadcast(cmd, {"text": base64text}) + return True + except AdbBroadcastError: + warnings.warn( + "set FastInputIME failed. use \"d(focused=True).set_text instead\"", + Warning) + return self(focused=True).set_text(text) + # warnings.warn("set FastInputIME failed. use \"adb shell input text\" instead", Warning) + # self.shell(["input", "text", text.replace(" ", "%s")]) + + def send_action(self, code: Union[str, int] = None): + """ + Simulate input method edito code + + Args: + code (str or int): input method editor code + + Examples: + send_action("search"), send_action(3) + + Refs: + https://developer.android.com/reference/android/view/inputmethod/EditorInfo + """ + self.set_input_ime(True) + __alias = { + "go": 2, + "search": 3, + "send": 4, + "next": 5, + "done": 6, + "previous": 7, + } + if isinstance(code, str): + code = __alias.get(code, code) + if code: + self._must_broadcast('ADB_KEYBOARD_EDITOR_CODE', {"code": str(code)}) + else: + self._must_broadcast('ADB_KEYBOARD_SMART_ENTER') + + def clear_text(self): + """ clear text + Raises: + EnvironmentError + """ + try: + self.set_input_ime(True) + self._must_broadcast('ADB_KEYBOARD_CLEAR_TEXT') + except AdbBroadcastError: + # for Android simulator + self(focused=True).clear_text() + + @deprecated(reason="use set_input_ime instead") + def wait_fastinput_ime(self, timeout=5.0): + """ wait FastInputIME is ready (Depreacated in version 3.1) + Args: + timeout(float): maxium wait time + + Raises: + EnvironmentError + """ + pass + # TODO: 模拟器待兼容 eg. Genymotion, 海马玩, Mumu + # deadline = time.time() + timeout + # while time.time() < deadline: + # ime_id, shown = self.current_ime() + # if ime_id != "com.github.uiautomator/.FastInputIME": + # self.set_fastinput_ime(True) + # time.sleep(0.5) + # continue + # if shown: + # return True + # time.sleep(0.2) + # raise EnvironmentError("FastInputIME started failed") + + def current_ime(self) -> str: + """ Current input method + Returns: + ime_method + + Example output: + "com.github.uiautomator/.FastInputIME" + """ + return self.shell(['settings', 'get', 'secure', 'default_input_method']).output.strip() + # _INPUT_METHOD_RE = re.compile(r'mCurMethodId=([-_./\w]+)') + # dim, _ = self.shell(['dumpsys', 'input_method']) + # m = _INPUT_METHOD_RE.search(dim) + # method_id = None if not m else m.group(1) + # shown = "mInputShown=true" in dim + # return (method_id, shown) diff --git a/uiautomator2/core.py b/uiautomator2/core.py index 5d7446c..0c9e5a4 100644 --- a/uiautomator2/core.py +++ b/uiautomator2/core.py @@ -205,7 +205,7 @@ def _setup_apks(self): if main_apk_info is None: self._install_apk(main_apk) elif main_apk_info.version_name != __apk_version__: - if "dev" in main_apk_info.version_name or "dirty" in main_apk_info.version_name: + if re.match(r"([\d.]+)\-(\d+)\-\w+", main_apk_info.version_name) or "dirty" in main_apk_info.version_name: logger.debug("skip version check for %s", main_apk_info.version_name) elif is_version_compatiable(__apk_version__, main_apk_info.version_name): logger.debug("apk version compatiable, expect %s, actual %s", __apk_version__, main_apk_info.version_name) diff --git a/uiautomator2/exceptions.py b/uiautomator2/exceptions.py index c0fdfdc..2549d69 100644 --- a/uiautomator2/exceptions.py +++ b/uiautomator2/exceptions.py @@ -11,6 +11,7 @@ class DeviceError(BaseException): class AdbShellError(DeviceError):... class ConnectError(DeviceError):... class HTTPError(DeviceError):... +class AdbBroadcastError(DeviceError):... class UiAutomationError(DeviceError): pass diff --git a/uiautomator2/utils.py b/uiautomator2/utils.py index a0d6130..ea1c07c 100644 --- a/uiautomator2/utils.py +++ b/uiautomator2/utils.py @@ -7,6 +7,7 @@ import threading import typing from typing import Union +import warnings from uiautomator2._proto import Direction from uiautomator2.exceptions import SessionBrokenError, UiObjectNotFoundError @@ -233,8 +234,18 @@ def _parse_version(version: str): if evs[1] == avs[1]: return evs[2] <= avs[2] return False - - + + +def deprecated(reason): + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + warnings.warn(f"Function '{func.__name__}' is deprecated: {reason}", DeprecationWarning, stacklevel=2) + return func(*args, **kwargs) + return wrapper + return decorator + + if __name__ == "__main__": for n in (1, 10000, 10000000, 10000000000): print(n, natualsize(n)) diff --git a/uiautomator2/version.py b/uiautomator2/version.py index b7bd947..ae65c92 100644 --- a/uiautomator2/version.py +++ b/uiautomator2/version.py @@ -6,7 +6,7 @@ # see release note for details -__apk_version__ = '2.3.11' +__apk_version__ = '2.4.0' # old apk version history # 2.3.3 make float windows smaller