uiautodev 0.8.0__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.

Files changed (40) hide show
  1. {uiautodev-0.8.0 → uiautodev-0.9.0}/PKG-INFO +9 -3
  2. {uiautodev-0.8.0 → uiautodev-0.9.0}/README.md +8 -2
  3. {uiautodev-0.8.0 → uiautodev-0.9.0}/pyproject.toml +1 -1
  4. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/__init__.py +1 -1
  5. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/app.py +41 -10
  6. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/cli.py +22 -2
  7. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/driver/android.py +37 -2
  8. uiautodev-0.9.0/uiautodev/exceptions.py +26 -0
  9. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/model.py +4 -2
  10. uiautodev-0.9.0/uiautodev/remote/harmony_mjpeg.py +199 -0
  11. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/remote/scrcpy.py +12 -10
  12. uiautodev-0.8.0/uiautodev/exceptions.py +0 -32
  13. {uiautodev-0.8.0 → uiautodev-0.9.0}/LICENSE +0 -0
  14. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/__main__.py +0 -0
  15. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/appium_proxy.py +0 -0
  16. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/binaries/scrcpy_server.jar +0 -0
  17. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/case.py +0 -0
  18. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/command_proxy.py +0 -0
  19. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/command_types.py +0 -0
  20. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/common.py +0 -0
  21. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/driver/appium.py +0 -0
  22. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/driver/base_driver.py +0 -0
  23. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/driver/harmony.py +0 -0
  24. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/driver/ios.py +0 -0
  25. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/driver/mock.py +0 -0
  26. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/driver/testdata/layout.json +0 -0
  27. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk +0 -0
  28. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/driver/udt/udt.py +0 -0
  29. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/provider.py +0 -0
  30. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/remote/android_input.py +0 -0
  31. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/remote/keycode.py +0 -0
  32. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/remote/touch_controller.py +0 -0
  33. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/router/android.py +0 -0
  34. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/router/device.py +0 -0
  35. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/router/xml.py +0 -0
  36. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/static/demo.html +0 -0
  37. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/utils/common.py +0 -0
  38. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/utils/envutils.py +0 -0
  39. {uiautodev-0.8.0 → uiautodev-0.9.0}/uiautodev/utils/exceptions.py +0 -0
  40. {uiautodev-0.8.0 → 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.8.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
- > 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]...
@@ -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]...
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "uiautodev"
3
- version = "0.8.0"
3
+ version = "0.9.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>"]
@@ -5,4 +5,4 @@
5
5
  """
6
6
 
7
7
  # version is auto managed by poetry
8
- __version__ = "0.8.0"
8
+ __version__ = "0.9.0"
@@ -87,6 +87,7 @@ def get_features(platform: str) -> Dict[str, bool]:
87
87
  features[feature_name] = True
88
88
  return features
89
89
 
90
+
90
91
  class InfoResponse(BaseModel):
91
92
  version: str
92
93
  description: str
@@ -140,15 +141,41 @@ def index_redirect():
140
141
  return RedirectResponse(url)
141
142
 
142
143
 
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)
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()
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}")
165
+
166
+
167
+ def get_harmony_mjpeg_server(serial: str):
168
+ from hypium import UiDriver
148
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)
149
175
 
150
- @app.websocket("/ws/android/scrcpy/{serial}")
151
- async def unified_ws(websocket: WebSocket, serial: str):
176
+
177
+ @app.websocket("/ws/harmony/mjpeg/{serial}")
178
+ async def unified_harmony_ws(websocket: WebSocket, serial: str):
152
179
  """
153
180
  Args:
154
181
  serial: device serial
@@ -159,9 +186,13 @@ async def unified_ws(websocket: WebSocket, serial: str):
159
186
  try:
160
187
  logger.info(f"WebSocket serial: {serial}")
161
188
 
162
- # 获取 ScrcpyServer 实例
163
- server = get_scrcpy_server(serial)
164
- await server.handle_unified_websocket(websocket, serial)
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]\"")
165
196
  except WebSocketDisconnect:
166
197
  logger.info(f"WebSocket disconnected by client.")
167
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
- 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
@@ -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