Skip to content
Open
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
6 changes: 3 additions & 3 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ Always reference these instructions first and fallback to search or bash command
- `poetry run uiautomator2 version` -- Check CLI functionality. Should output version number.

### Asset Synchronization (Optional)
- `make sync` -- Download required APK and JAR assets. FAILS due to network restrictions in sandboxed environment. This is EXPECTED and not required for development.
- Asset sync downloads Android APK and u2.jar from external hosts which are blocked in this environment.
- `make sync` -- Download required u2.apk asset. FAILS due to network restrictions in sandboxed environment. This is EXPECTED and not required for development.
- Asset sync downloads u2.apk from external hosts which are blocked in this environment.

### Commands That Will Fail (Expected)
- `make test` -- Mobile tests require Android device via ADB. Will fail with "Can't find any android device/emulator" - this is EXPECTED.
Expand Down Expand Up @@ -55,7 +55,7 @@ After making changes, ALWAYS run this validation sequence:

### Test Directories
- `tests/` -- Unit tests (25 tests, no device required)
- `mobile_tests/` -- Integration tests (30 tests, require Android device)
- `e2e_tests/` -- Integration/e2e tests (30 tests, require Android device)
- `demo_tests/` -- Example/demo tests

### Build Configuration
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ format:
poetry run isort . -m HANGING_INDENT -l 120

test:
poetry run pytest -v mobile_tests/
poetry run pytest -v e2e_tests/

covtest:
poetry run coverage run -m pytest -v demo_tests tests
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ This framework mainly consists of two parts:

Simply put, it exposes Android automation capabilities to Python through HTTP interfaces. This design makes Python-side code writing simpler and more intuitive.

> The Device Side run with a jar file which is also open source. see <https://github.com/openatx/android-uiautomator-server-jar>
> The Device Side runs with an APK which is also open source. see <https://github.com/openatx/android-uiautomator-server-jar>

# Dependencies
- Android version 4.4+
- Android version 5.0+
- Python 3.8+

# Installation
Expand Down Expand Up @@ -286,7 +286,7 @@ Set or get clipboard content.
d.set_clipboard('hello-world', 'label')

# Get clipboard
# Depends on input method (com.github.uiautomator/.AdbKeyboard)
# Depends on input method (com.github.uiautomator2/.AdbKeyboard)
d.set_input_ime()
print(d.clipboard)
```
Expand Down
4 changes: 2 additions & 2 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
- Discord: <https://discord.gg/PbJhnZJKDd>

# 依赖
- Android版本 4.4+
- Android版本 5.0+
- Python 3.8+

# 安装
Expand Down Expand Up @@ -281,7 +281,7 @@ print(d.wlan_ip)
d.set_clipboard('hello-world', 'label')

# 获取剪贴板
# 依赖输入法(com.github.uiautomator/.AdbKeyboard)
# 依赖输入法(com.github.uiautomator2/.AdbKeyboard)
d.set_input_ime()
print(d.clipboard)
```
Expand Down
2 changes: 1 addition & 1 deletion demo_tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def test_uiautomator_keeper(d: u2.Device):
kill_app_process(d)
d.sleep(.2)
assert get_app_process_pid(d) is None
d.shell('rm /data/local/tmp/u2.jar')
d.shell('rm /data/local/tmp/u2.apk')

d.start_uiautomator()
assert get_app_process_pid(d) > 0
Expand Down
4 changes: 2 additions & 2 deletions docs/2to3.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,8 @@ print(d.device_info)
- 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"
- 2.x return (ime_method_name, bool), e.g. ("com.github.uiautomator2/.AdbKeyboard", True)
- 3.x return ime_method_name, e.g. "com.github.uiautomator2/.AdbKeyboard"

### toast
- 2.x d.toast.get_message(5.0, default="")
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 3 additions & 1 deletion mobile_tests/test_settings.py → e2e_tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ def test_wait_timeout(d: u2.Device):


def test_operation_delay(dev: u2.Session):
x, y = dev(text="App").center()
obj = dev(text="App")
assert obj.wait(timeout=10)
x, y = obj.center()

# 测试前延迟
start = time.time()
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes
22 changes: 11 additions & 11 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,16 @@ def test_toybox_md5sum_success(self, mock_server):

# Mock the shell command to return toybox md5sum output
# Format: "md5hash filename"
mock_dev.shell.return_value = f"{local_md5} /data/local/tmp/u2.jar"
mock_dev.shell.return_value = f"{local_md5} /data/local/tmp/u2.apk"

# Mock the file read to return our test content
with patch("builtins.open", mock_open(read_data=test_content)):
result = server._check_device_file_hash("test.jar", "/data/local/tmp/u2.jar")
result = server._check_device_file_hash("test.apk", "/data/local/tmp/u2.apk")

# Verify the result is True (hash matches)
assert result is True
# Verify toybox md5sum was called
mock_dev.shell.assert_called_once_with(["toybox", "md5sum", "/data/local/tmp/u2.jar"])
mock_dev.shell.assert_called_once_with(["toybox", "md5sum", "/data/local/tmp/u2.apk"])

def test_toybox_not_found_fallback_to_md5(self, mock_server):
"""Test fallback to md5 command when toybox is not found"""
Expand All @@ -64,19 +64,19 @@ def test_toybox_not_found_fallback_to_md5(self, mock_server):
# Second call: md5 command output (format: "MD5 (filename) = md5hash")
mock_dev.shell.side_effect = [
"toybox: not found",
f"MD5 (/data/local/tmp/u2.jar) = {local_md5}"
f"MD5 (/data/local/tmp/u2.apk) = {local_md5}"
]

# Mock the file read to return our test content
with patch("builtins.open", mock_open(read_data=test_content)):
result = server._check_device_file_hash("test.jar", "/data/local/tmp/u2.jar")
result = server._check_device_file_hash("test.apk", "/data/local/tmp/u2.apk")

# Verify the result is True (hash matches)
assert result is True
# Verify both commands were called
assert mock_dev.shell.call_count == 2
assert mock_dev.shell.call_args_list[0][0][0] == ["toybox", "md5sum", "/data/local/tmp/u2.jar"]
assert mock_dev.shell.call_args_list[1][0][0] == ["md5", "/data/local/tmp/u2.jar"]
assert mock_dev.shell.call_args_list[0][0][0] == ["toybox", "md5sum", "/data/local/tmp/u2.apk"]
assert mock_dev.shell.call_args_list[1][0][0] == ["md5", "/data/local/tmp/u2.apk"]

def test_hash_mismatch(self, mock_server):
"""Test when the hash doesn't match"""
Expand All @@ -87,11 +87,11 @@ def test_hash_mismatch(self, mock_server):
different_md5 = hashlib.md5(b"different content").hexdigest()

# Mock the shell command to return a different hash
mock_dev.shell.return_value = f"{different_md5} /data/local/tmp/u2.jar"
mock_dev.shell.return_value = f"{different_md5} /data/local/tmp/u2.apk"

# Mock the file read to return our test content
with patch("builtins.open", mock_open(read_data=test_content)):
result = server._check_device_file_hash("test.jar", "/data/local/tmp/u2.jar")
result = server._check_device_file_hash("test.apk", "/data/local/tmp/u2.apk")

# Verify the result is False (hash doesn't match)
assert result is False
Expand All @@ -106,12 +106,12 @@ def test_md5_command_also_fails(self, mock_server):
# Mock the shell command to return errors for both commands
mock_dev.shell.side_effect = [
"toybox: not found",
"md5: /data/local/tmp/u2.jar: No such file or directory"
"md5: /data/local/tmp/u2.apk: No such file or directory"
]

# Mock the file read to return our test content
with patch("builtins.open", mock_open(read_data=test_content)):
result = server._check_device_file_hash("test.jar", "/data/local/tmp/u2.jar")
result = server._check_device_file_hash("test.apk", "/data/local/tmp/u2.apk")

# Verify the result is False (file not found on device)
assert result is False
Expand Down
2 changes: 1 addition & 1 deletion tests/test_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def __init__(self):
self._jsonrpc = Mock()
self._broadcast_calls = []
self._shell_calls = []
self._current_ime = 'com.github.uiautomator/.AdbKeyboard'
self._current_ime = 'com.github.uiautomator2/.AdbKeyboard'

@property
def adb_device(self):
Expand Down
4 changes: 2 additions & 2 deletions uiautomator2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,7 @@ def app_stop_all(self, excludes=[]):
Returns:
a list of killed apps
"""
our_apps = ['com.github.uiautomator', 'com.github.uiautomator.test']
our_apps = ['com.github.uiautomator', 'com.github.uiautomator.test', 'com.github.uiautomator2']
kill_pkgs = set(self.app_list_running()).difference(our_apps +
excludes)
for pkg_name in kill_pkgs:
Expand All @@ -692,7 +692,7 @@ def app_uninstall(self, package_name: str) -> bool:

def app_uninstall_all(self, excludes=[], verbose=False):
""" Uninstall all apps """
our_apps = ['com.github.uiautomator', 'com.github.uiautomator.test']
our_apps = ['com.github.uiautomator', 'com.github.uiautomator.test', 'com.github.uiautomator2']
output, _ = self.shell(['pm', 'list', 'packages', '-3'])
pkgs = re.findall(r'package:([^\s]+)', output)
pkgs = set(pkgs).difference(our_apps + excludes)
Expand Down
24 changes: 11 additions & 13 deletions uiautomator2/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,13 @@ def cmd_init(args):
if serial:
d = u2.connect(serial, port=args.port)
logger.debug("install apk to %s", d.serial)
d._setup_jar()
d._setup_apk()
d._setup_ime()
else:
for dev in adbutils.adb.iter_device():
d = u2.connect(dev, port=args.port)
logger.debug("install apk to %s", d.serial)
d._setup_jar()
d._setup_apk()
d._setup_ime()


Expand All @@ -52,29 +53,26 @@ def cmd_purge(args):
dev = adbutils.adb.device(args.serial)
dev.uninstall("com.github.uiautomator")
dev.uninstall("com.github.uiautomator.test")
dev.uninstall("com.github.uiautomator2")
dev.shell(["/data/local/tmp/atx-agent", "server", "--stop"])
dev.shell(["rm", "/data/local/tmp/atx-agent"])
logger.info("atx-agent stopped and removed")
dev.shell(["rm", "/data/local/tmp/minicap"])
dev.shell(["rm", "/data/local/tmp/minicap.so"])
dev.shell(["rm", "/data/local/tmp/minitouch"])
logger.info("minicap, minitouch removed")
dev.shell(["pm", "uninstall", "com.github.uiautomator"])
dev.shell(["pm", "uninstall", "com.github.uiautomator.test"])
logger.info("com.github.uiautomator uninstalled, all done !!!")
dev.shell(["rm", "/data/local/tmp/u2.apk"])
dev.shell(["rm", "/data/local/tmp/u2.jar"])
logger.info("minicap, minitouch, u2.apk removed")
logger.info("com.github.uiautomator2 uninstalled, all done !!!")


def cmd_copy_assets(args):
target_dir = pathlib.Path("assets")
target_dir.mkdir(exist_ok=True)
with with_package_resource("assets/u2.jar") as jar_path:
target_path = target_dir / "u2.jar"
shutil.copy2(jar_path, target_path)
print("Copied u2.jar to", target_path)
with with_package_resource("assets/app-uiautomator.apk") as apk_path:
target_path = target_dir / "app-uiautomator.apk"
with with_package_resource("assets/u2.apk") as apk_path:
target_path = target_dir / "u2.apk"
shutil.copy2(apk_path, target_path)
print("Copied app-uiautomator.apk to", target_path)
print("Copied u2.apk to", target_path)

def cmd_screenshot(args):
d = u2.connect(args.serial, port=args.port)
Expand Down
21 changes: 13 additions & 8 deletions uiautomator2/_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,22 @@ class InputMethodMixIn(AbstractShell):

@property
def __ime_id(self) -> str:
return 'com.github.uiautomator/.AdbKeyboard'
return 'com.github.uiautomator2/.AdbKeyboard'

def set_input_ime(self, enable: bool = True):
""" Enable of Disable InputIME """
if not enable:
self.shell(['ime', 'disable', self.__ime_id])
return
ime_list = self.__get_ime_list()
if self.__ime_id not in ime_list:
# default_input_method may keep a stale value after the apk is
# uninstalled or upgraded, so always verify the ime is installed.
self._setup_ime()
Comment on lines +54 to +58
ime_list = self.__get_ime_list()
if self.current_ime() == self.__ime_id:
return
# prepare ime
if self.__ime_id not in self.__get_ime_list():
self._setup_ime()
assert self.__ime_id in self.__get_ime_list()
assert self.__ime_id in ime_list

self.shell(['ime', 'enable', self.__ime_id])
self.shell(['ime', 'set', self.__ime_id])
Expand All @@ -68,10 +71,12 @@ def is_input_ime_installed(self) -> bool:

def _setup_ime(self):
logger.debug("installing AdbKeyboard ime")
with with_package_resource("assets/app-uiautomator.apk") as ime_apk_path:
# The u2.apk bundles the AdbKeyboard input method, it also runs the
# jsonrpc server through app_process after being pushed to the device.
with with_package_resource("assets/u2.apk") as ime_apk_path:
try:
self.adb_device.install(str(ime_apk_path), nolaunch=True, uninstall=True)
except adbutils.AdbError as e:
except adbutils.AdbError:
self.adb_device.uninstall(self.__ime_id.split('/')[0])
self.adb_device.install(str(ime_apk_path), nolaunch=True, uninstall=True)

Expand Down Expand Up @@ -159,7 +164,7 @@ def current_ime(self) -> str:
ime_method

Example output:
"com.github.uiautomator/.FastInputIME"
"com.github.uiautomator2/.AdbKeyboard"
"""
return self.shell(['settings', 'get', 'secure', 'default_input_method']).output.strip()
# _INPUT_METHOD_RE = re.compile(r'mCurMethodId=([-_./\w]+)')
Expand Down
34 changes: 9 additions & 25 deletions uiautomator2/assets/sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@

set -e

APK_VERSION=$(cat ../version.py| grep apk_version | awk '{print $NF}')
APK_VERSION=${APK_VERSION//[\"\']}
JAR_VERSION="0.4.1"
U2_VERSION=$(cat ../version.py| grep __u2_version__ | awk '{print $NF}')
U2_VERSION=${U2_VERSION//[\"\']}

cd "$(dirname $0)"

Expand All @@ -17,32 +16,17 @@ function download() {
curl -L "$URL" --output "$OUTPUT"
}

function download_apk(){
local VERSION=$1
local NAME=$2
local URL="https://github.com/openatx/android-uiautomator-server/releases/download/$VERSION/$NAME"
download "$URL" "$NAME"
unzip -tq "$NAME"
function download_apk() {
local URL="https://github.com/openatx/android-uiautomator-server-jar/releases/download/$U2_VERSION/u2.apk"
download "$URL" "u2.apk"
unzip -tq "u2.apk"
}

function download_jar() {
local URL="https://github.com/openatx/android-uiautomator-server-jar/releases/download/$JAR_VERSION/u2.jar"
download "$URL" "u2.jar"
if test -s u2.jar; then
echo "Download Jar sucessfully"
else
echo "Download Jar failed"
exit 1
fi
}

echo "APK_VERSION: $APK_VERSION"
echo "U2_VERSION: $U2_VERSION"

download_jar
download_apk "$APK_VERSION" "app-uiautomator.apk"
download_apk
cat > version.json <<EOF
{
"com.github.uiautomator": "$APK_VERSION",
"u2.jar": "$JAR_VERSION"
"u2.apk": "$U2_VERSION"
}
EOF
Loading
Loading