devhelmkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devhelmkit/__init__.py +44 -0
- devhelmkit/android/__init__.py +7 -0
- devhelmkit/android/driver.py +18 -0
- devhelmkit/assets/so/arm64-v8a/agent_v1.so +0 -0
- devhelmkit/assets/so/arm64-v8a/agent_v2.so +0 -0
- devhelmkit/assets/so/x86_64/agent.so +0 -0
- devhelmkit/core/__init__.py +3 -0
- devhelmkit/core/base_component.py +214 -0
- devhelmkit/core/base_driver.py +414 -0
- devhelmkit/core/base_window.py +25 -0
- devhelmkit/core/selector_spec.py +102 -0
- devhelmkit/entry.py +90 -0
- devhelmkit/exceptions.py +56 -0
- devhelmkit/harmony/__init__.py +3 -0
- devhelmkit/harmony/agent/__init__.py +7 -0
- devhelmkit/harmony/agent/so_manager.py +300 -0
- devhelmkit/harmony/config.py +124 -0
- devhelmkit/harmony/device/__init__.py +6 -0
- devhelmkit/harmony/device/hdc.py +430 -0
- devhelmkit/harmony/driver.py +1416 -0
- devhelmkit/harmony/finder/__init__.py +12 -0
- devhelmkit/harmony/finder/component_finder.py +336 -0
- devhelmkit/harmony/finder/popup_handler.py +81 -0
- devhelmkit/harmony/finder/selector_adapter.py +101 -0
- devhelmkit/harmony/finder/xpath_query.py +110 -0
- devhelmkit/harmony/rpc/__init__.py +12 -0
- devhelmkit/harmony/rpc/client.py +126 -0
- devhelmkit/harmony/rpc/proxy_v2.py +106 -0
- devhelmkit/harmony/rpc/remote_object.py +48 -0
- devhelmkit/harmony/uiobject.py +246 -0
- devhelmkit/harmony/uiwindow.py +43 -0
- devhelmkit/harmony/webview/__init__.py +17 -0
- devhelmkit/harmony/webview/chromedriver_manager.py +251 -0
- devhelmkit/harmony/webview/devtools_finder.py +131 -0
- devhelmkit/harmony/webview/webview_driver.py +326 -0
- devhelmkit/model/__init__.py +3 -0
- devhelmkit/model/action.py +147 -0
- devhelmkit/model/app_state.py +50 -0
- devhelmkit/model/constants.py +22 -0
- devhelmkit/model/display.py +32 -0
- devhelmkit/model/format_string.py +24 -0
- devhelmkit/model/input.py +50 -0
- devhelmkit/model/json_base.py +42 -0
- devhelmkit/model/keys.py +375 -0
- devhelmkit/model/match_pattern.py +15 -0
- devhelmkit/model/page.py +13 -0
- devhelmkit/model/params.py +42 -0
- devhelmkit/model/rect.py +58 -0
- devhelmkit/model/runnable.py +18 -0
- devhelmkit/utils/__init__.py +3 -0
- devhelmkit/utils/logger.py +72 -0
- devhelmkit/utils/retry.py +46 -0
- devhelmkit/utils/timeout.py +64 -0
- devhelmkit-0.1.0.dist-info/METADATA +411 -0
- devhelmkit-0.1.0.dist-info/RECORD +58 -0
- devhelmkit-0.1.0.dist-info/WHEEL +5 -0
- devhelmkit-0.1.0.dist-info/licenses/LICENSE +201 -0
- devhelmkit-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""WebViewDriver:webview 自动化驱动。
|
|
4
|
+
|
|
5
|
+
连接设备端 webview,通过 selenium webdriver 提供页面级自动化能力。
|
|
6
|
+
|
|
7
|
+
完整流程:
|
|
8
|
+
1. 探测 devtools 端口(domain socket 或 tcp)
|
|
9
|
+
2. 建立 hdc 端口转发(本地端口 -> 设备端 devtools)
|
|
10
|
+
3. 查询 webview 内核版本
|
|
11
|
+
4. 启动匹配版本的 chromedriver
|
|
12
|
+
5. 通过 selenium Remote 连接
|
|
13
|
+
6. 资源释放:移除端口转发 + quit webdriver + 停止 chromedriver
|
|
14
|
+
|
|
15
|
+
使用方式:
|
|
16
|
+
from devhelmkit.harmony.webview import WebViewDriver
|
|
17
|
+
|
|
18
|
+
wv = WebViewDriver(device)
|
|
19
|
+
wv.connect("com.huawei.hmos.browser")
|
|
20
|
+
wv.driver.get("https://www.baidu.com")
|
|
21
|
+
wv.close()
|
|
22
|
+
|
|
23
|
+
或通过 HarmonyDriver 入口:
|
|
24
|
+
d.webview("com.huawei.hmos.browser")
|
|
25
|
+
"""
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
import time
|
|
29
|
+
import urllib.request
|
|
30
|
+
from typing import Optional, TYPE_CHECKING
|
|
31
|
+
|
|
32
|
+
from devhelmkit.exceptions import DevhelmError
|
|
33
|
+
from devhelmkit.harmony.webview.chromedriver_manager import ChromedriverManager
|
|
34
|
+
from devhelmkit.harmony.webview.devtools_finder import DevtoolsFinder
|
|
35
|
+
from devhelmkit.utils import logger
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from selenium.webdriver.remote.webdriver import WebDriver
|
|
39
|
+
from devhelmkit.harmony.device.hdc import HdcDevice
|
|
40
|
+
|
|
41
|
+
# 默认本地 devtools 端口(自动分配空闲端口)
|
|
42
|
+
DEFAULT_DEVTOOL_PORT = 9222
|
|
43
|
+
|
|
44
|
+
# 默认连接超时(秒)
|
|
45
|
+
DEFAULT_CONNECTION_TIMEOUT = 60
|
|
46
|
+
|
|
47
|
+
# 页面加载超时(秒)
|
|
48
|
+
DEFAULT_PAGE_LOAD_TIMEOUT = 50
|
|
49
|
+
|
|
50
|
+
# 脚本执行超时(秒)
|
|
51
|
+
DEFAULT_SCRIPT_TIMEOUT = 50
|
|
52
|
+
|
|
53
|
+
# 隐式等待(秒)
|
|
54
|
+
IMPLICIT_WAIT_TIMEOUT = 20
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class WebViewDriver:
|
|
58
|
+
"""webview 自动化驱动,封装 selenium webdriver 连接与资源管理。"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, device: "HdcDevice",
|
|
61
|
+
chromedriver_search_path: str = "",
|
|
62
|
+
chromedriver_exe_path: str = "",
|
|
63
|
+
chromedriver_port: int = 9515):
|
|
64
|
+
"""
|
|
65
|
+
Args:
|
|
66
|
+
device: HdcDevice 实例
|
|
67
|
+
chromedriver_search_path: chromedriver 存放目录(多版本结构)
|
|
68
|
+
chromedriver_exe_path: 直接指定 chromedriver 路径(优先)
|
|
69
|
+
chromedriver_port: chromedriver 监听端口
|
|
70
|
+
"""
|
|
71
|
+
self._device = device
|
|
72
|
+
self._bundle_name: Optional[str] = None
|
|
73
|
+
self._driver: Optional["WebDriver"] = None
|
|
74
|
+
self._devtool_port = DEFAULT_DEVTOOL_PORT
|
|
75
|
+
self._remote_devtool_port = DEFAULT_DEVTOOL_PORT
|
|
76
|
+
self._fport_established = False
|
|
77
|
+
|
|
78
|
+
self._finder = DevtoolsFinder(device)
|
|
79
|
+
self._chromedriver = ChromedriverManager(
|
|
80
|
+
search_path=chromedriver_search_path,
|
|
81
|
+
exe_path=chromedriver_exe_path,
|
|
82
|
+
port=chromedriver_port,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def driver(self) -> "WebDriver":
|
|
87
|
+
"""selenium webdriver 实例。
|
|
88
|
+
|
|
89
|
+
未连接时抛异常,确保调用前已 connect。
|
|
90
|
+
"""
|
|
91
|
+
if self._driver is None:
|
|
92
|
+
raise DevhelmError("webview 未连接,请先调用 connect()")
|
|
93
|
+
return self._driver
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def devtools_url(self) -> str:
|
|
97
|
+
"""本地 devtools 访问地址。"""
|
|
98
|
+
return "http://127.0.0.1:%d" % self._devtool_port
|
|
99
|
+
|
|
100
|
+
def connect(self, bundle_name: str,
|
|
101
|
+
remote_devtools_port: Optional[int] = None,
|
|
102
|
+
connection_timeout: int = DEFAULT_CONNECTION_TIMEOUT,
|
|
103
|
+
options=None) -> "WebDriver":
|
|
104
|
+
"""连接指定应用的 webview。
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
bundle_name: 目标应用包名
|
|
108
|
+
remote_devtools_port: 自定义 webview 内核的 devtools 端口;
|
|
109
|
+
系统 web 内核无需指定,自动探测
|
|
110
|
+
connection_timeout: 连接超时(秒)
|
|
111
|
+
options: 传递给 selenium webdriver 的 options
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
selenium WebDriver 实例
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
DevhelmError: 连接失败
|
|
118
|
+
"""
|
|
119
|
+
self._bundle_name = bundle_name
|
|
120
|
+
self.close()
|
|
121
|
+
self._setup_port_forward(bundle_name, remote_devtools_port)
|
|
122
|
+
webview_version = self._get_webview_version()
|
|
123
|
+
self._chromedriver.start(webview_version)
|
|
124
|
+
self._driver = self._connect_selenium(options, connection_timeout)
|
|
125
|
+
return self._driver
|
|
126
|
+
|
|
127
|
+
def close(self) -> None:
|
|
128
|
+
"""释放资源:移除端口转发 + quit webdriver + 停止 chromedriver。"""
|
|
129
|
+
if self._driver is not None:
|
|
130
|
+
try:
|
|
131
|
+
self._driver.quit()
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.warn("quit webdriver 异常: %s", e)
|
|
134
|
+
finally:
|
|
135
|
+
self._driver = None
|
|
136
|
+
|
|
137
|
+
self._remove_port_forward()
|
|
138
|
+
|
|
139
|
+
if self._chromedriver is not None:
|
|
140
|
+
self._chromedriver.stop()
|
|
141
|
+
|
|
142
|
+
def switch_to_visible_window(self, index: int = 0) -> None:
|
|
143
|
+
"""切换到 visible 状态的窗口。
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
index: 第几个可见窗口(0 表示第一个)
|
|
147
|
+
"""
|
|
148
|
+
driver = self.driver
|
|
149
|
+
handles = driver.window_handles
|
|
150
|
+
if index < 0:
|
|
151
|
+
handles = list(reversed(handles))
|
|
152
|
+
index = abs(index) - 1
|
|
153
|
+
if index >= len(handles):
|
|
154
|
+
raise ValueError("窗口索引越界: %d (共 %d 个)" % (index, len(handles)))
|
|
155
|
+
|
|
156
|
+
count = index
|
|
157
|
+
for handle in handles:
|
|
158
|
+
driver.switch_to.window(handle)
|
|
159
|
+
visible = driver.execute_script("return document.visibilityState")
|
|
160
|
+
if visible == "visible":
|
|
161
|
+
if count == 0:
|
|
162
|
+
return
|
|
163
|
+
count -= 1
|
|
164
|
+
|
|
165
|
+
def get_all_windows(self, with_url: bool = False) -> list:
|
|
166
|
+
"""获取所有窗口信息。
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
with_url: True 返回包含 url/title/visible 的字典列表
|
|
170
|
+
"""
|
|
171
|
+
if not with_url:
|
|
172
|
+
return self.driver.window_handles
|
|
173
|
+
|
|
174
|
+
driver = self.driver
|
|
175
|
+
current = driver.current_window_handle
|
|
176
|
+
result = []
|
|
177
|
+
for handle in driver.window_handles:
|
|
178
|
+
driver.switch_to.window(handle)
|
|
179
|
+
result.append({
|
|
180
|
+
"handle": handle,
|
|
181
|
+
"url": driver.current_url,
|
|
182
|
+
"title": driver.title,
|
|
183
|
+
"visible": driver.execute_script("return document.visibilityState"),
|
|
184
|
+
})
|
|
185
|
+
driver.switch_to.window(current)
|
|
186
|
+
return result
|
|
187
|
+
|
|
188
|
+
def __getattr__(self, name):
|
|
189
|
+
"""代理未找到的属性到 selenium webdriver。"""
|
|
190
|
+
return getattr(self._driver, name)
|
|
191
|
+
|
|
192
|
+
def __enter__(self):
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
196
|
+
self.close()
|
|
197
|
+
|
|
198
|
+
# ============================================================
|
|
199
|
+
# 内部实现
|
|
200
|
+
# ============================================================
|
|
201
|
+
|
|
202
|
+
def _setup_port_forward(self, bundle_name: str,
|
|
203
|
+
remote_devtools_port: Optional[int]) -> None:
|
|
204
|
+
"""建立 hdc 端口转发。"""
|
|
205
|
+
self._devtool_port = _get_unused_port()
|
|
206
|
+
|
|
207
|
+
if remote_devtools_port is not None:
|
|
208
|
+
# 自定义 webview 内核,指定 tcp 端口
|
|
209
|
+
self._remote_devtool_port = remote_devtools_port
|
|
210
|
+
if not self._finder.check_tcp_port(remote_devtools_port):
|
|
211
|
+
raise DevhelmError(
|
|
212
|
+
"设备端 devtools 端口 %d 未开放,请检查应用是否已开启 web 调试"
|
|
213
|
+
% remote_devtools_port
|
|
214
|
+
)
|
|
215
|
+
self._device.shell(
|
|
216
|
+
"hdc fport tcp:%d tcp:%d" % (self._devtool_port, remote_devtools_port)
|
|
217
|
+
)
|
|
218
|
+
elif self._finder.is_using_domain_socket():
|
|
219
|
+
# 系统 web 内核,通过 domain socket 探测
|
|
220
|
+
socket_name = self._finder.find_devtools_socket(bundle_name)
|
|
221
|
+
if socket_name is None:
|
|
222
|
+
raise DevhelmError(
|
|
223
|
+
"未找到 %s 的 devtools socket,请检查应用是否已开启 web 调试"
|
|
224
|
+
% bundle_name
|
|
225
|
+
)
|
|
226
|
+
self._device.shell(
|
|
227
|
+
"hdc fport tcp:%d localabstract:%s"
|
|
228
|
+
% (self._devtool_port, socket_name)
|
|
229
|
+
)
|
|
230
|
+
else:
|
|
231
|
+
# 未指定端口且非 domain socket,尝试默认 tcp 端口
|
|
232
|
+
if not self._finder.check_tcp_port(self._remote_devtool_port):
|
|
233
|
+
raise DevhelmError(
|
|
234
|
+
"设备端 devtools 端口 %d 未开放" % self._remote_devtool_port
|
|
235
|
+
)
|
|
236
|
+
self._device.shell(
|
|
237
|
+
"hdc fport tcp:%d tcp:%d"
|
|
238
|
+
% (self._devtool_port, self._remote_devtool_port)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
self._fport_established = True
|
|
242
|
+
logger.info(
|
|
243
|
+
"devtools 端口转发已建立: tcp:%d -> 设备 (bundle=%s)",
|
|
244
|
+
self._devtool_port, bundle_name
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def _remove_port_forward(self) -> None:
|
|
248
|
+
"""移除 hdc 端口转发。"""
|
|
249
|
+
if not self._fport_established:
|
|
250
|
+
return
|
|
251
|
+
try:
|
|
252
|
+
self._device.shell("hdc fport rm tcp:%d" % self._devtool_port)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.warn("移除端口转发异常: %s", e)
|
|
255
|
+
finally:
|
|
256
|
+
self._fport_established = False
|
|
257
|
+
|
|
258
|
+
def _get_webview_version(self, retry: int = 3) -> int:
|
|
259
|
+
"""查询设备端 webview 内核版本。
|
|
260
|
+
|
|
261
|
+
通过 devtools /json/version 接口获取。
|
|
262
|
+
"""
|
|
263
|
+
for _ in range(retry):
|
|
264
|
+
try:
|
|
265
|
+
resp = urllib.request.urlopen(
|
|
266
|
+
self.devtools_url + "/json/version", timeout=5
|
|
267
|
+
).read().decode("utf-8", errors="ignore")
|
|
268
|
+
info = json.loads(resp)
|
|
269
|
+
browser = info.get("Browser", "")
|
|
270
|
+
match = re.search(r"/(\d+)\.", browser)
|
|
271
|
+
if match:
|
|
272
|
+
version = int(match.group(1))
|
|
273
|
+
logger.info("webview 内核版本: %d", version)
|
|
274
|
+
return version
|
|
275
|
+
logger.warn("未知 webview 版本信息: %s", resp)
|
|
276
|
+
except Exception as e:
|
|
277
|
+
logger.warn("查询 webview 版本失败: %s", e)
|
|
278
|
+
logger.warn("无法获取 webview 版本,回退到 114")
|
|
279
|
+
return 114
|
|
280
|
+
|
|
281
|
+
def _connect_selenium(self, options, timeout: int) -> "WebDriver":
|
|
282
|
+
"""通过 selenium Remote 连接 chromedriver。"""
|
|
283
|
+
try:
|
|
284
|
+
from selenium import webdriver
|
|
285
|
+
from selenium.webdriver.remote.remote_connection import RemoteConnection
|
|
286
|
+
except ImportError as e:
|
|
287
|
+
raise DevhelmError(
|
|
288
|
+
"selenium 未安装,请执行 pip install selenium 或 pip install devhelmkit[webview]"
|
|
289
|
+
) from e
|
|
290
|
+
|
|
291
|
+
RemoteConnection.set_timeout(timeout)
|
|
292
|
+
|
|
293
|
+
if options is None:
|
|
294
|
+
options = webdriver.ChromeOptions()
|
|
295
|
+
options.add_experimental_option(
|
|
296
|
+
"debuggerAddress", "127.0.0.1:%d" % self._devtool_port
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
driver = webdriver.Remote(
|
|
301
|
+
command_executor=self._chromedriver.host,
|
|
302
|
+
options=options
|
|
303
|
+
)
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logger.error("连接 chromedriver 失败: %s", e)
|
|
306
|
+
self._chromedriver.stop()
|
|
307
|
+
raise DevhelmError("selenium webdriver 连接失败: %s" % e) from e
|
|
308
|
+
|
|
309
|
+
driver.set_page_load_timeout(DEFAULT_PAGE_LOAD_TIMEOUT)
|
|
310
|
+
driver.set_script_timeout(DEFAULT_SCRIPT_TIMEOUT)
|
|
311
|
+
driver.implicitly_wait(IMPLICIT_WAIT_TIMEOUT)
|
|
312
|
+
|
|
313
|
+
logger.info("webview 已连接, 窗口数: %d", len(driver.window_handles))
|
|
314
|
+
logger.info("当前页面: %s", driver.current_url)
|
|
315
|
+
return driver
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _get_unused_port() -> int:
|
|
319
|
+
"""获取一个本地空闲端口。"""
|
|
320
|
+
import socket
|
|
321
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
322
|
+
try:
|
|
323
|
+
sock.bind(("127.0.0.1", 0))
|
|
324
|
+
return sock.getsockname()[1]
|
|
325
|
+
finally:
|
|
326
|
+
sock.close()
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""操作类型与上下文:ActionType / ActionTrackGroup / ActionContext / ActionContextStack。
|
|
4
|
+
|
|
5
|
+
ActionContextStack 持有 driver 的 weakref.proxy,避免循环引用。
|
|
6
|
+
"""
|
|
7
|
+
import weakref
|
|
8
|
+
from typing import List, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
from devhelmkit.exceptions import DevhelmError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ActionType:
|
|
14
|
+
"""操作类型常量。"""
|
|
15
|
+
CLICK = "click"
|
|
16
|
+
DOUBLE_CLICK = "double_click"
|
|
17
|
+
LONG_CLICK = "long_click"
|
|
18
|
+
SWIPE = "swipe"
|
|
19
|
+
DRAG = "drag"
|
|
20
|
+
PINCH_IN = "pinch_in"
|
|
21
|
+
PINCH_OUT = "pinch_out"
|
|
22
|
+
INPUT_TEXT = "input_text"
|
|
23
|
+
PRESS_KEY = "press_key"
|
|
24
|
+
MOUSE_SCROLL = "mouse_scroll"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ActionTrackGroup:
|
|
28
|
+
"""操作路径信息。
|
|
29
|
+
|
|
30
|
+
使用 __init__ 接收可变参数 *points_group,每个参数是 list[(x, y)]。
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, *points_group):
|
|
34
|
+
if len(points_group) <= 0:
|
|
35
|
+
raise DevhelmError("No data for ActionTrackGroup")
|
|
36
|
+
for points in points_group:
|
|
37
|
+
if not isinstance(points, list):
|
|
38
|
+
raise TypeError("Invalid point_list: %s" % points)
|
|
39
|
+
for point in points:
|
|
40
|
+
x, y = point # 解包校验
|
|
41
|
+
self._trace_group: List[List[Tuple[int, int]]] = list(points_group)
|
|
42
|
+
|
|
43
|
+
def __getitem__(self, item):
|
|
44
|
+
return self._trace_group[item]
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def first_track(self):
|
|
48
|
+
if len(self._trace_group) < 1:
|
|
49
|
+
raise DevhelmError("No data in this ActionTraceGroup")
|
|
50
|
+
return self[0]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ActionContext:
|
|
54
|
+
"""单次操作上下文。"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, level: int = 0):
|
|
57
|
+
self.level = level
|
|
58
|
+
self.reset()
|
|
59
|
+
|
|
60
|
+
def reset(self):
|
|
61
|
+
self.action_desc: Optional[str] = None
|
|
62
|
+
self.action_type: Optional[str] = None
|
|
63
|
+
self.pre_page = None
|
|
64
|
+
self.next_page = None
|
|
65
|
+
self.target_selector = None
|
|
66
|
+
self.target_component = None
|
|
67
|
+
self.target_recognize_page = None
|
|
68
|
+
self.action_runnable = None
|
|
69
|
+
self.allow_skip = False
|
|
70
|
+
self.metrics = None
|
|
71
|
+
|
|
72
|
+
def __repr__(self):
|
|
73
|
+
return str(self.action_desc)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ActionContextStack:
|
|
77
|
+
"""操作上下文栈。
|
|
78
|
+
|
|
79
|
+
用于嵌套操作时管理上下文层级,向栈底传递页面/控件信息。
|
|
80
|
+
持有 driver 的 weakref.proxy 避免循环引用,支持 with 语法,
|
|
81
|
+
仅最顶层 action 通知 HookExtensionManager。
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, driver):
|
|
85
|
+
self.stack: List[ActionContext] = []
|
|
86
|
+
self.default_context = ActionContext()
|
|
87
|
+
self.driver = weakref.proxy(driver)
|
|
88
|
+
self.last_action: Optional[ActionContext] = None
|
|
89
|
+
|
|
90
|
+
def push(self) -> ActionContext:
|
|
91
|
+
new_context = ActionContext(len(self.stack))
|
|
92
|
+
self.stack.append(new_context)
|
|
93
|
+
return new_context
|
|
94
|
+
|
|
95
|
+
def pop(self, success: bool = True) -> Optional[ActionContext]:
|
|
96
|
+
if len(self.stack) <= 0:
|
|
97
|
+
return None
|
|
98
|
+
current_action_ctx = self.stack.pop()
|
|
99
|
+
self._set_upper_level_info()
|
|
100
|
+
if len(self.stack) <= 0 and success and (not current_action_ctx.allow_skip):
|
|
101
|
+
self.last_action = current_action_ctx
|
|
102
|
+
return current_action_ctx
|
|
103
|
+
|
|
104
|
+
def _set_upper_level_info(self):
|
|
105
|
+
"""向栈底传递页面/控件信息。"""
|
|
106
|
+
if len(self.stack) < 1:
|
|
107
|
+
return
|
|
108
|
+
top_item = self.top_item
|
|
109
|
+
for item in reversed(self.stack[:-1]):
|
|
110
|
+
if item.pre_page is None:
|
|
111
|
+
item.pre_page = top_item.pre_page
|
|
112
|
+
if item.next_page is None:
|
|
113
|
+
item.next_page = top_item.next_page
|
|
114
|
+
item.target_recognize_page = top_item.target_recognize_page
|
|
115
|
+
item.target_component = top_item.target_component
|
|
116
|
+
item.target_selector = top_item.target_selector
|
|
117
|
+
|
|
118
|
+
def __len__(self):
|
|
119
|
+
return len(self.stack)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def top_item(self) -> ActionContext:
|
|
123
|
+
if len(self.stack) == 0:
|
|
124
|
+
return self.default_context
|
|
125
|
+
return self.stack[-1]
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def bottom_item(self) -> ActionContext:
|
|
129
|
+
if len(self.stack) == 0:
|
|
130
|
+
return self.default_context
|
|
131
|
+
return self.stack[0]
|
|
132
|
+
|
|
133
|
+
def enter_new_context(self, action_type: str, action_desc: str,
|
|
134
|
+
action_runnable=None, metrics=None):
|
|
135
|
+
ctx = self.push()
|
|
136
|
+
ctx.action_type = action_type
|
|
137
|
+
ctx.action_desc = action_desc
|
|
138
|
+
ctx.action_runnable = action_runnable
|
|
139
|
+
ctx.metrics = metrics
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
def __enter__(self) -> ActionContext:
|
|
143
|
+
return self.top_item
|
|
144
|
+
|
|
145
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
146
|
+
self.pop(exc_val is None)
|
|
147
|
+
return False
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""应用与窗口状态类型:AppState / WindowMode / ResizeDirection /
|
|
4
|
+
WindowState / OSType。"""
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import IntEnum
|
|
7
|
+
from typing import Optional, Tuple
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AppState(IntEnum):
|
|
11
|
+
"""应用状态。"""
|
|
12
|
+
FOREGROUND = 0
|
|
13
|
+
BACKGROUND = 1
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WindowMode(IntEnum):
|
|
17
|
+
"""窗口模式。"""
|
|
18
|
+
FULLSCREEN = 0
|
|
19
|
+
PRIMARY = 1
|
|
20
|
+
SECONDARY = 2
|
|
21
|
+
FLOATING = 3
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ResizeDirection(IntEnum):
|
|
25
|
+
"""窗口调整大小方向。"""
|
|
26
|
+
LEFT = 0
|
|
27
|
+
RIGHT = 1
|
|
28
|
+
TOP = 2
|
|
29
|
+
BOTTOM = 3
|
|
30
|
+
TOP_LEFT = 4
|
|
31
|
+
TOP_RIGHT = 5
|
|
32
|
+
BOTTOM_LEFT = 6
|
|
33
|
+
BOTTOM_RIGHT = 7
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class WindowState:
|
|
38
|
+
"""窗口状态。"""
|
|
39
|
+
window_id: str
|
|
40
|
+
mode: WindowMode
|
|
41
|
+
bounds: Optional[Tuple[int, int, int, int]] = None
|
|
42
|
+
|
|
43
|
+
def is_fullscreen(self) -> bool:
|
|
44
|
+
return self.mode == WindowMode.FULLSCREEN
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class OSType:
|
|
48
|
+
"""操作系统类型(类常量风格)。"""
|
|
49
|
+
OHOS = "OHOS"
|
|
50
|
+
HMOS = "HMOS"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""常量定义。"""
|
|
4
|
+
|
|
5
|
+
# 超时与等待
|
|
6
|
+
INPUT_TEXT_WAIT_TIME = 1
|
|
7
|
+
DEFAULT_IDLE_TIME = 0.5
|
|
8
|
+
DEFAULT_SLIDE_TIME = 0.3
|
|
9
|
+
MAX_TIMEOUT = 60
|
|
10
|
+
DEFAULT_TIMEOUT = 5
|
|
11
|
+
WAIT_FOR_INSTALL_TIME = 3
|
|
12
|
+
FAST_SWIPE_TIME = 0.25
|
|
13
|
+
SWIPE_INTERVAL = 0.5
|
|
14
|
+
|
|
15
|
+
# 手势参数
|
|
16
|
+
NAV_GESTURE_SPEED = 8000
|
|
17
|
+
DEFAULT_SWIPE_DURATION = 0.5
|
|
18
|
+
DEFAULT_DOUBLE_CLICK_INTERVAL = 0.15
|
|
19
|
+
DEFAULT_LONG_CLICK_INTERVAL = 1
|
|
20
|
+
|
|
21
|
+
# 内部常量
|
|
22
|
+
FULL_DRIVER_TMP_KEY = "_tmp_full_uidriver"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""显示器相关类型:DisplayRotation / DisplayInfo。"""
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import IntEnum
|
|
6
|
+
from typing import Tuple
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DisplayRotation(IntEnum):
|
|
10
|
+
"""屏幕旋转状态。"""
|
|
11
|
+
PORTRAIT = 0
|
|
12
|
+
LANDSCAPE = 1
|
|
13
|
+
REVERSE_PORTRAIT = 2
|
|
14
|
+
REVERSE_LANDSCAPE = 3
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class DisplayInfo:
|
|
19
|
+
"""显示器信息。"""
|
|
20
|
+
display_id: int
|
|
21
|
+
width: int
|
|
22
|
+
height: int
|
|
23
|
+
density: int
|
|
24
|
+
rotation: DisplayRotation = DisplayRotation.PORTRAIT
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def size(self) -> Tuple[int, int]:
|
|
28
|
+
return (self.width, self.height)
|
|
29
|
+
|
|
30
|
+
def is_portrait(self) -> bool:
|
|
31
|
+
return self.rotation in (DisplayRotation.PORTRAIT,
|
|
32
|
+
DisplayRotation.REVERSE_PORTRAIT)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""FormatString:带变量的模板字符串。
|
|
4
|
+
|
|
5
|
+
通过 __getattr__ 转发 template 字符串的方法,使其可像 str 一样使用。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FormatString:
|
|
10
|
+
"""格式化字符串(带变量的模板字符串)。"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, template: str, **kwargs):
|
|
13
|
+
self.template = template
|
|
14
|
+
self.variables = kwargs
|
|
15
|
+
|
|
16
|
+
def __getattr__(self, item):
|
|
17
|
+
# 转发 template 字符串的方法
|
|
18
|
+
return getattr(self.template, item)
|
|
19
|
+
|
|
20
|
+
def __str__(self):
|
|
21
|
+
return self.template
|
|
22
|
+
|
|
23
|
+
def __repr__(self):
|
|
24
|
+
return self.template
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""输入设备与手势类型:InputDevice / MouseButton / GestureStep /
|
|
4
|
+
TouchPadSwipeOptions / GestureAction。"""
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import IntEnum
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InputDevice(IntEnum):
|
|
11
|
+
"""输入设备类型。"""
|
|
12
|
+
TOUCH = 0
|
|
13
|
+
MOUSE = 1
|
|
14
|
+
PEN = 2
|
|
15
|
+
KNUCKLE = 3
|
|
16
|
+
TOUCHPAD = 4
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MouseButton(IntEnum):
|
|
20
|
+
"""鼠标按键。"""
|
|
21
|
+
LEFT = 0
|
|
22
|
+
RIGHT = 1
|
|
23
|
+
MIDDLE = 2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class GestureStep:
|
|
28
|
+
"""手势步骤。"""
|
|
29
|
+
action: str
|
|
30
|
+
x: int
|
|
31
|
+
y: int
|
|
32
|
+
duration: float = 0.0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class TouchPadSwipeOptions:
|
|
37
|
+
"""触控板滑动选项。"""
|
|
38
|
+
speed: Optional[int] = None
|
|
39
|
+
step_length: Optional[int] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class GestureAction:
|
|
44
|
+
"""手势动作。"""
|
|
45
|
+
steps: List[GestureStep] = field(default_factory=list)
|
|
46
|
+
input_device: InputDevice = InputDevice.TOUCH
|
|
47
|
+
|
|
48
|
+
def add_step(self, action: str, x: int, y: int, duration: float = 0.0):
|
|
49
|
+
"""添加手势步骤。"""
|
|
50
|
+
self.steps.append(GestureStep(action=action, x=x, y=y, duration=duration))
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""JsonBase:JSON 序列化基类。
|
|
4
|
+
|
|
5
|
+
过滤 None 值、支持 api_level 参数、不使用 @dataclass,确保 RPC 协议兼容性。
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any, Dict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class JsonBase:
|
|
12
|
+
"""JSON 序列化基类。"""
|
|
13
|
+
|
|
14
|
+
def to_dict(self, api_level: int = 9) -> Dict[str, Any]:
|
|
15
|
+
"""转换为字典(过滤 None 值)。
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
api_level: API 版本级别,用于版本兼容。
|
|
19
|
+
"""
|
|
20
|
+
result = {}
|
|
21
|
+
for key, value in self.__dict__.items():
|
|
22
|
+
if value is not None:
|
|
23
|
+
result[key] = value
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
def to_json(self) -> str:
|
|
27
|
+
"""转换为 JSON 字符串。"""
|
|
28
|
+
return json.dumps(self.to_dict(), ensure_ascii=False,
|
|
29
|
+
separators=(',', ':'))
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_dict(cls, data: Dict[str, Any]):
|
|
33
|
+
"""从字典创建实例。"""
|
|
34
|
+
obj = cls.__new__(cls)
|
|
35
|
+
for key, value in data.items():
|
|
36
|
+
setattr(obj, key, value)
|
|
37
|
+
return obj
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_json(cls, json_str: str):
|
|
41
|
+
"""从 JSON 字符串创建实例。"""
|
|
42
|
+
return cls.from_dict(json.loads(json_str))
|