Skip to content

optimize list_apps and screenshot #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
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
20 changes: 11 additions & 9 deletions hmdriver2/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,11 @@ def install_app(self, apk_path: str):
def uninstall_app(self, package_name: str):
self.hdc.uninstall(package_name)

def list_apps(self) -> List:
return self.hdc.list_apps()
def list_apps(self, include_system_apps: bool = False) -> List:
return self.hdc.list_apps(include_system_apps)

def app_version(self, bundle_name) -> Dict:
return self.hdc.app_version(bundle_name)

def has_app(self, package_name: str) -> bool:
return self.hdc.has_app(package_name)
Expand Down Expand Up @@ -342,22 +345,20 @@ def push_file(self, lpath: str, rpath: str):
"""
self.hdc.send_file(lpath, rpath)

def screenshot(self, path: str) -> str:
def screenshot(self, path: str, method: str = "snapshot_display") -> str:
"""
Take a screenshot of the device display.

Args:
path (str): The local path to save the screenshot.
method (str): The screenshot method to use. Options are:
- "snapshot_display" (default, recommended for better performance)
- "screenCap" (alternative method, higher quality but slower).

Returns:
str: The path where the screenshot is saved.
"""
_uuid = uuid.uuid4().hex
_tmp_path = f"/data/local/tmp/_tmp_{_uuid}.jpeg"
self.shell(f"snapshot_display -f {_tmp_path}")
self.pull_file(_tmp_path, path)
self.shell(f"rm -rf {_tmp_path}") # remove local path
return path
return self.hdc.screenshot(path, method=method)

def shell(self, cmd) -> CommandResult:
return self.hdc.shell(cmd)
Expand Down Expand Up @@ -486,3 +487,4 @@ def xpath(self):
"""
from ._xpath import _XPath
return _XPath(self)

100 changes: 90 additions & 10 deletions hmdriver2/hdc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import re
import os
import subprocess
from typing import Union, List, Dict, Tuple
from typing import Union, List, Dict, Tuple, Optional

from . import logger
from .utils import FreePort
Expand Down Expand Up @@ -139,10 +139,64 @@ def install(self, apkpath: str):
raise HdcError("HDC install error", result.error)
return result

def list_apps(self) -> List[str]:
result = self.shell("bm dump -a")
def list_apps(self, include_system_apps: bool = False) -> List[str]:
"""
List installed applications on the device. (Lazy loading, default: third-party apps)

Args:
include_system_apps (bool): If True, include system apps in the list.
If False, only list third-party apps.

Returns:
List[str]: A list of application package names.

Note:
- When include_system_apps is False, the list typically contains around 50 third-party apps.
- When include_system_apps is True, the list typically contains around 200 apps in total.
"""
# Construct the shell command based on the include_system_apps flag
if include_system_apps:
command = "bm dump -a"
else:
command = "bm dump -a | grep -v 'com.huawei'"

# Execute the shell command
result = self.shell(command)
raw = result.output.split('\n')
return [item.strip() for item in raw]

# Filter out strings starting with 'ID:' and empty strings
return [item.strip() for item in raw if item.strip() and not re.match(r'^ID:', item.strip())]

def app_version(self, bundlename: str) -> Dict[str, Optional[str]]:
"""
Get the version information of an app installed on the device.

Args:
bundlename (str): The bundle name of the app.

Returns:
dict: A dictionary containing the version information:
- "versionName": The version name of the app.
- "versionCode": The version code of the app.
"""
result = _execute_command(f"{self.hdc_prefix} -t {self.serial} shell bm dump -n {bundlename} | grep '\"versionCode\":\\|versionName\"'")

matches = re.findall(r'"versionCode":\s*(\d+),\s*"versionName":\s*"([^"]*)"', result.output)
if not matches:
return dict(
version_name='',
version_code=''
)

# Select the last match
version_code, version_name = matches[-1]
version_code = int(version_code) if version_code.isdigit() else None
version_name = version_name if version_name != "" else None

return dict(
version_name=version_name,
version_code=version_code
)

def has_app(self, package_name: str) -> bool:
data = self.shell("bm dump -a").output
Expand Down Expand Up @@ -260,12 +314,38 @@ def swipe(self, x1, y1, x2, y2, speed=1000):
def input_text(self, x: int, y: int, text: str):
self.shell(f"uitest uiInput inputText {x} {y} {text}")

def screenshot(self, path: str) -> str:
_uuid = uuid.uuid4().hex
_tmp_path = f"/data/local/tmp/_tmp_{_uuid}.jpeg"
self.shell(f"snapshot_display -f {_tmp_path}")
self.recv_file(_tmp_path, path)
self.shell(f"rm -rf {_tmp_path}") # remove local path
def screenshot(self, path: str, method: str = "snapshot_display") -> str:
"""
Take a screenshot using one of the two available methods.

Args:
path (str): The local path where the screenshot will be saved.
method (str): The screenshot method to use. Options are:
- "snapshot_display" (default, recommended for better performance)
This method is faster and more efficient, but the image quality is lower.
- "screenCap" (alternative method)
This method produces higher-quality images (5~20 times clearer), but it is slower.

Returns:
str: The local path where the screenshot is saved.
"""
if method == "snapshot_display":
# Use the recommended method (snapshot_display)
_uuid = uuid.uuid4().hex
_tmp_path = f"/data/local/tmp/_tmp_{_uuid}.jpeg"
self.shell(f"snapshot_display -f {_tmp_path}")
self.recv_file(_tmp_path, path)
self.shell(f"rm -rf {_tmp_path}")
elif method == "screenCap":
# Use the alternative method (screenCap)
_uuid = uuid.uuid4().hex
_tmp_path = f"/data/local/tmp/{_uuid}.png"
self.shell(f"uitest screenCap -p {_tmp_path}")
self.recv_file(_tmp_path, path)
self.shell(f"rm -rf {_tmp_path}")
else:
raise ValueError(f"Invalid screenshot method: {method}. Use 'snapshot_display' or 'screenCap'.")

return path

def dump_hierarchy(self) -> Dict:
Expand Down
51 changes: 46 additions & 5 deletions tests/test_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,40 @@ def test_uninstall_app(d):


def test_list_apps(d):
assert "com.samples.test.uitest" in d.list_apps()
apps = d.list_apps()
# Assert that no item in the list starts with "ID:"
assert not any(item.startswith("ID:") for item in apps)

# Assert that no empty strings are in the list
assert "" not in apps

# Assert that "com.samples.test.uitest" is in the list
assert "com.sogou.input" in apps
# assert "com.samples.test.uitest" in apps

# Test all apps (include system apps)
all_apps = d.list_apps(include_system_apps=True)

# Assert that "com.huawei.systemmanager" is in the list (system app)
assert "com.huawei.hmos.settings" in all_apps, "'com.huawei.hmos.settings' is not in the full apps list."

# Assert that "com.sogou.input" is also in the list
assert "com.sogou.input" in all_apps, "'com.sogou.input' is not in the full apps list."

# Assert that the total number of apps is greater than the number of third-party apps
assert len(all_apps) > len(apps), "Full apps list is not larger than third-party apps list."


def test_app_version(d):
version_info = d.app_version('com.sogou.input')

# Assert that version_name and version_code exist in the returned dictionary
assert 'version_name' in version_info, "version_name is missing in the returned dictionary."
assert 'version_code' in version_info, "version_code is missing in the returned dictionary."

# Assert specific values
assert version_info['version_name'] == "1.0.4", "version_name does not match expected value."
assert version_info['version_code'] == 5, "version_code does not match expected value."


def test_has_app(d):
Expand Down Expand Up @@ -123,12 +156,18 @@ def test_push_file(d):
d.push_file(lpath, rpath)


def test_screenshot(d) -> str:
def test_screenshot(d):
lpath = "./test.png"
d.screenshot(lpath)
assert os.path.exists(lpath)


def test_screenshot_by_screen_cap(d):
lpath = "./test_screen_cap.png"
d.screenshot(lpath, method='screenCap')
assert os.path.exists(lpath)


def test_shell(d):
d.shell("pwd")

Expand Down Expand Up @@ -176,7 +215,9 @@ def test_toast(d):

def test_gesture(d):
d(id="drag").click()
d.gesture.start(630, 984, interval=1).move(0.2, 0.4, interval=.5).pause(interval=1).move(0.5, 0.6, interval=.5).pause(interval=1).action()
d.gesture.start(630, 984, interval=1).move(0.2, 0.4, interval=.5).pause(interval=1).move(0.5, 0.6,
interval=.5).pause(
interval=1).action()
d.go_back()


Expand Down Expand Up @@ -213,7 +254,7 @@ def test_screenrecord2(d):
def test_xpath(d):
d.force_start_app("com.samples.test.uitest", "EntryAbility")

xpath1 = '//root[1]/Row[1]/Column[1]/Row[1]/Button[3]' # showToast
xpath1 = '//root[1]/Row[1]/Column[1]/Row[1]/Button[3]' # showToast
xpath2 = '//*[@text="showDialog"]'

d.toast_watcher.start()
Expand All @@ -222,4 +263,4 @@ def test_xpath(d):
print(f"toast: {toast}")
assert toast == "testMessage"

d.xpath(xpath2).click()
d.xpath(xpath2).click()