uiautodev 0.8.0__tar.gz → 0.10.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.

Files changed (41) hide show
  1. {uiautodev-0.8.0 → uiautodev-0.10.0}/PKG-INFO +11 -28
  2. {uiautodev-0.8.0 → uiautodev-0.10.0}/README.md +9 -26
  3. {uiautodev-0.8.0 → uiautodev-0.10.0}/pyproject.toml +2 -2
  4. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/__init__.py +1 -1
  5. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/app.py +43 -11
  6. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/cli.py +22 -2
  7. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/driver/android.py +37 -2
  8. uiautodev-0.10.0/uiautodev/exceptions.py +26 -0
  9. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/model.py +4 -2
  10. uiautodev-0.10.0/uiautodev/remote/harmony_mjpeg.py +199 -0
  11. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/remote/scrcpy.py +12 -10
  12. uiautodev-0.10.0/uiautodev/router/proxy.py +58 -0
  13. uiautodev-0.8.0/uiautodev/exceptions.py +0 -32
  14. {uiautodev-0.8.0 → uiautodev-0.10.0}/LICENSE +0 -0
  15. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/__main__.py +0 -0
  16. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/appium_proxy.py +0 -0
  17. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/binaries/scrcpy_server.jar +0 -0
  18. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/case.py +0 -0
  19. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/command_proxy.py +0 -0
  20. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/command_types.py +0 -0
  21. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/common.py +0 -0
  22. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/driver/appium.py +0 -0
  23. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/driver/base_driver.py +0 -0
  24. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/driver/harmony.py +0 -0
  25. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/driver/ios.py +0 -0
  26. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/driver/mock.py +0 -0
  27. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/driver/testdata/layout.json +0 -0
  28. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk +0 -0
  29. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/driver/udt/udt.py +0 -0
  30. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/provider.py +0 -0
  31. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/remote/android_input.py +0 -0
  32. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/remote/keycode.py +0 -0
  33. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/remote/touch_controller.py +0 -0
  34. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/router/android.py +0 -0
  35. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/router/device.py +0 -0
  36. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/router/xml.py +0 -0
  37. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/static/demo.html +0 -0
  38. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/utils/common.py +0 -0
  39. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/utils/envutils.py +0 -0
  40. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/utils/exceptions.py +0 -0
  41. {uiautodev-0.8.0 → uiautodev-0.10.0}/uiautodev/utils/usbmux.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uiautodev
3
- Version: 0.8.0
3
+ Version: 0.10.0
4
4
  Summary: Mobile UI Automation, include UI hierarchy inspector, script recorder
5
5
  License: MIT
6
6
  Author: codeskyblue
@@ -18,7 +18,7 @@ Requires-Dist: Pillow
18
18
  Requires-Dist: adbutils (>=2.8.10,<3)
19
19
  Requires-Dist: click (>=8.1.7,<9.0.0)
20
20
  Requires-Dist: construct
21
- Requires-Dist: fastapi (==0.115.12)
21
+ Requires-Dist: fastapi (>=0.115.12,<1)
22
22
  Requires-Dist: httpx
23
23
  Requires-Dist: lxml
24
24
  Requires-Dist: pydantic (>=2.6,<3.0)
@@ -38,15 +38,21 @@ Description-Content-Type: text/markdown
38
38
 
39
39
  https://uiauto.dev
40
40
 
41
- > backup site: https://uiauto.devsleep.com
41
+ > In China visit: https://uiauto.devsleep.com
42
42
 
43
- UI Inspector for Android and iOS, help inspector element properties, and auto generate XPath, script.
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]...
@@ -70,31 +76,8 @@ uiauto.dev
70
76
  ```
71
77
 
72
78
  # DEVELOP
73
- ```bash
74
- # install poetry (python package manager)
75
- pip install poetry # pipx install poetry
76
-
77
- # install deps
78
- poetry install
79
-
80
- # format import
81
- make format
82
79
 
83
- # run server
84
- make dev
85
-
86
- # If you encounter the error NameError: name 'int2byte' is not defined,
87
- # try installing a stable version of the construct package to resolve it:
88
- # and restart: make dev
89
- pip install construct==2.9.45
90
-
91
- ```
92
-
93
- 运行测试
94
-
95
- ```sh
96
- make test
97
- ```
80
+ see [DEVELOP.md](DEVELOP.md)
98
81
 
99
82
  # Links
100
83
  - https://app.tangoapp.dev/ 基于webadb的手机远程控制项目
@@ -4,15 +4,21 @@
4
4
 
5
5
  https://uiauto.dev
6
6
 
7
- > backup site: https://uiauto.devsleep.com
7
+ > In China visit: https://uiauto.devsleep.com
8
8
 
9
- UI Inspector for Android and iOS, help inspector element properties, and auto generate XPath, script.
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]...
@@ -36,31 +42,8 @@ uiauto.dev
36
42
  ```
37
43
 
38
44
  # DEVELOP
39
- ```bash
40
- # install poetry (python package manager)
41
- pip install poetry # pipx install poetry
42
-
43
- # install deps
44
- poetry install
45
-
46
- # format import
47
- make format
48
45
 
49
- # run server
50
- make dev
51
-
52
- # If you encounter the error NameError: name 'int2byte' is not defined,
53
- # try installing a stable version of the construct package to resolve it:
54
- # and restart: make dev
55
- pip install construct==2.9.45
56
-
57
- ```
58
-
59
- 运行测试
60
-
61
- ```sh
62
- make test
63
- ```
46
+ see [DEVELOP.md](DEVELOP.md)
64
47
 
65
48
  # Links
66
49
  - https://app.tangoapp.dev/ 基于webadb的手机远程控制项目
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "uiautodev"
3
- version = "0.8.0"
3
+ version = "0.10.0"
4
4
  description = "Mobile UI Automation, include UI hierarchy inspector, script recorder"
5
5
  homepage = "https://uiauto.dev"
6
6
  authors = ["codeskyblue <codeskyblue@gmail.com>"]
@@ -17,7 +17,7 @@ adbutils = ">=2.8.10,<3"
17
17
  click = "^8.1.7"
18
18
  pygments = ">=2"
19
19
  uiautomator2 = ">=3.2.0,<4"
20
- fastapi = "0.115.12"
20
+ fastapi = ">=0.115.12,<1"
21
21
  pydantic = "^2.6"
22
22
  wdapy = ">0.2.2,<1"
23
23
  websockets = ">=10.4"
@@ -5,4 +5,4 @@
5
5
  """
6
6
 
7
7
  # version is auto managed by poetry
8
- __version__ = "0.8.0"
8
+ __version__ = "0.10.0"
@@ -27,6 +27,7 @@ from uiautodev.provider import AndroidProvider, HarmonyProvider, IOSProvider, Mo
27
27
  from uiautodev.remote.scrcpy import ScrcpyServer
28
28
  from uiautodev.router.android import router as android_device_router
29
29
  from uiautodev.router.device import make_router
30
+ from uiautodev.router.proxy import router as proxy_router
30
31
  from uiautodev.router.xml import router as xml_router
31
32
  from uiautodev.utils.envutils import Environment
32
33
 
@@ -70,7 +71,7 @@ else:
70
71
 
71
72
  app.include_router(xml_router, prefix="/api/xml", tags=["xml"])
72
73
  app.include_router(android_device_router, prefix="/api/android", tags=["android"])
73
-
74
+ app.include_router(proxy_router, prefix="/proxy", tags=["proxy"])
74
75
 
75
76
  @app.get('/api/{platform}/features')
76
77
  def get_features(platform: str) -> Dict[str, bool]:
@@ -87,6 +88,7 @@ def get_features(platform: str) -> Dict[str, bool]:
87
88
  features[feature_name] = True
88
89
  return features
89
90
 
91
+
90
92
  class InfoResponse(BaseModel):
91
93
  version: str
92
94
  description: str
@@ -140,15 +142,41 @@ def index_redirect():
140
142
  return RedirectResponse(url)
141
143
 
142
144
 
143
- def get_scrcpy_server(serial: str):
144
- # 这里主要是为了避免两次websocket建立建立,启动两个scrcpy进程
145
- logger.info("create scrcpy server for %s", serial)
146
- device = adbutils.device(serial)
147
- return ScrcpyServer(device)
145
+ @app.websocket("/ws/android/scrcpy/{serial}")
146
+ async def handle_android_ws(websocket: WebSocket, serial: str):
147
+ """
148
+ Args:
149
+ serial: device serial
150
+ websocket: WebSocket
151
+ """
152
+ await websocket.accept()
148
153
 
154
+ try:
155
+ logger.info(f"WebSocket serial: {serial}")
156
+ device = adbutils.device(serial)
157
+ server = ScrcpyServer(device)
158
+ await server.handle_unified_websocket(websocket, serial)
159
+ except WebSocketDisconnect:
160
+ logger.info(f"WebSocket disconnected by client.")
161
+ except Exception as e:
162
+ logger.exception(f"WebSocket error for serial={serial}: {e}")
163
+ await websocket.close(code=1000, reason=str(e))
164
+ finally:
165
+ logger.info(f"WebSocket closed for serial={serial}")
149
166
 
150
- @app.websocket("/ws/android/scrcpy/{serial}")
151
- async def unified_ws(websocket: WebSocket, serial: str):
167
+
168
+ def get_harmony_mjpeg_server(serial: str):
169
+ from hypium import UiDriver
170
+
171
+ from uiautodev.remote.harmony_mjpeg import HarmonyMjpegServer
172
+ driver = UiDriver.connect(device_sn=serial)
173
+ logger.info("create harmony mjpeg server for %s", serial)
174
+ logger.info(f'device wake_up_display: {driver.wake_up_display()}')
175
+ return HarmonyMjpegServer(driver)
176
+
177
+
178
+ @app.websocket("/ws/harmony/mjpeg/{serial}")
179
+ async def unified_harmony_ws(websocket: WebSocket, serial: str):
152
180
  """
153
181
  Args:
154
182
  serial: device serial
@@ -159,9 +187,13 @@ async def unified_ws(websocket: WebSocket, serial: str):
159
187
  try:
160
188
  logger.info(f"WebSocket serial: {serial}")
161
189
 
162
- # 获取 ScrcpyServer 实例
163
- server = get_scrcpy_server(serial)
164
- await server.handle_unified_websocket(websocket, serial)
190
+ # 获取 HarmonyScrcpyServer 实例
191
+ server = get_harmony_mjpeg_server(serial)
192
+ server.start()
193
+ await server.handle_ws(websocket)
194
+ except ImportError as e:
195
+ logger.error(f"missing library for harmony: {e}")
196
+ await websocket.close(code=1000, reason="missing library, fix by \"pip install uiautodev[harmony]\"")
165
197
  except WebSocketDisconnect:
166
198
  logger.info(f"WebSocket disconnected by client.")
167
199
  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
- print("uiautodev version:", __version__)
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)
@@ -126,13 +126,48 @@ class AndroidDriver(BaseDriver):
126
126
 
127
127
  def volume_mute(self):
128
128
  self.adb_device.keyevent("VOLUME_MUTE")
129
-
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
+
130
158
  def app_list(self) -> List[AppInfo]:
131
159
  results = []
132
160
  output = self.adb_device.shell(["pm", "list", "packages", '-3'])
133
161
  for m in re.finditer(r"^package:([^\s]+)\r?$", output, re.M):
134
162
  packageName = m.group(1)
135
- results.append(AppInfo(packageName=packageName))
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)
136
171
  return results
137
172
 
138
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 # can be seen as description
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__), '../binaries/scrcpy_server.jar')
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 # type: ignore
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 = asyncio.create_task(self._handle_control_websocket(websocket))
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,58 @@
1
+ import asyncio
2
+
3
+ import httpx
4
+ import websockets
5
+ from fastapi import APIRouter, HTTPException, Request, WebSocket, WebSocketDisconnect
6
+ from fastapi.responses import Response
7
+ import logging
8
+
9
+
10
+ logger = logging.getLogger(__name__)
11
+ router = APIRouter()
12
+
13
+
14
+ # HTTP 转发
15
+ @router.api_route("/http/{target_url:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"])
16
+ async def proxy_http(request: Request, target_url: str):
17
+ logger.info(f"HTTP target_url: {target_url}")
18
+
19
+ async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
20
+ body = await request.body()
21
+ resp = await client.request(
22
+ request.method,
23
+ target_url,
24
+ content=body,
25
+ headers={k: v for k, v in request.headers.items() if k.lower() != "host" and k.lower() != "x-target-url"}
26
+ )
27
+ return Response(content=resp.content, status_code=resp.status_code, headers=dict(resp.headers))
28
+
29
+ # WebSocket 转发
30
+ @router.websocket("/ws/{target_url:path}")
31
+ async def proxy_ws(websocket: WebSocket, target_url: str):
32
+ await websocket.accept()
33
+ logger.info(f"WebSocket target_url: {target_url}")
34
+
35
+ try:
36
+ async with websockets.connect(target_url) as target_ws:
37
+ async def from_client():
38
+ while True:
39
+ msg = await websocket.receive_text()
40
+ await target_ws.send(msg)
41
+
42
+ async def from_server():
43
+ while True:
44
+ msg = await target_ws.recv()
45
+ if isinstance(msg, bytes):
46
+ await websocket.send_bytes(msg)
47
+ elif isinstance(msg, str):
48
+ await websocket.send_text(msg)
49
+ else:
50
+ raise RuntimeError("Unknown message type", msg)
51
+
52
+ await asyncio.gather(from_client(), from_server())
53
+
54
+ except WebSocketDisconnect:
55
+ pass
56
+ except Exception as e:
57
+ logger.error(f"WS Error: {e}")
58
+ await websocket.close()
@@ -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