uiautodev 0.7.2__tar.gz → 0.9.0__tar.gz
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.
Potentially problematic release.
This version of uiautodev might be problematic. Click here for more details.
- {uiautodev-0.7.2 → uiautodev-0.9.0}/PKG-INFO +9 -3
- {uiautodev-0.7.2 → uiautodev-0.9.0}/README.md +8 -2
- {uiautodev-0.7.2 → uiautodev-0.9.0}/pyproject.toml +1 -1
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/__init__.py +1 -1
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/app.py +59 -11
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/cli.py +22 -2
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/driver/android.py +46 -2
- uiautodev-0.9.0/uiautodev/exceptions.py +26 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/model.py +4 -2
- uiautodev-0.9.0/uiautodev/remote/harmony_mjpeg.py +199 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/remote/scrcpy.py +12 -10
- uiautodev-0.9.0/uiautodev/router/android.py +42 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/router/device.py +2 -17
- uiautodev-0.7.2/uiautodev/exceptions.py +0 -32
- {uiautodev-0.7.2 → uiautodev-0.9.0}/LICENSE +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/__main__.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/appium_proxy.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/binaries/scrcpy_server.jar +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/case.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/command_proxy.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/command_types.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/common.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/driver/appium.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/driver/base_driver.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/driver/harmony.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/driver/ios.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/driver/mock.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/driver/testdata/layout.json +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/driver/udt/udt.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/provider.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/remote/android_input.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/remote/keycode.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/remote/touch_controller.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/router/xml.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/static/demo.html +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/utils/common.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/utils/envutils.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/utils/exceptions.py +0 -0
- {uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/utils/usbmux.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: uiautodev
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: Mobile UI Automation, include UI hierarchy inspector, script recorder
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: codeskyblue
|
|
@@ -38,15 +38,21 @@ Description-Content-Type: text/markdown
|
|
|
38
38
|
|
|
39
39
|
https://uiauto.dev
|
|
40
40
|
|
|
41
|
-
>
|
|
41
|
+
> In China visit: https://uiauto.devsleep.com
|
|
42
42
|
|
|
43
|
-
UI Inspector for Android and
|
|
43
|
+
UI Inspector for Android, iOS and Harmony help inspector element properties, and auto generate XPath, script.
|
|
44
44
|
|
|
45
45
|
# Install
|
|
46
46
|
```bash
|
|
47
47
|
pip install uiautodev
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
To enable Harmony support, run the following command to install its dependencies:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
uiautodev install-harmony
|
|
54
|
+
```
|
|
55
|
+
|
|
50
56
|
# Usage
|
|
51
57
|
```bash
|
|
52
58
|
Usage: uiauto.dev [OPTIONS] COMMAND [ARGS]...
|
|
@@ -4,15 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
https://uiauto.dev
|
|
6
6
|
|
|
7
|
-
>
|
|
7
|
+
> In China visit: https://uiauto.devsleep.com
|
|
8
8
|
|
|
9
|
-
UI Inspector for Android and
|
|
9
|
+
UI Inspector for Android, iOS and Harmony help inspector element properties, and auto generate XPath, script.
|
|
10
10
|
|
|
11
11
|
# Install
|
|
12
12
|
```bash
|
|
13
13
|
pip install uiautodev
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
To enable Harmony support, run the following command to install its dependencies:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
uiautodev install-harmony
|
|
20
|
+
```
|
|
21
|
+
|
|
16
22
|
# Usage
|
|
17
23
|
```bash
|
|
18
24
|
Usage: uiauto.dev [OPTIONS] COMMAND [ARGS]...
|
|
@@ -9,7 +9,7 @@ import os
|
|
|
9
9
|
import platform
|
|
10
10
|
import signal
|
|
11
11
|
from pathlib import Path
|
|
12
|
-
from typing import List
|
|
12
|
+
from typing import Dict, List
|
|
13
13
|
|
|
14
14
|
import adbutils
|
|
15
15
|
import uvicorn
|
|
@@ -25,6 +25,7 @@ from uiautodev.common import convert_bytes_to_image, get_webpage_url, ocr_image
|
|
|
25
25
|
from uiautodev.model import Node
|
|
26
26
|
from uiautodev.provider import AndroidProvider, HarmonyProvider, IOSProvider, MockProvider
|
|
27
27
|
from uiautodev.remote.scrcpy import ScrcpyServer
|
|
28
|
+
from uiautodev.router.android import router as android_device_router
|
|
28
29
|
from uiautodev.router.device import make_router
|
|
29
30
|
from uiautodev.router.xml import router as xml_router
|
|
30
31
|
from uiautodev.utils.envutils import Environment
|
|
@@ -68,6 +69,23 @@ else:
|
|
|
68
69
|
app.include_router(harmony_router, prefix="/api/harmony", tags=["harmony"])
|
|
69
70
|
|
|
70
71
|
app.include_router(xml_router, prefix="/api/xml", tags=["xml"])
|
|
72
|
+
app.include_router(android_device_router, prefix="/api/android", tags=["android"])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.get('/api/{platform}/features')
|
|
76
|
+
def get_features(platform: str) -> Dict[str, bool]:
|
|
77
|
+
"""Get features supported by the specified platform"""
|
|
78
|
+
features = {}
|
|
79
|
+
# 获取所有带有指定平台tag的路由
|
|
80
|
+
for route in app.routes:
|
|
81
|
+
if hasattr(route, 'tags') and platform in route.tags:
|
|
82
|
+
if route.path.startswith(f"/api/{platform}/{{serial}}/"):
|
|
83
|
+
# 提取特性名称
|
|
84
|
+
parts = route.path.split('/')
|
|
85
|
+
feature_name = parts[-1]
|
|
86
|
+
if not feature_name.startswith('{'):
|
|
87
|
+
features[feature_name] = True
|
|
88
|
+
return features
|
|
71
89
|
|
|
72
90
|
|
|
73
91
|
class InfoResponse(BaseModel):
|
|
@@ -123,15 +141,41 @@ def index_redirect():
|
|
|
123
141
|
return RedirectResponse(url)
|
|
124
142
|
|
|
125
143
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
144
|
+
@app.websocket("/ws/android/scrcpy/{serial}")
|
|
145
|
+
async def handle_android_ws(websocket: WebSocket, serial: str):
|
|
146
|
+
"""
|
|
147
|
+
Args:
|
|
148
|
+
serial: device serial
|
|
149
|
+
websocket: WebSocket
|
|
150
|
+
"""
|
|
151
|
+
await websocket.accept()
|
|
131
152
|
|
|
153
|
+
try:
|
|
154
|
+
logger.info(f"WebSocket serial: {serial}")
|
|
155
|
+
device = adbutils.device(serial)
|
|
156
|
+
server = ScrcpyServer(device)
|
|
157
|
+
await server.handle_unified_websocket(websocket, serial)
|
|
158
|
+
except WebSocketDisconnect:
|
|
159
|
+
logger.info(f"WebSocket disconnected by client.")
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.exception(f"WebSocket error for serial={serial}: {e}")
|
|
162
|
+
await websocket.close(code=1000, reason=str(e))
|
|
163
|
+
finally:
|
|
164
|
+
logger.info(f"WebSocket closed for serial={serial}")
|
|
132
165
|
|
|
133
|
-
|
|
134
|
-
|
|
166
|
+
|
|
167
|
+
def get_harmony_mjpeg_server(serial: str):
|
|
168
|
+
from hypium import UiDriver
|
|
169
|
+
|
|
170
|
+
from uiautodev.remote.harmony_mjpeg import HarmonyMjpegServer
|
|
171
|
+
driver = UiDriver.connect(device_sn=serial)
|
|
172
|
+
logger.info("create harmony mjpeg server for %s", serial)
|
|
173
|
+
logger.info(f'device wake_up_display: {driver.wake_up_display()}')
|
|
174
|
+
return HarmonyMjpegServer(driver)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.websocket("/ws/harmony/mjpeg/{serial}")
|
|
178
|
+
async def unified_harmony_ws(websocket: WebSocket, serial: str):
|
|
135
179
|
"""
|
|
136
180
|
Args:
|
|
137
181
|
serial: device serial
|
|
@@ -142,9 +186,13 @@ async def unified_ws(websocket: WebSocket, serial: str):
|
|
|
142
186
|
try:
|
|
143
187
|
logger.info(f"WebSocket serial: {serial}")
|
|
144
188
|
|
|
145
|
-
# 获取
|
|
146
|
-
server =
|
|
147
|
-
|
|
189
|
+
# 获取 HarmonyScrcpyServer 实例
|
|
190
|
+
server = get_harmony_mjpeg_server(serial)
|
|
191
|
+
server.start()
|
|
192
|
+
await server.handle_ws(websocket)
|
|
193
|
+
except ImportError as e:
|
|
194
|
+
logger.error(f"missing library for harmony: {e}")
|
|
195
|
+
await websocket.close(code=1000, reason="missing library, fix by \"pip install uiautodev[harmony]\"")
|
|
148
196
|
except WebSocketDisconnect:
|
|
149
197
|
logger.info(f"WebSocket disconnected by client.")
|
|
150
198
|
except Exception as e:
|
|
@@ -19,6 +19,7 @@ import click
|
|
|
19
19
|
import httpx
|
|
20
20
|
import pydantic
|
|
21
21
|
import uvicorn
|
|
22
|
+
from retry import retry
|
|
22
23
|
|
|
23
24
|
from uiautodev import __version__, command_proxy
|
|
24
25
|
from uiautodev.command_types import Command
|
|
@@ -29,7 +30,13 @@ from uiautodev.utils.common import convert_params_to_model, print_json
|
|
|
29
30
|
logger = logging.getLogger(__name__)
|
|
30
31
|
|
|
31
32
|
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
|
|
32
|
-
|
|
33
|
+
HARMONY_PACKAGES = [
|
|
34
|
+
"setuptools",
|
|
35
|
+
"https://public.uiauto.devsleep.com/harmony/xdevice-5.0.7.200.tar.gz",
|
|
36
|
+
"https://public.uiauto.devsleep.com/harmony/xdevice-devicetest-5.0.7.200.tar.gz",
|
|
37
|
+
"https://public.uiauto.devsleep.com/harmony/xdevice-ohos-5.0.7.200.tar.gz",
|
|
38
|
+
"https://public.uiauto.devsleep.com/harmony/hypium-5.0.7.200.tar.gz",
|
|
39
|
+
]
|
|
33
40
|
|
|
34
41
|
@click.group(context_settings=CONTEXT_SETTINGS)
|
|
35
42
|
@click.option("--verbose", "-v", is_flag=True, default=False, help="verbose mode")
|
|
@@ -116,6 +123,19 @@ def self_update():
|
|
|
116
123
|
subprocess.run([sys.executable, '-m', "pip", "install", "--upgrade", "uiautodev"])
|
|
117
124
|
|
|
118
125
|
|
|
126
|
+
@cli.command('install-harmony')
|
|
127
|
+
def install_harmony():
|
|
128
|
+
for lib_url in HARMONY_PACKAGES:
|
|
129
|
+
click.echo(f"Installing {lib_url} ...")
|
|
130
|
+
pip_install(lib_url)
|
|
131
|
+
|
|
132
|
+
@retry(tries=2, delay=3, backoff=2)
|
|
133
|
+
def pip_install(package: str):
|
|
134
|
+
"""Install a package using pip."""
|
|
135
|
+
subprocess.run([sys.executable, '-m', "pip", "install", package], check=True)
|
|
136
|
+
click.echo(f"Successfully installed {package}")
|
|
137
|
+
|
|
138
|
+
|
|
119
139
|
@cli.command(help="start uiauto.dev local server [Default]")
|
|
120
140
|
@click.option("--port", default=20242, help="port number", show_default=True)
|
|
121
141
|
@click.option("--host", default="127.0.0.1", help="host", show_default=True)
|
|
@@ -123,7 +143,7 @@ def self_update():
|
|
|
123
143
|
@click.option("-f", "--force", is_flag=True, default=False, help="shutdown alrealy runningserver")
|
|
124
144
|
@click.option("-s", "--no-browser", is_flag=True, default=False, help="silent mode, do not open browser")
|
|
125
145
|
def server(port: int, host: str, reload: bool, force: bool, no_browser: bool):
|
|
126
|
-
|
|
146
|
+
click.echo(f"uiautodev version: {__version__}")
|
|
127
147
|
if force:
|
|
128
148
|
try:
|
|
129
149
|
httpx.get(f"http://{host}:{port}/shutdown", timeout=3)
|
|
@@ -31,6 +31,15 @@ class AndroidDriver(BaseDriver):
|
|
|
31
31
|
@cached_property
|
|
32
32
|
def ud(self) -> u2.Device:
|
|
33
33
|
return u2.connect_usb(self.serial)
|
|
34
|
+
|
|
35
|
+
def get_current_activity(self) -> str:
|
|
36
|
+
ret = self.adb_device.shell2(["dumpsys", "activity", "activities"], rstrip=True, timeout=5)
|
|
37
|
+
# 使用正则查找包含前台 activity 的行
|
|
38
|
+
match = re.search(r"mResumedActivity:.*? ([\w\.]+\/[\w\.]+)", ret.output)
|
|
39
|
+
if match:
|
|
40
|
+
return match.group(1) # 返回包名/类名,例如 com.example/.MainActivity
|
|
41
|
+
else:
|
|
42
|
+
return ""
|
|
34
43
|
|
|
35
44
|
def screenshot(self, id: int) -> Image.Image:
|
|
36
45
|
if id > 0:
|
|
@@ -117,13 +126,48 @@ class AndroidDriver(BaseDriver):
|
|
|
117
126
|
|
|
118
127
|
def volume_mute(self):
|
|
119
128
|
self.adb_device.keyevent("VOLUME_MUTE")
|
|
120
|
-
|
|
129
|
+
|
|
130
|
+
def get_app_version(self, package_name: str) -> Optional[dict]:
|
|
131
|
+
"""
|
|
132
|
+
Get the version information of an app, including mainVersion and subVersion.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
package_name (str): The package name of the app.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
dict: A dictionary containing mainVersion and subVersion.
|
|
139
|
+
"""
|
|
140
|
+
output = self.adb_device.shell(["dumpsys", "package", package_name])
|
|
141
|
+
|
|
142
|
+
# versionName
|
|
143
|
+
m = re.search(r"versionName=(?P<name>[^\s]+)", output)
|
|
144
|
+
version_name = m.group("name") if m else ""
|
|
145
|
+
if version_name == "null": # Java dumps "null" for null values
|
|
146
|
+
version_name = None
|
|
147
|
+
|
|
148
|
+
# versionCode
|
|
149
|
+
m = re.search(r"versionCode=(?P<code>\d+)", output)
|
|
150
|
+
version_code = m.group("code") if m else ""
|
|
151
|
+
version_code = int(version_code) if version_code.isdigit() else None
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
"versionName": version_name,
|
|
155
|
+
"versionCode": version_code
|
|
156
|
+
}
|
|
157
|
+
|
|
121
158
|
def app_list(self) -> List[AppInfo]:
|
|
122
159
|
results = []
|
|
123
160
|
output = self.adb_device.shell(["pm", "list", "packages", '-3'])
|
|
124
161
|
for m in re.finditer(r"^package:([^\s]+)\r?$", output, re.M):
|
|
125
162
|
packageName = m.group(1)
|
|
126
|
-
|
|
163
|
+
# get version
|
|
164
|
+
version_info = self.get_app_version(packageName)
|
|
165
|
+
app_info = AppInfo(
|
|
166
|
+
packageName=packageName,
|
|
167
|
+
versionName=version_info.get("versionName"),
|
|
168
|
+
versionCode=version_info.get("versionCode")
|
|
169
|
+
)
|
|
170
|
+
results.append(app_info)
|
|
127
171
|
return results
|
|
128
172
|
|
|
129
173
|
def open_app_file(self, package: str) -> Iterator[bytes]:
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""Created on Tue Mar 05 2024 11:16:29 by codeskyblue
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
class UiautoException(Exception):
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DriverException(UiautoException):
|
|
12
|
+
"""Base class for all driver-related exceptions."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
class IOSDriverException(DriverException): ...
|
|
16
|
+
class AndroidDriverException(DriverException): ...
|
|
17
|
+
class HarmonyDriverException(DriverException): ...
|
|
18
|
+
class AppiumDriverException(DriverException): ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MethodError(UiautoException):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ElementNotFoundError(MethodError): ...
|
|
26
|
+
class RequestError(UiautoException): ...
|
|
@@ -33,7 +33,7 @@ class Rect(BaseModel):
|
|
|
33
33
|
|
|
34
34
|
class Node(BaseModel):
|
|
35
35
|
key: str
|
|
36
|
-
name: str
|
|
36
|
+
name: str # can be seen as description
|
|
37
37
|
bounds: Optional[Tuple[float, float, float, float]] = None
|
|
38
38
|
rect: Optional[Rect] = None
|
|
39
39
|
properties: Dict[str, Union[str, bool]] = {}
|
|
@@ -50,4 +50,6 @@ class WindowSize(typing.NamedTuple):
|
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
class AppInfo(BaseModel):
|
|
53
|
-
packageName: str
|
|
53
|
+
packageName: str
|
|
54
|
+
versionName: Optional[str] = None # Allow None values
|
|
55
|
+
versionCode: Optional[int] = None
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import socket
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from threading import Thread
|
|
7
|
+
|
|
8
|
+
from fastapi import WebSocket
|
|
9
|
+
from hypium import KeyCode
|
|
10
|
+
|
|
11
|
+
from uiautodev.exceptions import HarmonyDriverException
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HarmonyMjpegServer:
|
|
17
|
+
"""
|
|
18
|
+
HarmonyMjpegServer is responsible for handling screen streaming functionality
|
|
19
|
+
for HarmonyOS devices that support ABC proxy (a communication interface).
|
|
20
|
+
|
|
21
|
+
It manages WebSocket clients, communicates with the ABC server over gRPC, and streams
|
|
22
|
+
the device's screen data in real-time to connected clients.
|
|
23
|
+
|
|
24
|
+
This server is specifically designed for devices running in 'abc mode' and requires that
|
|
25
|
+
the target device expose an `abc_proxy` attribute for communication.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
device: The HarmonyOS device object.
|
|
29
|
+
driver: The controlling driver which may wrap the device.
|
|
30
|
+
abc_rpc_addr: Tuple containing the IP and port used to communicate with abc_proxy.
|
|
31
|
+
channel: The gRPC communication channel (initialized later).
|
|
32
|
+
clients: A set of connected WebSocket clients.
|
|
33
|
+
loop: Asyncio event loop used to run asynchronous tasks.
|
|
34
|
+
is_running: Boolean flag indicating if the streaming service is active.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
RuntimeError: If the connected device does not support abc_proxy.
|
|
38
|
+
|
|
39
|
+
References:
|
|
40
|
+
- Huawei HarmonyOS Python Guidelines:
|
|
41
|
+
https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/hypium-python-guidelines
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, driver):
|
|
45
|
+
if hasattr(driver, "_device"):
|
|
46
|
+
device = driver._device
|
|
47
|
+
else:
|
|
48
|
+
device = driver
|
|
49
|
+
logger.info(f'device: {device}')
|
|
50
|
+
if not hasattr(device, "abc_proxy") or device.abc_proxy is None:
|
|
51
|
+
raise HarmonyDriverException("Only abc mode can support screen recorder")
|
|
52
|
+
self.device = device
|
|
53
|
+
self.driver = driver
|
|
54
|
+
self.abc_rpc_addr = ("127.0.0.1", device.abc_proxy.port)
|
|
55
|
+
self.channel = None
|
|
56
|
+
self.clients = set()
|
|
57
|
+
self.loop = asyncio.get_event_loop()
|
|
58
|
+
self.is_running = False
|
|
59
|
+
|
|
60
|
+
def connect(self):
|
|
61
|
+
self.channel = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
62
|
+
self.channel.connect(self.abc_rpc_addr)
|
|
63
|
+
|
|
64
|
+
def start(self, timeout=3600):
|
|
65
|
+
if self.channel is None:
|
|
66
|
+
self.connect()
|
|
67
|
+
self.is_running = True
|
|
68
|
+
self.timeout = timeout
|
|
69
|
+
self.stop_capture_if_running()
|
|
70
|
+
msg_json = {'api': "startCaptureScreen", 'args': []}
|
|
71
|
+
full_msg = {
|
|
72
|
+
"module": "com.ohos.devicetest.hypiumApiHelper",
|
|
73
|
+
"method": "Captures",
|
|
74
|
+
"params": msg_json,
|
|
75
|
+
"request_id": datetime.now().strftime("%Y%m%d%H%M%S%f")
|
|
76
|
+
}
|
|
77
|
+
full_msg_str = json.dumps(full_msg, ensure_ascii=False, separators=(',', ':'))
|
|
78
|
+
self.channel.sendall(full_msg_str.encode("utf-8") + b'\n')
|
|
79
|
+
reply = self.channel.recv(1024)
|
|
80
|
+
logger.info(f'reply: {reply}')
|
|
81
|
+
if b"true" in reply:
|
|
82
|
+
thread_record = Thread(target=self._record_worker)
|
|
83
|
+
thread_record.start()
|
|
84
|
+
else:
|
|
85
|
+
raise RuntimeError("Fail to start screen capture")
|
|
86
|
+
|
|
87
|
+
def stop_capture_if_running(self):
|
|
88
|
+
msg_json = {'api': "stopCaptureScreen", 'args': []}
|
|
89
|
+
full_msg = {
|
|
90
|
+
"module": "com.ohos.devicetest.hypiumApiHelper",
|
|
91
|
+
"method": "Captures",
|
|
92
|
+
"params": msg_json,
|
|
93
|
+
"request_id": datetime.now().strftime("%Y%m%d%H%M%S%f")
|
|
94
|
+
}
|
|
95
|
+
full_msg_str = json.dumps(full_msg, ensure_ascii=False, separators=(',', ':'))
|
|
96
|
+
self.channel.sendall(full_msg_str.encode("utf-8") + b'\n')
|
|
97
|
+
reply = self.channel.recv(1024)
|
|
98
|
+
logger.info(f'stop reply: {reply}')
|
|
99
|
+
|
|
100
|
+
async def handle_ws(self, websocket: WebSocket):
|
|
101
|
+
self.clients.add(websocket)
|
|
102
|
+
serial = getattr(self.device, "device_sn", "unknown")
|
|
103
|
+
logger.info(f"[{serial}] WebSocket connected")
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
while True:
|
|
107
|
+
message = await websocket.receive_text()
|
|
108
|
+
logger.info(f"Received message: {message}")
|
|
109
|
+
try:
|
|
110
|
+
data = json.loads(message)
|
|
111
|
+
if data.get('type') == 'touch':
|
|
112
|
+
action = data.get('action')
|
|
113
|
+
x, y = data.get('x'), data.get('y')
|
|
114
|
+
if action == 'normal':
|
|
115
|
+
self.driver.touch((x, y))
|
|
116
|
+
elif action == 'long':
|
|
117
|
+
self.driver.touch(target=(x, y), mode='long')
|
|
118
|
+
elif action == 'double':
|
|
119
|
+
self.driver.touch(target=(x, y), mode='double')
|
|
120
|
+
elif action == 'move':
|
|
121
|
+
self.driver.slide(
|
|
122
|
+
start=(data.get('x1'), data.get('y1')),
|
|
123
|
+
end=(data.get('x2'), data.get('y2')),
|
|
124
|
+
slide_time=0.1
|
|
125
|
+
)
|
|
126
|
+
elif data.get('type') == 'keyEvent':
|
|
127
|
+
event_number = data['eventNumber']
|
|
128
|
+
if event_number == 187:
|
|
129
|
+
self.driver.swipe_to_recent_task()
|
|
130
|
+
elif event_number == 3:
|
|
131
|
+
self.driver.go_home()
|
|
132
|
+
elif event_number == 4:
|
|
133
|
+
self.driver.go_back()
|
|
134
|
+
elif event_number == 224:
|
|
135
|
+
self.driver.wake_up_display()
|
|
136
|
+
elif data.get('type') == 'text':
|
|
137
|
+
detail = data.get('detail')
|
|
138
|
+
if detail == 'CODE_AC_BACK':
|
|
139
|
+
self.driver.press_key(KeyCode.DEL)
|
|
140
|
+
elif detail == 'CODE_AC_ENTER':
|
|
141
|
+
self.driver.press_key(KeyCode.ENTER)
|
|
142
|
+
else:
|
|
143
|
+
self.driver.shell(
|
|
144
|
+
f"uitest uiInput inputText {data.get('x')} {data.get('y')} {detail}")
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.warning(f"Failed to handle message: {e}")
|
|
147
|
+
except Exception as e:
|
|
148
|
+
logger.info(f"WebSocket closed: {e}")
|
|
149
|
+
finally:
|
|
150
|
+
self.clients.discard(websocket)
|
|
151
|
+
|
|
152
|
+
def _record_worker(self):
|
|
153
|
+
tmp_data = b''
|
|
154
|
+
start_flag = b'\xff\xd8'
|
|
155
|
+
end_flag = b'\xff\xd9'
|
|
156
|
+
while self.is_running:
|
|
157
|
+
try:
|
|
158
|
+
result = self.channel.recv(4096 * 1024)
|
|
159
|
+
tmp_data += result
|
|
160
|
+
while start_flag in tmp_data and end_flag in tmp_data:
|
|
161
|
+
start_index = tmp_data.index(start_flag)
|
|
162
|
+
end_index = tmp_data.index(end_flag) + 2
|
|
163
|
+
frame = tmp_data[start_index:end_index]
|
|
164
|
+
tmp_data = tmp_data[end_index:]
|
|
165
|
+
asyncio.run_coroutine_threadsafe(self._broadcast(frame), self.loop)
|
|
166
|
+
except Exception as e:
|
|
167
|
+
logger.warning(f"Record worker error: {e}")
|
|
168
|
+
self.is_running = False
|
|
169
|
+
self.channel = None
|
|
170
|
+
break
|
|
171
|
+
|
|
172
|
+
async def _broadcast(self, data):
|
|
173
|
+
for client in self.clients.copy():
|
|
174
|
+
try:
|
|
175
|
+
await client.send_bytes(data)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
logger.info(f"Send error, removing client: {e}")
|
|
178
|
+
self.clients.discard(client)
|
|
179
|
+
|
|
180
|
+
def stop(self):
|
|
181
|
+
self.is_running = False
|
|
182
|
+
if self.channel is None:
|
|
183
|
+
return
|
|
184
|
+
msg_json = {'api': "stopCaptureScreen", 'args': []}
|
|
185
|
+
full_msg = {
|
|
186
|
+
"module": "com.ohos.devicetest.hypiumApiHelper",
|
|
187
|
+
"method": "Captures",
|
|
188
|
+
"params": msg_json,
|
|
189
|
+
"request_id": datetime.now().strftime("%Y%m%d%H%M%S%f")
|
|
190
|
+
}
|
|
191
|
+
full_msg_str = json.dumps(full_msg, ensure_ascii=False, separators=(',', ':'))
|
|
192
|
+
self.channel.sendall(full_msg_str.encode("utf-8") + b'\n')
|
|
193
|
+
reply = self.channel.recv(1024)
|
|
194
|
+
if b"true" not in reply:
|
|
195
|
+
logger.info("Fail to stop capture")
|
|
196
|
+
self.channel.close()
|
|
197
|
+
self.channel = None
|
|
198
|
+
for client in self.clients:
|
|
199
|
+
asyncio.run_coroutine_threadsafe(client.close(), self.loop)
|
|
@@ -16,6 +16,7 @@ from uiautodev.remote.touch_controller import ScrcpyTouchController
|
|
|
16
16
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
|
+
|
|
19
20
|
class ScrcpyServer:
|
|
20
21
|
"""
|
|
21
22
|
ScrcpyServer class is responsible for managing the scrcpy server on Android devices.
|
|
@@ -30,17 +31,18 @@ class ScrcpyServer:
|
|
|
30
31
|
Args:
|
|
31
32
|
scrcpy_jar_path (str, optional): Path to the scrcpy server JAR file. Defaults to None.
|
|
32
33
|
"""
|
|
33
|
-
self.scrcpy_jar_path = scrcpy_jar_path or os.path.join(os.path.dirname(__file__),
|
|
34
|
+
self.scrcpy_jar_path = scrcpy_jar_path or os.path.join(os.path.dirname(__file__),
|
|
35
|
+
'../binaries/scrcpy_server.jar')
|
|
34
36
|
self.device = device
|
|
35
37
|
self.resolution_width = 0 # scrcpy 投屏转换宽度
|
|
36
38
|
self.resolution_height = 0 # scrcpy 投屏转换高度
|
|
37
|
-
|
|
39
|
+
|
|
38
40
|
self._shell_conn: AdbConnection
|
|
39
41
|
self._video_conn: socket.socket
|
|
40
42
|
self._control_conn: socket.socket
|
|
41
43
|
|
|
42
44
|
self._setup_connection()
|
|
43
|
-
|
|
45
|
+
|
|
44
46
|
def _setup_connection(self):
|
|
45
47
|
self._shell_conn = self._start_scrcpy_server(control=True)
|
|
46
48
|
self._video_conn = self._connect_scrcpy(self.device)
|
|
@@ -73,7 +75,7 @@ class ScrcpyServer:
|
|
|
73
75
|
self._shell_conn.close()
|
|
74
76
|
except:
|
|
75
77
|
pass
|
|
76
|
-
|
|
78
|
+
|
|
77
79
|
def __del__(self):
|
|
78
80
|
self.close()
|
|
79
81
|
|
|
@@ -108,14 +110,14 @@ class ScrcpyServer:
|
|
|
108
110
|
)
|
|
109
111
|
conn = device.shell(start_command, stream=True)
|
|
110
112
|
logger.debug("scrcpy output: %s", conn.conn.recv(100))
|
|
111
|
-
return conn
|
|
113
|
+
return conn # type: ignore
|
|
112
114
|
|
|
113
115
|
async def handle_unified_websocket(self, websocket: WebSocket, serial=''):
|
|
114
116
|
logger.info(f"[Unified] WebSocket connection from {websocket} for serial: {serial}")
|
|
115
117
|
|
|
116
118
|
video_task = asyncio.create_task(self._stream_video_to_websocket(self._video_conn, websocket))
|
|
117
|
-
control_task
|
|
118
|
-
|
|
119
|
+
control_task = asyncio.create_task(self._handle_control_websocket(websocket))
|
|
120
|
+
|
|
119
121
|
try:
|
|
120
122
|
# 不使用 return_exceptions=True,让异常能够正确传播
|
|
121
123
|
await asyncio.gather(video_task, control_task)
|
|
@@ -129,14 +131,14 @@ class ScrcpyServer:
|
|
|
129
131
|
async def _stream_video_to_websocket(self, conn: socket.socket, ws: WebSocket):
|
|
130
132
|
# Set socket to non-blocking mode
|
|
131
133
|
conn.setblocking(False)
|
|
132
|
-
|
|
134
|
+
|
|
133
135
|
while True:
|
|
134
136
|
# check if ws closed
|
|
135
137
|
if ws.client_state.name != "CONNECTED":
|
|
136
138
|
logger.info('WebSocket no longer connected. Exiting video stream.')
|
|
137
139
|
break
|
|
138
140
|
# Use asyncio to read data asynchronously
|
|
139
|
-
data = await asyncio.get_event_loop().sock_recv(conn, 1024*1024)
|
|
141
|
+
data = await asyncio.get_event_loop().sock_recv(conn, 1024 * 1024)
|
|
140
142
|
if not data:
|
|
141
143
|
logger.warning('No data received, connection may be closed.')
|
|
142
144
|
raise ConnectionError("Video stream ended unexpectedly")
|
|
@@ -174,4 +176,4 @@ class ScrcpyServer:
|
|
|
174
176
|
await ws.send_text(json.dumps({"type": "pong"}))
|
|
175
177
|
except json.JSONDecodeError as e:
|
|
176
178
|
logger.error(f"Invalid JSON message: {e}")
|
|
177
|
-
continue
|
|
179
|
+
continue
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# prefix for /api/android/{serial}/shell
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Dict, Optional
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, Request, Response
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from uiautodev.driver.android import AndroidDriver
|
|
10
|
+
from uiautodev.model import ShellResponse
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
router = APIRouter()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AndroidShellPayload(BaseModel):
|
|
18
|
+
command: str
|
|
19
|
+
|
|
20
|
+
@router.post("/{serial}/shell")
|
|
21
|
+
def shell(serial: str, payload: AndroidShellPayload) -> ShellResponse:
|
|
22
|
+
"""Run a shell command on an Android device"""
|
|
23
|
+
try:
|
|
24
|
+
driver = AndroidDriver(serial)
|
|
25
|
+
return driver.shell(payload.command)
|
|
26
|
+
except NotImplementedError as e:
|
|
27
|
+
return Response(content="shell not implemented", media_type="text/plain", status_code=501)
|
|
28
|
+
except Exception as e:
|
|
29
|
+
logger.exception("shell failed")
|
|
30
|
+
return ShellResponse(output="", error=str(e))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@router.get("/{serial}/current_activity")
|
|
34
|
+
async def get_current_activity(serial: str) -> Response:
|
|
35
|
+
"""Get the current activity of the Android device"""
|
|
36
|
+
try:
|
|
37
|
+
driver = AndroidDriver(serial)
|
|
38
|
+
activity = driver.get_current_activity()
|
|
39
|
+
return Response(content=activity, media_type="text/plain")
|
|
40
|
+
except Exception as e:
|
|
41
|
+
logger.exception("get_current_activity failed")
|
|
42
|
+
return Response(content="", media_type="text/plain")
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import io
|
|
8
8
|
import logging
|
|
9
|
-
from typing import Any, Dict, List
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
10
|
|
|
11
|
-
from fastapi import APIRouter, Response
|
|
11
|
+
from fastapi import APIRouter, Query, Request, Response
|
|
12
12
|
from fastapi.responses import StreamingResponse
|
|
13
13
|
from pydantic import BaseModel
|
|
14
14
|
|
|
@@ -19,9 +19,6 @@ from uiautodev.provider import BaseProvider
|
|
|
19
19
|
|
|
20
20
|
logger = logging.getLogger(__name__)
|
|
21
21
|
|
|
22
|
-
class AndroidShellPayload(BaseModel):
|
|
23
|
-
command: str
|
|
24
|
-
|
|
25
22
|
|
|
26
23
|
def make_router(provider: BaseProvider) -> APIRouter:
|
|
27
24
|
router = APIRouter()
|
|
@@ -37,18 +34,6 @@ def make_router(provider: BaseProvider) -> APIRouter:
|
|
|
37
34
|
logger.exception("list_devices failed")
|
|
38
35
|
return Response(content=str(e), media_type="text/plain", status_code=500)
|
|
39
36
|
|
|
40
|
-
@router.post("/{serial}/shell")
|
|
41
|
-
def android_shell(serial: str, payload: AndroidShellPayload) -> ShellResponse:
|
|
42
|
-
"""Run a shell command on an Android device"""
|
|
43
|
-
try:
|
|
44
|
-
driver = provider.get_device_driver(serial)
|
|
45
|
-
return driver.shell(payload.command)
|
|
46
|
-
except NotImplementedError as e:
|
|
47
|
-
return Response(content="shell not implemented", media_type="text/plain", status_code=501)
|
|
48
|
-
except Exception as e:
|
|
49
|
-
logger.exception("shell failed")
|
|
50
|
-
return ShellResponse(output="", error=str(e))
|
|
51
|
-
|
|
52
37
|
@router.get(
|
|
53
38
|
"/{serial}/screenshot/{id}",
|
|
54
39
|
responses={200: {"content": {"image/jpeg": {}}}},
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
|
|
4
|
-
"""Created on Tue Mar 05 2024 11:16:29 by codeskyblue
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
class UiautoException(Exception):
|
|
8
|
-
pass
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class IOSDriverException(UiautoException):
|
|
12
|
-
pass
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class AndroidDriverException(UiautoException):
|
|
16
|
-
pass
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class AppiumDriverException(UiautoException):
|
|
20
|
-
pass
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class MethodError(UiautoException):
|
|
24
|
-
pass
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class ElementNotFoundError(MethodError):
|
|
28
|
-
pass
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class RequestError(UiautoException):
|
|
32
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{uiautodev-0.7.2 → uiautodev-0.9.0}/uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|