Skip to content

Commit aa29910

Browse files
authored
Merge pull request #44 from lixf6/master
optimize list_apps and screenshot
2 parents 297537b + 827acb6 commit aa29910

3 files changed

Lines changed: 147 additions & 24 deletions

File tree

hmdriver2/driver.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,11 @@ def install_app(self, apk_path: str):
117117
def uninstall_app(self, package_name: str):
118118
self.hdc.uninstall(package_name)
119119

120-
def list_apps(self) -> List:
121-
return self.hdc.list_apps()
120+
def list_apps(self, include_system_apps: bool = False) -> List:
121+
return self.hdc.list_apps(include_system_apps)
122+
123+
def app_version(self, bundle_name) -> Dict:
124+
return self.hdc.app_version(bundle_name)
122125

123126
def has_app(self, package_name: str) -> bool:
124127
return self.hdc.has_app(package_name)
@@ -342,22 +345,20 @@ def push_file(self, lpath: str, rpath: str):
342345
"""
343346
self.hdc.send_file(lpath, rpath)
344347

345-
def screenshot(self, path: str) -> str:
348+
def screenshot(self, path: str, method: str = "snapshot_display") -> str:
346349
"""
347350
Take a screenshot of the device display.
348351
349352
Args:
350353
path (str): The local path to save the screenshot.
354+
method (str): The screenshot method to use. Options are:
355+
- "snapshot_display" (default, recommended for better performance)
356+
- "screenCap" (alternative method, higher quality but slower).
351357
352358
Returns:
353359
str: The path where the screenshot is saved.
354360
"""
355-
_uuid = uuid.uuid4().hex
356-
_tmp_path = f"/data/local/tmp/_tmp_{_uuid}.jpeg"
357-
self.shell(f"snapshot_display -f {_tmp_path}")
358-
self.pull_file(_tmp_path, path)
359-
self.shell(f"rm -rf {_tmp_path}") # remove local path
360-
return path
361+
return self.hdc.screenshot(path, method=method)
361362

362363
def shell(self, cmd) -> CommandResult:
363364
return self.hdc.shell(cmd)
@@ -486,3 +487,4 @@ def xpath(self):
486487
"""
487488
from ._xpath import _XPath
488489
return _XPath(self)
490+

hmdriver2/hdc.py

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import re
77
import os
88
import subprocess
9-
from typing import Union, List, Dict, Tuple
9+
from typing import Union, List, Dict, Tuple, Optional
1010

1111
from . import logger
1212
from .utils import FreePort
@@ -139,10 +139,64 @@ def install(self, apkpath: str):
139139
raise HdcError("HDC install error", result.error)
140140
return result
141141

142-
def list_apps(self) -> List[str]:
143-
result = self.shell("bm dump -a")
142+
def list_apps(self, include_system_apps: bool = False) -> List[str]:
143+
"""
144+
List installed applications on the device. (Lazy loading, default: third-party apps)
145+
146+
Args:
147+
include_system_apps (bool): If True, include system apps in the list.
148+
If False, only list third-party apps.
149+
150+
Returns:
151+
List[str]: A list of application package names.
152+
153+
Note:
154+
- When include_system_apps is False, the list typically contains around 50 third-party apps.
155+
- When include_system_apps is True, the list typically contains around 200 apps in total.
156+
"""
157+
# Construct the shell command based on the include_system_apps flag
158+
if include_system_apps:
159+
command = "bm dump -a"
160+
else:
161+
command = "bm dump -a | grep -v 'com.huawei'"
162+
163+
# Execute the shell command
164+
result = self.shell(command)
144165
raw = result.output.split('\n')
145-
return [item.strip() for item in raw]
166+
167+
# Filter out strings starting with 'ID:' and empty strings
168+
return [item.strip() for item in raw if item.strip() and not re.match(r'^ID:', item.strip())]
169+
170+
def app_version(self, bundlename: str) -> Dict[str, Optional[str]]:
171+
"""
172+
Get the version information of an app installed on the device.
173+
174+
Args:
175+
bundlename (str): The bundle name of the app.
176+
177+
Returns:
178+
dict: A dictionary containing the version information:
179+
- "versionName": The version name of the app.
180+
- "versionCode": The version code of the app.
181+
"""
182+
result = _execute_command(f"{self.hdc_prefix} -t {self.serial} shell bm dump -n {bundlename} | grep '\"versionCode\":\\|versionName\"'")
183+
184+
matches = re.findall(r'"versionCode":\s*(\d+),\s*"versionName":\s*"([^"]*)"', result.output)
185+
if not matches:
186+
return dict(
187+
version_name='',
188+
version_code=''
189+
)
190+
191+
# Select the last match
192+
version_code, version_name = matches[-1]
193+
version_code = int(version_code) if version_code.isdigit() else None
194+
version_name = version_name if version_name != "" else None
195+
196+
return dict(
197+
version_name=version_name,
198+
version_code=version_code
199+
)
146200

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

263-
def screenshot(self, path: str) -> str:
264-
_uuid = uuid.uuid4().hex
265-
_tmp_path = f"/data/local/tmp/_tmp_{_uuid}.jpeg"
266-
self.shell(f"snapshot_display -f {_tmp_path}")
267-
self.recv_file(_tmp_path, path)
268-
self.shell(f"rm -rf {_tmp_path}") # remove local path
317+
def screenshot(self, path: str, method: str = "snapshot_display") -> str:
318+
"""
319+
Take a screenshot using one of the two available methods.
320+
321+
Args:
322+
path (str): The local path where the screenshot will be saved.
323+
method (str): The screenshot method to use. Options are:
324+
- "snapshot_display" (default, recommended for better performance)
325+
This method is faster and more efficient, but the image quality is lower.
326+
- "screenCap" (alternative method)
327+
This method produces higher-quality images (5~20 times clearer), but it is slower.
328+
329+
Returns:
330+
str: The local path where the screenshot is saved.
331+
"""
332+
if method == "snapshot_display":
333+
# Use the recommended method (snapshot_display)
334+
_uuid = uuid.uuid4().hex
335+
_tmp_path = f"/data/local/tmp/_tmp_{_uuid}.jpeg"
336+
self.shell(f"snapshot_display -f {_tmp_path}")
337+
self.recv_file(_tmp_path, path)
338+
self.shell(f"rm -rf {_tmp_path}")
339+
elif method == "screenCap":
340+
# Use the alternative method (screenCap)
341+
_uuid = uuid.uuid4().hex
342+
_tmp_path = f"/data/local/tmp/{_uuid}.png"
343+
self.shell(f"uitest screenCap -p {_tmp_path}")
344+
self.recv_file(_tmp_path, path)
345+
self.shell(f"rm -rf {_tmp_path}")
346+
else:
347+
raise ValueError(f"Invalid screenshot method: {method}. Use 'snapshot_display' or 'screenCap'.")
348+
269349
return path
270350

271351
def dump_hierarchy(self) -> Dict:

tests/test_driver.py

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,40 @@ def test_uninstall_app(d):
5151

5252

5353
def test_list_apps(d):
54-
assert "com.samples.test.uitest" in d.list_apps()
54+
apps = d.list_apps()
55+
# Assert that no item in the list starts with "ID:"
56+
assert not any(item.startswith("ID:") for item in apps)
57+
58+
# Assert that no empty strings are in the list
59+
assert "" not in apps
60+
61+
# Assert that "com.samples.test.uitest" is in the list
62+
assert "com.sogou.input" in apps
63+
# assert "com.samples.test.uitest" in apps
64+
65+
# Test all apps (include system apps)
66+
all_apps = d.list_apps(include_system_apps=True)
67+
68+
# Assert that "com.huawei.systemmanager" is in the list (system app)
69+
assert "com.huawei.hmos.settings" in all_apps, "'com.huawei.hmos.settings' is not in the full apps list."
70+
71+
# Assert that "com.sogou.input" is also in the list
72+
assert "com.sogou.input" in all_apps, "'com.sogou.input' is not in the full apps list."
73+
74+
# Assert that the total number of apps is greater than the number of third-party apps
75+
assert len(all_apps) > len(apps), "Full apps list is not larger than third-party apps list."
76+
77+
78+
def test_app_version(d):
79+
version_info = d.app_version('com.sogou.input')
80+
81+
# Assert that version_name and version_code exist in the returned dictionary
82+
assert 'version_name' in version_info, "version_name is missing in the returned dictionary."
83+
assert 'version_code' in version_info, "version_code is missing in the returned dictionary."
84+
85+
# Assert specific values
86+
assert version_info['version_name'] == "1.0.4", "version_name does not match expected value."
87+
assert version_info['version_code'] == 5, "version_code does not match expected value."
5588

5689

5790
def test_has_app(d):
@@ -123,12 +156,18 @@ def test_push_file(d):
123156
d.push_file(lpath, rpath)
124157

125158

126-
def test_screenshot(d) -> str:
159+
def test_screenshot(d):
127160
lpath = "./test.png"
128161
d.screenshot(lpath)
129162
assert os.path.exists(lpath)
130163

131164

165+
def test_screenshot_by_screen_cap(d):
166+
lpath = "./test_screen_cap.png"
167+
d.screenshot(lpath, method='screenCap')
168+
assert os.path.exists(lpath)
169+
170+
132171
def test_shell(d):
133172
d.shell("pwd")
134173

@@ -176,7 +215,9 @@ def test_toast(d):
176215

177216
def test_gesture(d):
178217
d(id="drag").click()
179-
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()
218+
d.gesture.start(630, 984, interval=1).move(0.2, 0.4, interval=.5).pause(interval=1).move(0.5, 0.6,
219+
interval=.5).pause(
220+
interval=1).action()
180221
d.go_back()
181222

182223

@@ -213,7 +254,7 @@ def test_screenrecord2(d):
213254
def test_xpath(d):
214255
d.force_start_app("com.samples.test.uitest", "EntryAbility")
215256

216-
xpath1 = '//root[1]/Row[1]/Column[1]/Row[1]/Button[3]' # showToast
257+
xpath1 = '//root[1]/Row[1]/Column[1]/Row[1]/Button[3]' # showToast
217258
xpath2 = '//*[@text="showDialog"]'
218259

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

225-
d.xpath(xpath2).click()
266+
d.xpath(xpath2).click()

0 commit comments

Comments
 (0)