uiautodev 0.6.0__tar.gz → 0.7.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.6.0 → uiautodev-0.7.0}/PKG-INFO +14 -2
- {uiautodev-0.6.0 → uiautodev-0.7.0}/README.md +12 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/pyproject.toml +6 -2
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/__init__.py +1 -1
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/app.py +46 -1
- uiautodev-0.7.0/uiautodev/binaries/scrcpy_server.jar +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/cli.py +6 -22
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/driver/android.py +1 -2
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/provider.py +1 -1
- uiautodev-0.7.0/uiautodev/remote/android_input.py +74 -0
- uiautodev-0.7.0/uiautodev/remote/keycode.py +350 -0
- uiautodev-0.7.0/uiautodev/remote/scrcpy.py +177 -0
- uiautodev-0.7.0/uiautodev/remote/touch_controller.py +123 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/router/device.py +2 -1
- {uiautodev-0.6.0 → uiautodev-0.7.0}/LICENSE +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/__main__.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/appium_proxy.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/case.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/command_proxy.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/command_types.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/common.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/driver/appium.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/driver/base_driver.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/driver/harmony.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/driver/ios.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/driver/mock.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/driver/testdata/layout.json +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/driver/udt/appium-uiautomator2-v5.12.4-light.apk +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/driver/udt/udt.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/exceptions.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/model.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/router/xml.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/static/demo.html +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/utils/common.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/utils/envutils.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.0}/uiautodev/utils/exceptions.py +0 -0
- {uiautodev-0.6.0 → uiautodev-0.7.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.7.0
|
|
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.
|
|
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)
|
|
@@ -80,6 +80,18 @@ make format
|
|
|
80
80
|
|
|
81
81
|
# run server
|
|
82
82
|
make dev
|
|
83
|
+
|
|
84
|
+
# If you encounter the error NameError: name 'int2byte' is not defined,
|
|
85
|
+
# try installing a stable version of the construct package to resolve it:
|
|
86
|
+
# and restart: make dev
|
|
87
|
+
pip install construct==2.9.45
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
运行测试
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
make test
|
|
83
95
|
```
|
|
84
96
|
|
|
85
97
|
# LICENSE
|
|
@@ -48,6 +48,18 @@ make format
|
|
|
48
48
|
|
|
49
49
|
# run server
|
|
50
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
|
|
51
63
|
```
|
|
52
64
|
|
|
53
65
|
# LICENSE
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "uiautodev"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.7.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>"]
|
|
7
7
|
license = "MIT"
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
|
|
10
|
+
include = [
|
|
11
|
+
{path = "uiautodev/binaries/scrcpy.jar"}
|
|
12
|
+
]
|
|
13
|
+
|
|
10
14
|
[tool.poetry.dependencies]
|
|
11
15
|
python = "^3.8"
|
|
12
16
|
pillow = "*"
|
|
13
|
-
adbutils = "
|
|
17
|
+
adbutils = ">=2.8.10,<3"
|
|
14
18
|
construct = "*"
|
|
15
19
|
lxml = "*"
|
|
16
20
|
click = "^8.1.7"
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|
|
@@ -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
|
|
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')
|
|
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.6.0 → uiautodev-0.7.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
|