uiautodev 0.6.0__py3-none-any.whl → 0.7.1__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.
uiautodev/__init__.py CHANGED
@@ -5,4 +5,4 @@
5
5
  """
6
6
 
7
7
  # version is auto managed by poetry
8
- __version__ = "0.6.0"
8
+ __version__ = "0.7.1"
uiautodev/app.py CHANGED
@@ -11,16 +11,20 @@ import signal
11
11
  from pathlib import Path
12
12
  from typing import List
13
13
 
14
+ import adbutils
14
15
  import uvicorn
15
- from fastapi import FastAPI, File, UploadFile
16
+ from fastapi import FastAPI, File, UploadFile, WebSocket
16
17
  from fastapi.middleware.cors import CORSMiddleware
17
18
  from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
18
19
  from pydantic import BaseModel
20
+ from rich.logging import RichHandler
21
+ from starlette.websockets import WebSocketDisconnect
19
22
 
20
23
  from uiautodev import __version__
21
24
  from uiautodev.common import convert_bytes_to_image, get_webpage_url, ocr_image
22
25
  from uiautodev.model import Node
23
26
  from uiautodev.provider import AndroidProvider, HarmonyProvider, IOSProvider, MockProvider
27
+ from uiautodev.remote.scrcpy import ScrcpyServer
24
28
  from uiautodev.router.device import make_router
25
29
  from uiautodev.router.xml import router as xml_router
26
30
  from uiautodev.utils.envutils import Environment
@@ -29,6 +33,16 @@ logger = logging.getLogger(__name__)
29
33
 
30
34
  app = FastAPI()
31
35
 
36
+
37
+ def enable_logger_to_console():
38
+ _logger = logging.getLogger("uiautodev")
39
+ _logger.setLevel(logging.DEBUG)
40
+ _logger.addHandler(RichHandler(enable_link_path=False))
41
+
42
+
43
+ if os.getenv("UIAUTODEV_DEBUG"):
44
+ enable_logger_to_console()
45
+
32
46
  app.add_middleware(
33
47
  CORSMiddleware,
34
48
  allow_origins=["*"],
@@ -109,5 +123,36 @@ def index_redirect():
109
123
  return RedirectResponse(url)
110
124
 
111
125
 
126
+ def get_scrcpy_server(serial: str):
127
+ # 这里主要是为了避免两次websocket建立建立,启动两个scrcpy进程
128
+ logger.info("create scrcpy server for %s", serial)
129
+ device = adbutils.device(serial)
130
+ return ScrcpyServer(device)
131
+
132
+
133
+ @app.websocket("/ws/android/scrcpy/{serial}")
134
+ async def unified_ws(websocket: WebSocket, serial: str):
135
+ """
136
+ Args:
137
+ serial: device serial
138
+ websocket: WebSocket
139
+ """
140
+ await websocket.accept()
141
+
142
+ try:
143
+ logger.info(f"WebSocket serial: {serial}")
144
+
145
+ # 获取 ScrcpyServer 实例
146
+ server = get_scrcpy_server(serial)
147
+ await server.handle_unified_websocket(websocket, serial)
148
+ except WebSocketDisconnect:
149
+ logger.info(f"WebSocket disconnected by client.")
150
+ except Exception as e:
151
+ logger.exception(f"WebSocket error for serial={serial}: {e}")
152
+ await websocket.close(code=1000, reason=str(e))
153
+ finally:
154
+ logger.info(f"WebSocket closed for serial={serial}")
155
+
156
+
112
157
  if __name__ == '__main__':
113
158
  uvicorn.run("uiautodev.app:app", port=4000, reload=True, use_colors=True)
Binary file
uiautodev/cli.py CHANGED
@@ -7,6 +7,7 @@
7
7
  from __future__ import annotations
8
8
 
9
9
  import logging
10
+ import os
10
11
  import platform
11
12
  import subprocess
12
13
  import sys
@@ -29,30 +30,12 @@ logger = logging.getLogger(__name__)
29
30
 
30
31
  CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
31
32
 
33
+
32
34
  @click.group(context_settings=CONTEXT_SETTINGS)
33
35
  @click.option("--verbose", "-v", is_flag=True, default=False, help="verbose mode")
34
36
  def cli(verbose: bool):
35
37
  if verbose:
36
- # try to enable logger is not very easy
37
- # you have to setup logHandler(logFormatter) for the root logger
38
- # and set all children logger to DEBUG
39
- # that's why it is not easy to use it with logging
40
- root_logger = logging.getLogger(__name__.split(".")[0])
41
- root_logger.setLevel(logging.DEBUG)
42
-
43
- console_handler = logging.StreamHandler()
44
- console_handler.setLevel(logging.DEBUG)
45
-
46
- formatter = logging.Formatter('%(name)s - %(levelname)s - %(message)s')
47
- console_handler.setFormatter(formatter)
48
-
49
- root_logger.addHandler(console_handler)
50
-
51
- # set all children logger to DEBUG
52
- for k in root_logger.manager.loggerDict.keys():
53
- if k.startswith(root_logger.name+"."):
54
- logging.getLogger(k).setLevel(logging.DEBUG)
55
-
38
+ os.environ['UIAUTODEV_DEBUG'] = '1'
56
39
  logger.debug("Verbose mode enabled")
57
40
 
58
41
 
@@ -113,7 +96,7 @@ def case():
113
96
  def appium(command: Command, params: list[str] = None):
114
97
  from uiautodev.driver.appium import AppiumProvider
115
98
  from uiautodev.exceptions import AppiumDriverException
116
-
99
+
117
100
  provider = AppiumProvider()
118
101
  try:
119
102
  run_driver_command(provider, command, params)
@@ -150,7 +133,7 @@ def server(port: int, host: str, reload: bool, force: bool, no_browser: bool):
150
133
  use_color = True
151
134
  if platform.system() == 'Windows':
152
135
  use_color = False
153
-
136
+
154
137
  if not no_browser:
155
138
  th = threading.Thread(target=open_browser_when_server_start, args=(f"http://{host}:{port}",))
156
139
  th.daemon = True
@@ -171,6 +154,7 @@ def open_browser_when_server_start(server_url: str):
171
154
  logger.info("open browser: %s", web_url)
172
155
  webbrowser.open(web_url)
173
156
 
157
+
174
158
  def main():
175
159
  # set logger level to INFO
176
160
  # logging.basicConfig(level=logging.INFO)
@@ -71,8 +71,7 @@ class AndroidDriver(BaseDriver):
71
71
  try:
72
72
  return self.ud.dump_hierarchy()
73
73
  except Exception as e:
74
- logger.exception("unexpected dump error: %s", e)
75
- raise AndroidDriverException("Failed to dump hierarchy")
74
+ raise AndroidDriverException(f"Failed to dump hierarchy: {str(e)}")
76
75
 
77
76
  def tap(self, x: int, y: int):
78
77
  self.adb_device.click(x, y)
uiautodev/provider.py CHANGED
@@ -12,7 +12,7 @@ import adbutils
12
12
 
13
13
  from uiautodev.driver.android import AndroidDriver
14
14
  from uiautodev.driver.base_driver import BaseDriver
15
- from uiautodev.driver.harmony import HarmonyDriver, HDC
15
+ from uiautodev.driver.harmony import HDC, HarmonyDriver
16
16
  from uiautodev.driver.ios import IOSDriver
17
17
  from uiautodev.driver.mock import MockDriver
18
18
  from uiautodev.exceptions import UiautoException
@@ -0,0 +1,74 @@
1
+ # Ref
2
+ # https://github.com/Genymobile/scrcpy/blob/master/app/src/android/input.h
3
+ from enum import IntEnum
4
+
5
+
6
+ class MetaState(IntEnum):
7
+ """Android meta state flags ported from Android's KeyEvent class
8
+
9
+ These flags represent the state of meta keys such as ALT, SHIFT, CTRL, etc.
10
+ They can be combined using bitwise OR operations to represent multiple
11
+ meta keys being pressed simultaneously.
12
+
13
+ The values and comments are taken directly from the Android source code
14
+ to maintain compatibility and provide accurate descriptions.
15
+ """
16
+ # No meta keys are pressed
17
+ NONE = 0x0
18
+
19
+ # This mask is used to check whether one of the SHIFT meta keys is pressed
20
+ SHIFT_ON = 0x1
21
+
22
+ # This mask is used to check whether one of the ALT meta keys is pressed
23
+ ALT_ON = 0x2
24
+
25
+ # This mask is used to check whether the SYM meta key is pressed
26
+ SYM_ON = 0x4
27
+
28
+ # This mask is used to check whether the FUNCTION meta key is pressed
29
+ FUNCTION_ON = 0x8
30
+
31
+ # This mask is used to check whether the left ALT meta key is pressed
32
+ ALT_LEFT_ON = 0x10
33
+
34
+ # This mask is used to check whether the right ALT meta key is pressed
35
+ ALT_RIGHT_ON = 0x20
36
+
37
+ # This mask is used to check whether the left SHIFT meta key is pressed
38
+ SHIFT_LEFT_ON = 0x40
39
+
40
+ # This mask is used to check whether the right SHIFT meta key is pressed
41
+ SHIFT_RIGHT_ON = 0x80
42
+
43
+ # This mask is used to check whether the CAPS LOCK meta key is on
44
+ CAPS_LOCK_ON = 0x100000
45
+
46
+ # This mask is used to check whether the NUM LOCK meta key is on
47
+ NUM_LOCK_ON = 0x200000
48
+
49
+ # This mask is used to check whether the SCROLL LOCK meta key is on
50
+ SCROLL_LOCK_ON = 0x400000
51
+
52
+ # This mask is used to check whether one of the CTRL meta keys is pressed
53
+ CTRL_ON = 0x1000
54
+
55
+ # This mask is used to check whether the left CTRL meta key is pressed
56
+ CTRL_LEFT_ON = 0x2000
57
+
58
+ # This mask is used to check whether the right CTRL meta key is pressed
59
+ CTRL_RIGHT_ON = 0x4000
60
+
61
+ # This mask is used to check whether one of the META meta keys is pressed
62
+ META_ON = 0x10000
63
+
64
+ # This mask is used to check whether the left META meta key is pressed
65
+ META_LEFT_ON = 0x20000
66
+
67
+ # This mask is used to check whether the right META meta key is pressed
68
+ META_RIGHT_ON = 0x40000
69
+
70
+
71
+ class KeyeventAction(IntEnum):
72
+ DOWN = 0
73
+ UP = 1
74
+ MULTIPLE = 2
@@ -0,0 +1,350 @@
1
+ from enum import IntEnum
2
+
3
+
4
+ class KeyCode(IntEnum):
5
+ """Android key codes ported from Android's KeyEvent class
6
+
7
+ This enum contains all the key codes defined in Android's KeyEvent class,
8
+ which are used for sending key events to Android devices through scrcpy.
9
+
10
+ The comments for each key code are taken directly from the Android source code
11
+ to maintain compatibility and provide accurate descriptions.
12
+ """
13
+ # Unknown key code
14
+ UNKNOWN = 0
15
+ # Soft Left key - Usually situated below the display on phones
16
+ SOFT_LEFT = 1
17
+ # Soft Right key - Usually situated below the display on phones
18
+ SOFT_RIGHT = 2
19
+ # Home key - This key is handled by the framework and is never delivered to applications
20
+ HOME = 3
21
+ # Back key
22
+ BACK = 4
23
+ # Call key
24
+ CALL = 5
25
+ # End Call key
26
+ ENDCALL = 6
27
+ # '0' key
28
+ KEY_0 = 7
29
+ # '1' key
30
+ KEY_1 = 8
31
+ # '2' key
32
+ KEY_2 = 9
33
+ # '3' key
34
+ KEY_3 = 10
35
+ # '4' key
36
+ KEY_4 = 11
37
+ # '5' key
38
+ KEY_5 = 12
39
+ # '6' key
40
+ KEY_6 = 13
41
+ # '7' key
42
+ KEY_7 = 14
43
+ # '8' key
44
+ KEY_8 = 15
45
+ # '9' key
46
+ KEY_9 = 16
47
+ # '*' key
48
+ STAR = 17
49
+ # '#' key
50
+ POUND = 18
51
+ # Directional Pad Up key - May also be synthesized from trackball motions
52
+ DPAD_UP = 19
53
+ # Directional Pad Down key - May also be synthesized from trackball motions
54
+ DPAD_DOWN = 20
55
+ # Directional Pad Left key - May also be synthesized from trackball motions
56
+ DPAD_LEFT = 21
57
+ # Directional Pad Right key - May also be synthesized from trackball motions
58
+ DPAD_RIGHT = 22
59
+ # Directional Pad Center key - May also be synthesized from trackball motions
60
+ DPAD_CENTER = 23
61
+ # Volume Up key - Adjusts the speaker volume up
62
+ VOLUME_UP = 24
63
+ # Volume Down key - Adjusts the speaker volume down
64
+ VOLUME_DOWN = 25
65
+ # Power key
66
+ POWER = 26
67
+ # Camera key - Used to launch a camera application or take pictures
68
+ CAMERA = 27
69
+ # Clear key
70
+ CLEAR = 28
71
+ A = 29
72
+ B = 30
73
+ C = 31
74
+ D = 32
75
+ E = 33
76
+ F = 34
77
+ G = 35
78
+ H = 36
79
+ I = 37
80
+ J = 38
81
+ K = 39
82
+ L = 40
83
+ M = 41
84
+ N = 42
85
+ O = 43
86
+ P = 44
87
+ Q = 45
88
+ R = 46
89
+ S = 47
90
+ T = 48
91
+ U = 49
92
+ V = 50
93
+ W = 51
94
+ X = 52
95
+ Y = 53
96
+ Z = 54
97
+ COMMA = 55
98
+ PERIOD = 56
99
+ ALT_LEFT = 57
100
+ ALT_RIGHT = 58
101
+ SHIFT_LEFT = 59
102
+ SHIFT_RIGHT = 60
103
+ TAB = 61
104
+ SPACE = 62
105
+ SYM = 63
106
+ EXPLORER = 64
107
+ ENVELOPE = 65
108
+ # Enter key
109
+ ENTER = 66
110
+ # Backspace key - Deletes characters before the insertion point
111
+ DEL = 67
112
+ GRAVE = 68
113
+ MINUS = 69
114
+ EQUALS = 70
115
+ LEFT_BRACKET = 71
116
+ RIGHT_BRACKET = 72
117
+ BACKSLASH = 73
118
+ SEMICOLON = 74
119
+ APOSTROPHE = 75
120
+ SLASH = 76
121
+ AT = 77
122
+ NUM = 78
123
+ HEADSETHOOK = 79
124
+ FOCUS = 80
125
+ PLUS = 81
126
+ # Menu key
127
+ MENU = 82
128
+ NOTIFICATION = 83
129
+ SEARCH = 84
130
+ MEDIA_PLAY_PAUSE = 85
131
+ MEDIA_STOP = 86
132
+ MEDIA_NEXT = 87
133
+ MEDIA_PREVIOUS = 88
134
+ MEDIA_REWIND = 89
135
+ MEDIA_FAST_FORWARD = 90
136
+ MUTE = 91
137
+ PAGE_UP = 92
138
+ PAGE_DOWN = 93
139
+ PICTSYMBOLS = 94
140
+ SWITCH_CHARSET = 95
141
+ BUTTON_A = 96
142
+ BUTTON_B = 97
143
+ BUTTON_C = 98
144
+ BUTTON_X = 99
145
+ BUTTON_Y = 100
146
+ BUTTON_Z = 101
147
+ BUTTON_L1 = 102
148
+ BUTTON_R1 = 103
149
+ BUTTON_L2 = 104
150
+ BUTTON_R2 = 105
151
+ BUTTON_THUMBL = 106
152
+ BUTTON_THUMBR = 107
153
+ BUTTON_START = 108
154
+ BUTTON_SELECT = 109
155
+ BUTTON_MODE = 110
156
+ ESCAPE = 111
157
+ FORWARD_DEL = 112
158
+ CTRL_LEFT = 113
159
+ CTRL_RIGHT = 114
160
+ CAPS_LOCK = 115
161
+ SCROLL_LOCK = 116
162
+ META_LEFT = 117
163
+ META_RIGHT = 118
164
+ FUNCTION = 119
165
+ SYSRQ = 120
166
+ BREAK = 121
167
+ MOVE_HOME = 122
168
+ MOVE_END = 123
169
+ INSERT = 124
170
+ FORWARD = 125
171
+ MEDIA_PLAY = 126
172
+ MEDIA_PAUSE = 127
173
+ MEDIA_CLOSE = 128
174
+ MEDIA_EJECT = 129
175
+ MEDIA_RECORD = 130
176
+ F1 = 131
177
+ F2 = 132
178
+ F3 = 133
179
+ F4 = 134
180
+ F5 = 135
181
+ F6 = 136
182
+ F7 = 137
183
+ F8 = 138
184
+ F9 = 139
185
+ F10 = 140
186
+ F11 = 141
187
+ F12 = 142
188
+ NUM_LOCK = 143
189
+ NUMPAD_0 = 144
190
+ NUMPAD_1 = 145
191
+ NUMPAD_2 = 146
192
+ NUMPAD_3 = 147
193
+ NUMPAD_4 = 148
194
+ NUMPAD_5 = 149
195
+ NUMPAD_6 = 150
196
+ NUMPAD_7 = 151
197
+ NUMPAD_8 = 152
198
+ NUMPAD_9 = 153
199
+ NUMPAD_DIVIDE = 154
200
+ NUMPAD_MULTIPLY = 155
201
+ NUMPAD_SUBTRACT = 156
202
+ NUMPAD_ADD = 157
203
+ NUMPAD_DOT = 158
204
+ NUMPAD_COMMA = 159
205
+ NUMPAD_ENTER = 160
206
+ NUMPAD_EQUALS = 161
207
+ NUMPAD_LEFT_PAREN = 162
208
+ NUMPAD_RIGHT_PAREN = 163
209
+ VOLUME_MUTE = 164
210
+ INFO = 165
211
+ CHANNEL_UP = 166
212
+ CHANNEL_DOWN = 167
213
+ ZOOM_IN = 168
214
+ ZOOM_OUT = 169
215
+ TV = 170
216
+ WINDOW = 171
217
+ GUIDE = 172
218
+ DVR = 173
219
+ BOOKMARK = 174
220
+ CAPTIONS = 175
221
+ SETTINGS = 176
222
+ TV_POWER = 177
223
+ TV_INPUT = 178
224
+ STB_POWER = 179
225
+ STB_INPUT = 180
226
+ AVR_POWER = 181
227
+ AVR_INPUT = 182
228
+ PROG_RED = 183
229
+ PROG_GREEN = 184
230
+ PROG_YELLOW = 185
231
+ PROG_BLUE = 186
232
+ APP_SWITCH = 187
233
+ BUTTON_1 = 188
234
+ BUTTON_2 = 189
235
+ BUTTON_3 = 190
236
+ BUTTON_4 = 191
237
+ BUTTON_5 = 192
238
+ BUTTON_6 = 193
239
+ BUTTON_7 = 194
240
+ BUTTON_8 = 195
241
+ BUTTON_9 = 196
242
+ BUTTON_10 = 197
243
+ BUTTON_11 = 198
244
+ BUTTON_12 = 199
245
+ BUTTON_13 = 200
246
+ BUTTON_14 = 201
247
+ BUTTON_15 = 202
248
+ BUTTON_16 = 203
249
+ LANGUAGE_SWITCH = 204
250
+ MANNER_MODE = 205
251
+ MODE_3D = 206
252
+ CONTACTS = 207
253
+ CALENDAR = 208
254
+ MUSIC = 209
255
+ CALCULATOR = 210
256
+ ZENKAKU_HANKAKU = 211
257
+ EISU = 212
258
+ MUHENKAN = 213
259
+ HENKAN = 214
260
+ KATAKANA_HIRAGANA = 215
261
+ YEN = 216
262
+ RO = 217
263
+ KANA = 218
264
+ ASSIST = 219
265
+ BRIGHTNESS_DOWN = 220
266
+ BRIGHTNESS_UP = 221
267
+ MEDIA_AUDIO_TRACK = 222
268
+ SLEEP = 223
269
+ WAKEUP = 224
270
+ PAIRING = 225
271
+ MEDIA_TOP_MENU = 226
272
+ KEY_11 = 227
273
+ KEY_12 = 228
274
+ LAST_CHANNEL = 229
275
+ TV_DATA_SERVICE = 230
276
+ VOICE_ASSIST = 231
277
+ TV_RADIO_SERVICE = 232
278
+ TV_TELETEXT = 233
279
+ TV_NUMBER_ENTRY = 234
280
+ TV_TERRESTRIAL_ANALOG = 235
281
+ TV_TERRESTRIAL_DIGITAL = 236
282
+ TV_SATELLITE = 237
283
+ TV_SATELLITE_BS = 238
284
+ TV_SATELLITE_CS = 239
285
+ TV_SATELLITE_SERVICE = 240
286
+ TV_NETWORK = 241
287
+ TV_ANTENNA_CABLE = 242
288
+ TV_INPUT_HDMI_1 = 243
289
+ TV_INPUT_HDMI_2 = 244
290
+ TV_INPUT_HDMI_3 = 245
291
+ TV_INPUT_HDMI_4 = 246
292
+ TV_INPUT_COMPOSITE_1 = 247
293
+ TV_INPUT_COMPOSITE_2 = 248
294
+ TV_INPUT_COMPONENT_1 = 249
295
+ TV_INPUT_COMPONENT_2 = 250
296
+ TV_INPUT_VGA_1 = 251
297
+ TV_AUDIO_DESCRIPTION = 252
298
+ TV_AUDIO_DESCRIPTION_MIX_UP = 253
299
+ TV_AUDIO_DESCRIPTION_MIX_DOWN = 254
300
+ TV_ZOOM_MODE = 255
301
+ TV_CONTENTS_MENU = 256
302
+ TV_MEDIA_CONTEXT_MENU = 257
303
+ TV_TIMER_PROGRAMMING = 258
304
+ HELP = 259
305
+ NAVIGATE_PREVIOUS = 260
306
+ NAVIGATE_NEXT = 261
307
+ NAVIGATE_IN = 262
308
+ NAVIGATE_OUT = 263
309
+ STEM_PRIMARY = 264
310
+ STEM_1 = 265
311
+ STEM_2 = 266
312
+ STEM_3 = 267
313
+ DPAD_UP_LEFT = 268
314
+ DPAD_DOWN_LEFT = 269
315
+ DPAD_UP_RIGHT = 270
316
+ DPAD_DOWN_RIGHT = 271
317
+ MEDIA_SKIP_FORWARD = 272
318
+ MEDIA_SKIP_BACKWARD = 273
319
+ MEDIA_STEP_FORWARD = 274
320
+ MEDIA_STEP_BACKWARD = 275
321
+ SOFT_SLEEP = 276
322
+ CUT = 277
323
+ COPY = 278
324
+ PASTE = 279
325
+ SYSTEM_NAVIGATION_UP = 280
326
+ SYSTEM_NAVIGATION_DOWN = 281
327
+ SYSTEM_NAVIGATION_LEFT = 282
328
+ SYSTEM_NAVIGATION_RIGHT = 283
329
+ ALL_APPS = 284
330
+
331
+ # =========================================================================
332
+ # Aliases for original Android KeyEvent names
333
+ # =========================================================================
334
+ # These aliases are provided to maintain compatibility with the original
335
+ # Android KeyEvent naming convention (AKEYCODE_*). This makes it easier
336
+ # to reference keys using the same names as in Android documentation.
337
+
338
+ # Numeric key aliases
339
+ KEYCODE_0 = KEY_0 # '0' key
340
+ KEYCODE_1 = KEY_1 # '1' key
341
+ KEYCODE_2 = KEY_2 # '2' key
342
+ KEYCODE_3 = KEY_3 # '3' key
343
+ KEYCODE_4 = KEY_4 # '4' key
344
+ KEYCODE_5 = KEY_5 # '5' key
345
+ KEYCODE_6 = KEY_6 # '6' key
346
+ KEYCODE_7 = KEY_7 # '7' key
347
+ KEYCODE_8 = KEY_8 # '8' key
348
+ KEYCODE_9 = KEY_9 # '9' key
349
+ KEYCODE_11 = KEY_11 # '11' key
350
+ KEYCODE_12 = KEY_12 # '12' key
@@ -0,0 +1,177 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import os
5
+ import socket
6
+ import struct
7
+ from typing import Optional
8
+
9
+ import retry
10
+ from adbutils import AdbError, Network, adb
11
+ from adbutils._adb import AdbConnection
12
+ from adbutils._device import AdbDevice
13
+ from starlette.websockets import WebSocket, WebSocketDisconnect
14
+
15
+ from uiautodev.remote.touch_controller import ScrcpyTouchController
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ class ScrcpyServer:
20
+ """
21
+ ScrcpyServer class is responsible for managing the scrcpy server on Android devices.
22
+ It handles the initialization, communication, and control of the scrcpy server,
23
+ including video streaming and touch control.
24
+ """
25
+
26
+ def __init__(self, device: AdbDevice, scrcpy_jar_path: Optional[str] = None):
27
+ """
28
+ Initializes the ScrcpyServer instance.
29
+
30
+ Args:
31
+ scrcpy_jar_path (str, optional): Path to the scrcpy server JAR file. Defaults to None.
32
+ """
33
+ self.scrcpy_jar_path = scrcpy_jar_path or os.path.join(os.path.dirname(__file__), '../binaries/scrcpy_server.jar')
34
+ self.device = device
35
+ self.resolution_width = 0 # scrcpy 投屏转换宽度
36
+ self.resolution_height = 0 # scrcpy 投屏转换高度
37
+
38
+ self._shell_conn: AdbConnection
39
+ self._video_conn: socket.socket
40
+ self._control_conn: socket.socket
41
+
42
+ self._setup_connection()
43
+
44
+ def _setup_connection(self):
45
+ self._shell_conn = self._start_scrcpy_server(control=True)
46
+ self._video_conn = self._connect_scrcpy(self.device)
47
+ self._control_conn = self._connect_scrcpy(self.device)
48
+ self._parse_scrcpy_info(self._video_conn)
49
+ self.controller = ScrcpyTouchController(self._control_conn)
50
+
51
+ @retry.retry(exceptions=AdbError, tries=20, delay=0.1)
52
+ def _connect_scrcpy(self, device: AdbDevice) -> socket.socket:
53
+ return device.create_connection(Network.LOCAL_ABSTRACT, 'scrcpy')
54
+
55
+ def _parse_scrcpy_info(self, conn: socket.socket):
56
+ dummy_byte = conn.recv(1)
57
+ if not dummy_byte or dummy_byte != b"\x00":
58
+ raise ConnectionError("Did not receive Dummy Byte!")
59
+ logger.debug('Received Dummy Byte!')
60
+ device_name = conn.recv(64).decode('utf-8').rstrip('\x00')
61
+ logger.debug(f'Device name: {device_name}')
62
+ codec = conn.recv(4)
63
+ logger.debug(f'resolution_data: {codec}')
64
+ resolution_data = conn.recv(8)
65
+ logger.debug(f'resolution_data: {resolution_data}')
66
+ self.resolution_width, self.resolution_height = struct.unpack(">II", resolution_data)
67
+ logger.debug(f'Resolution: {self.resolution_width}x{self.resolution_height}')
68
+
69
+ def close(self):
70
+ try:
71
+ self._control_conn.close()
72
+ self._video_conn.close()
73
+ self._shell_conn.close()
74
+ except:
75
+ pass
76
+
77
+ def __del__(self):
78
+ self.close()
79
+
80
+ def _start_scrcpy_server(self, control: bool = True) -> AdbConnection:
81
+ """
82
+ Pushes the scrcpy server JAR file to the Android device and starts the scrcpy server.
83
+
84
+ Args:
85
+ control (bool, optional): Whether to enable touch control. Defaults to True.
86
+
87
+ Returns:
88
+ AdbConnection
89
+ """
90
+ # 获取设备对象
91
+ device = self.device
92
+
93
+ # 推送 scrcpy 服务器到设备
94
+ device.sync.push(self.scrcpy_jar_path, '/data/local/tmp/scrcpy_server.jar', check=True)
95
+ logger.info('scrcpy server JAR pushed to device')
96
+
97
+ # 构建启动 scrcpy 服务器的命令
98
+ start_command = (
99
+ 'CLASSPATH=/data/local/tmp/scrcpy_server.jar '
100
+ 'app_process / '
101
+ 'com.genymobile.scrcpy.Server 2.7 '
102
+ 'log_level=info max_size=1024 max_fps=30 '
103
+ 'video_bit_rate=8000000 tunnel_forward=true '
104
+ 'send_frame_meta=false '
105
+ f'control={"true" if control else "false"} '
106
+ 'audio=false show_touches=false stay_awake=false '
107
+ 'power_off_on_close=false clipboard_autosync=false'
108
+ )
109
+ conn = device.shell(start_command, stream=True)
110
+ logger.debug("scrcpy output: %s", conn.conn.recv(100))
111
+ return conn # type: ignore
112
+
113
+ async def handle_unified_websocket(self, websocket: WebSocket, serial=''):
114
+ logger.info(f"[Unified] WebSocket connection from {websocket} for serial: {serial}")
115
+
116
+ 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
+ try:
120
+ # 不使用 return_exceptions=True,让异常能够正确传播
121
+ await asyncio.gather(video_task, control_task)
122
+ finally:
123
+ # 取消任务
124
+ for task in (video_task, control_task):
125
+ if not task.done():
126
+ task.cancel()
127
+ logger.info(f"[Unified] WebSocket closed for serial={serial}")
128
+
129
+ async def _stream_video_to_websocket(self, conn: socket.socket, ws: WebSocket):
130
+ # Set socket to non-blocking mode
131
+ conn.setblocking(False)
132
+
133
+ while True:
134
+ # check if ws closed
135
+ if ws.client_state.name != "CONNECTED":
136
+ logger.info('WebSocket no longer connected. Exiting video stream.')
137
+ break
138
+ # Use asyncio to read data asynchronously
139
+ data = await asyncio.get_event_loop().sock_recv(conn, 1024*1024)
140
+ if not data:
141
+ logger.warning('No data received, connection may be closed.')
142
+ raise ConnectionError("Video stream ended unexpectedly")
143
+ # send data to ws
144
+ await ws.send_bytes(data)
145
+
146
+ async def _handle_control_websocket(self, ws: WebSocket):
147
+ while True:
148
+ try:
149
+ message = await ws.receive_text()
150
+ logger.debug(f"[Unified] Received message: {message}")
151
+ message = json.loads(message)
152
+
153
+ width, height = self.resolution_width, self.resolution_height
154
+ message_type = message.get('type')
155
+ if message_type == 'touchMove':
156
+ xP = message['xP']
157
+ yP = message['yP']
158
+ self.controller.move(int(xP * width), int(yP * height), width, height)
159
+ elif message_type == 'touchDown':
160
+ xP = message['xP']
161
+ yP = message['yP']
162
+ self.controller.down(int(xP * width), int(yP * height), width, height)
163
+ elif message_type == 'touchUp':
164
+ xP = message['xP']
165
+ yP = message['yP']
166
+ self.controller.up(int(xP * width), int(yP * height), width, height)
167
+ elif message_type == 'keyEvent':
168
+ event_number = message['data']['eventNumber']
169
+ self.device.shell(f'input keyevent {event_number}')
170
+ elif message_type == 'text':
171
+ text = message['detail']
172
+ self.device.shell(f'am broadcast -a SONIC_KEYBOARD --es msg \'{text}\'')
173
+ elif message_type == 'ping':
174
+ await ws.send_text(json.dumps({"type": "pong"}))
175
+ except json.JSONDecodeError as e:
176
+ logger.error(f"Invalid JSON message: {e}")
177
+ continue
@@ -0,0 +1,123 @@
1
+ import enum
2
+ import socket
3
+ import struct
4
+
5
+ from construct import Byte, Int16ub, Int32ub, Int64ub, Struct
6
+
7
+ from uiautodev.remote.android_input import KeyeventAction, MetaState
8
+ from uiautodev.remote.keycode import KeyCode
9
+
10
+
11
+ # https://github.com/Genymobile/scrcpy/blob/master/app/src/control_msg.h#L29
12
+ class MessageType(enum.IntEnum):
13
+ INJECT_KEYCODE = 0
14
+ INJECT_TEXT = 1
15
+ INJECT_TOUCH_EVENT = 2
16
+ INJECT_SCROLL_EVENT = 3
17
+ BACK_OR_SCREEN_ON = 4
18
+ EXPAND_NOTIFICATION_PANEL = 5
19
+ EXPAND_SETTINGS_PANEL = 6
20
+ COLLAPSE_PANELS = 7
21
+ GET_CLIPBOARD = 8
22
+ SET_CLIPBOARD = 9
23
+ SET_DISPLAY_POWER = 10
24
+ ROTATE_DEVICE = 11
25
+ UHID_CREATE = 12
26
+ UHID_INPUT = 13
27
+ UHID_DESTROY = 14
28
+ OPEN_HARD_KEYBOARD_SETTINGS = 15
29
+ START_APP = 16
30
+ RESET_VIDEO = 17
31
+
32
+
33
+ TouchEvent = Struct(
34
+ "type" / Byte, # SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT
35
+ "action" / Byte, # AKEY_EVENT_ACTION_DOWN
36
+ "pointer_id" / Int64ub, # 8-byte pointer ID
37
+ "x" / Int32ub, # X coordinate
38
+ "y" / Int32ub, # Y coordinate
39
+ "width" / Int16ub, # width
40
+ "height" / Int16ub, # height
41
+ "pressure" / Int16ub, # pressure
42
+ "action_button" / Int32ub, # action button
43
+ "buttons" / Int32ub # buttons
44
+ )
45
+
46
+
47
+ # Define the structure for key events
48
+ KeyEvent = Struct(
49
+ "type" / Byte, # SC_CONTROL_MSG_TYPE_INJECT_KEYCODE
50
+ "action" / Byte, # AKEY_EVENT_ACTION (DOWN, UP, MULTIPLE)
51
+ "keycode" / Int32ub, # Android keycode
52
+ "repeat" / Int32ub, # Repeat count
53
+ "metastate" / Int32ub # Meta state flags (SHIFT, ALT, etc.)
54
+ )
55
+
56
+
57
+ class ScrcpyTouchController:
58
+ """scrcpy控制类,支持scrcpy版本>=2.2"""
59
+
60
+ def __init__(self, control_socket: socket.socket):
61
+ self.control_socket = control_socket
62
+
63
+ def _build_touch_event(self, action: int, x: int, y: int, width: int, height: int):
64
+ x = max(0, min(x, width))
65
+ y = max(0, min(y, height))
66
+ return TouchEvent.build(dict(
67
+ type=MessageType.INJECT_TOUCH_EVENT,
68
+ action=action,
69
+ pointer_id=1,
70
+ x=x,
71
+ y=y,
72
+ width=width,
73
+ height=height,
74
+ pressure=1,
75
+ action_button=1, # AMOTION_EVENT_BUTTON_PRIMARY (action button)
76
+ buttons=1, # AMOTION_EVENT_BUTTON_PRIMARY (buttons)
77
+ ))
78
+
79
+ def down(self, x: int, y: int, width: int, height: int):
80
+ """发送down操作"""
81
+ data = self._build_touch_event(0, x, y, width, height)
82
+ self.control_socket.send(data)
83
+
84
+ def up(self, x: int, y: int, width: int, height: int):
85
+ """发送up操作"""
86
+ data = self._build_touch_event(1, x, y, width, height)
87
+ self.control_socket.send(data)
88
+
89
+ def move(self, x: int, y: int, width: int, height: int):
90
+ """发送move操作"""
91
+ data = self._build_touch_event(2, x, y, width, height)
92
+ self.control_socket.send(data)
93
+
94
+ def text(self, text: str):
95
+ """发送文本操作"""
96
+
97
+ # buffer = text.encode("utf-8")
98
+ # values = struct.pack(self.format_string, 2, 3, 1, len(buffer), 0, 0, 0, self.const_value,
99
+ # self.unknown1, self.unknown2) + buffer
100
+ # self.control_socket.send(values)
101
+ pass
102
+
103
+ def key(self, action: KeyeventAction, keycode: KeyCode, repeat: int, metastate: MetaState):
104
+ """
105
+ Send a keycode event to the Android device
106
+
107
+ Args:
108
+ action: Key action (DOWN, UP, or MULTIPLE)
109
+ keycode: Android key code to send
110
+ repeat: Number of times the key is repeated
111
+ metastate: Meta state flags (SHIFT, ALT, etc.)
112
+ """
113
+ # Build the data using the KeyEvent structure
114
+ data = KeyEvent.build(dict(
115
+ type=MessageType.INJECT_KEYCODE, # Type byte
116
+ action=action, # Action byte
117
+ keycode=keycode, # Keycode (4 bytes)
118
+ repeat=repeat, # Repeat count (4 bytes)
119
+ metastate=metastate, # Meta state (4 bytes)
120
+ ))
121
+
122
+ # Send the data to the control socket
123
+ self.control_socket.send(data)
@@ -80,7 +80,8 @@ def make_router(provider: BaseProvider) -> APIRouter:
80
80
  else:
81
81
  return Response(content=f"Invalid format: {format}", media_type="text/plain", status_code=400)
82
82
  except Exception as e:
83
- logger.exception("dump_hierarchy failed")
83
+ #logger.exception("dump_hierarchy failed")
84
+ logger.error(f"Error dumping hierarchy: {str(e)}")
84
85
  return Response(content=str(e), media_type="text/plain", status_code=500)
85
86
 
86
87
  @router.post('/{serial}/command/tap')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: uiautodev
3
- Version: 0.6.0
3
+ Version: 0.7.1
4
4
  Summary: Mobile UI Automation, include UI hierarchy inspector, script recorder
5
5
  License: MIT
6
6
  Author: codeskyblue
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Programming Language :: Python :: 3.13
17
- Requires-Dist: adbutils (>=2.7.0,<3.0.0)
17
+ Requires-Dist: adbutils (>=2.8.10,<3)
18
18
  Requires-Dist: click (>=8.1.7,<9.0.0)
19
19
  Requires-Dist: construct
20
20
  Requires-Dist: fastapi (>=0.111.0,<0.112.0)
@@ -25,8 +25,9 @@ Requires-Dist: poetry (>=1.8.2,<2.0.0)
25
25
  Requires-Dist: pydantic (>=2.6,<3.0)
26
26
  Requires-Dist: pygments (>=2)
27
27
  Requires-Dist: uiautomator2 (>=2)
28
- Requires-Dist: uvicorn[standard]
28
+ Requires-Dist: uvicorn
29
29
  Requires-Dist: wdapy (>=0.2.2,<0.3.0)
30
+ Requires-Dist: websockets (>=10.4)
30
31
  Project-URL: Homepage, https://uiauto.dev
31
32
  Description-Content-Type: text/markdown
32
33
 
@@ -80,7 +81,23 @@ make format
80
81
 
81
82
  # run server
82
83
  make dev
84
+
85
+ # If you encounter the error NameError: name 'int2byte' is not defined,
86
+ # try installing a stable version of the construct package to resolve it:
87
+ # and restart: make dev
88
+ pip install construct==2.9.45
89
+
90
+ ```
91
+
92
+ 运行测试
93
+
94
+ ```sh
95
+ make test
83
96
  ```
84
97
 
98
+ # Links
99
+ - https://app.tangoapp.dev/ 基于webadb的手机远程控制项目
100
+ - https://docs.tangoapp.dev/scrcpy/video/web-codecs/ H264解码器
101
+
85
102
  # LICENSE
86
103
  [MIT](LICENSE)
@@ -1,13 +1,14 @@
1
- uiautodev/__init__.py,sha256=nCGwaTL1ifIZD9hnH2Gl14g3cUbMyEWFZmTebRCLGrs,164
1
+ uiautodev/__init__.py,sha256=C3uyY7lWpX_Ggw9MkWZQfP13oV4gM2QgBttrO1NQzO0,164
2
2
  uiautodev/__main__.py,sha256=0WZHyHW-M7FG5RexANNoIB5pkCX8xwQbTnmaOA9Y1kg,176
3
- uiautodev/app.py,sha256=U0aDW2dwNEVy5kSBAbTCo2CAVyzaEZrJFBzQsCSoV3c,3215
3
+ uiautodev/app.py,sha256=dzylhfAId-GqjsyQuJznCNzn2NIK9WZ7PsiHhUrazvY,4615
4
4
  uiautodev/appium_proxy.py,sha256=yMzPnIDo50hYSaq0g5bXUpgRrFa_849wNa2o7ZpxGNY,1773
5
+ uiautodev/binaries/scrcpy_server.jar,sha256=ojxWWfNsJg8QXAItJ7yz6v_6JgcOe6qe2mbQE3ehrbo,71200
5
6
  uiautodev/case.py,sha256=Jk2_5X2F-XIPnGuYTCqOVQiwwchwOhF7uKK5oKv5shg,3919
6
- uiautodev/cli.py,sha256=K4CEvGJSDWLAFR5tvls2Qp4evQ2lApLoUxA-4DI2-Sc,6235
7
+ uiautodev/cli.py,sha256=nqxVTaNkItIN5REGEgVPFr0JWxPmeIPz_tZvFQNU7Jc,5442
7
8
  uiautodev/command_proxy.py,sha256=C9z-dITMED4k5na45NMdEWAXlLh3Ll921mKuefFyk78,5226
8
9
  uiautodev/command_types.py,sha256=pWdnCS4FRnEIiR1ynd4cjXX8zeFndjPztacBlukt61E,1761
9
10
  uiautodev/common.py,sha256=1A0kXfxVrp_i5mc_aRjuqSDWFFZ7DwZR9qpRLu2GMMg,1488
10
- uiautodev/driver/android.py,sha256=LOOF2d7Fg8crzA7G0rjBMQ63PQlCtLNCWfR20YEhrSE,6394
11
+ uiautodev/driver/android.py,sha256=ZdgMkcoyqleibHbPeg-9bDnpFx3KHBnQxhLZlnKOmbE,6344
11
12
  uiautodev/driver/appium.py,sha256=U3TGpOXmu3tEa3E1ttTFoXehOfFyjavJQ3XA4CtqeBE,5308
12
13
  uiautodev/driver/base_driver.py,sha256=neoVS9T8MN5RXvW6vvqXSc6pcBW_fqujJurjUrlt0OA,2816
13
14
  uiautodev/driver/harmony.py,sha256=93pwlg04wazey8MQM6DEvcBkr52REYVw4bwz321fK38,8031
@@ -18,16 +19,20 @@ uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk,sha256=cKUVKpqEiGRXOD
18
19
  uiautodev/driver/udt/udt.py,sha256=p6opbUtYxEGTINIX83F6m2CtzB42iSSBYRv1SjXCEFg,8351
19
20
  uiautodev/exceptions.py,sha256=TuRD5SWQk5N2_KjrcDuXG_p84LBhLa2QEEXyFNFm0yQ,465
20
21
  uiautodev/model.py,sha256=hziSYyh7ufaKbQrMmRMcIVeZ0x-pwkLMHljrI-qu184,950
21
- uiautodev/provider.py,sha256=oElv04fsxrBBBm1FZ6VyNqquSQbhJXD25ApC7UgDsVI,2862
22
- uiautodev/router/device.py,sha256=GHW83L63MxnjAEmTwhpetGatiu8iO0AOYr74UyFuF24,4835
22
+ uiautodev/provider.py,sha256=EnBI8PD2eoBjqugGS5cmy8GZ1Z7EaEF6YEQSfpOMN6s,2862
23
+ uiautodev/remote/android_input.py,sha256=r9y2SxnDw0GhN44emL-2Nz0UasaVaVtzh53hd-LJ710,2445
24
+ uiautodev/remote/keycode.py,sha256=RHSJVfcNY2pelQd7_tcE6T0j3n8CKBkiku7A2AJZUpk,8097
25
+ uiautodev/remote/scrcpy.py,sha256=Iagm9RrdB67KC2fkyyzpCeHYT0VMSQhoBEks1R_bXpo,7380
26
+ uiautodev/remote/touch_controller.py,sha256=dYl5XTLaYEyZiNJmKwHQpw9QhPSkN3iUetJSaiQJBHg,4255
27
+ uiautodev/router/device.py,sha256=zYrxjVGjrz7Jw8G9KNO0su22OIiQDuuSHBDBDpC1PG8,4899
23
28
  uiautodev/router/xml.py,sha256=MKVLhjMBqE4qbEraQxvdrVp_OBnylEL9Wti5lnmBDk4,891
24
29
  uiautodev/static/demo.html,sha256=qC7qUZP5Af9T3V5EuFGbovzv8mArwiGMWsX_vcs_Bt0,1240
25
30
  uiautodev/utils/common.py,sha256=L1qBBBS6jRgkXlGy5o6Xafo49auLXKRWyX9x8U_IKjc,4821
26
31
  uiautodev/utils/envutils.py,sha256=Clyt2Hz9PXpK_fT0yWbMmixXyGvCaJO3LAgamM7aUVc,197
27
32
  uiautodev/utils/exceptions.py,sha256=lL_G_E41KWvfXnl32-E4Vgr3_HyTboxq_EwzdQMuvK4,637
28
33
  uiautodev/utils/usbmux.py,sha256=LYupLDn7U4KFKhYQJrmIroS-3040gqZQVDRDB_FNDJM,17386
29
- uiautodev-0.6.0.dist-info/LICENSE,sha256=RyeW676gBYO7AVVP2zQgfEx5rPSt46vR47xXZe7TlX4,1068
30
- uiautodev-0.6.0.dist-info/METADATA,sha256=BmJOpByP-LzY61Q5-DUfH7eq3VnIXSHZ1viCbbCK4Qs,2373
31
- uiautodev-0.6.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
32
- uiautodev-0.6.0.dist-info/entry_points.txt,sha256=zBY8GgseYAAzPFA5Cf4rCCS9ivdyWsNxMVVYIaGAHJU,88
33
- uiautodev-0.6.0.dist-info/RECORD,,
34
+ uiautodev-0.7.1.dist-info/LICENSE,sha256=RyeW676gBYO7AVVP2zQgfEx5rPSt46vR47xXZe7TlX4,1068
35
+ uiautodev-0.7.1.dist-info/METADATA,sha256=r50rNe8RnSTSsyAECW_HFmH_FwScV1CKwpucBJ9yuds,2776
36
+ uiautodev-0.7.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
37
+ uiautodev-0.7.1.dist-info/entry_points.txt,sha256=zBY8GgseYAAzPFA5Cf4rCCS9ivdyWsNxMVVYIaGAHJU,88
38
+ uiautodev-0.7.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.2
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any