autoglm-gui 1.5.0__py3-none-any.whl → 1.5.2__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.
- AutoGLM_GUI/__init__.py +1 -1
- AutoGLM_GUI/__main__.py +11 -2
- AutoGLM_GUI/adb_plus/qr_pair.py +3 -3
- AutoGLM_GUI/agents/__init__.py +7 -2
- AutoGLM_GUI/agents/factory.py +46 -6
- AutoGLM_GUI/agents/glm/agent.py +8 -3
- AutoGLM_GUI/agents/glm/async_agent.py +515 -0
- AutoGLM_GUI/agents/glm/parser.py +4 -2
- AutoGLM_GUI/agents/mai/agent.py +3 -0
- AutoGLM_GUI/agents/protocols.py +111 -1
- AutoGLM_GUI/agents/stream_runner.py +11 -7
- AutoGLM_GUI/api/__init__.py +3 -1
- AutoGLM_GUI/api/agents.py +103 -37
- AutoGLM_GUI/api/devices.py +72 -0
- AutoGLM_GUI/api/history.py +27 -1
- AutoGLM_GUI/api/layered_agent.py +9 -8
- AutoGLM_GUI/api/mcp.py +6 -4
- AutoGLM_GUI/config_manager.py +38 -1
- AutoGLM_GUI/device_manager.py +28 -4
- AutoGLM_GUI/device_metadata_manager.py +174 -0
- AutoGLM_GUI/devices/mock_device.py +8 -1
- AutoGLM_GUI/models/history.py +45 -1
- AutoGLM_GUI/phone_agent_manager.py +145 -32
- AutoGLM_GUI/scheduler_manager.py +52 -6
- AutoGLM_GUI/schemas.py +101 -0
- AutoGLM_GUI/scrcpy_stream.py +2 -1
- AutoGLM_GUI/static/assets/{about-BQm96DAl.js → about-D7r9gCvG.js} +1 -1
- AutoGLM_GUI/static/assets/{alert-dialog-B42XxGPR.js → alert-dialog-BKM-yRiQ.js} +1 -1
- AutoGLM_GUI/static/assets/chat-k6TTD7PW.js +129 -0
- AutoGLM_GUI/static/assets/{circle-alert-D4rSJh37.js → circle-alert-sohSDLhl.js} +1 -1
- AutoGLM_GUI/static/assets/{dialog-DZ78cEcj.js → dialog-BgtPh0d5.js} +1 -1
- AutoGLM_GUI/static/assets/eye-DLqKbQmg.js +1 -0
- AutoGLM_GUI/static/assets/history-Bv1lfGUU.js +1 -0
- AutoGLM_GUI/static/assets/index-CV7jGxGm.css +1 -0
- AutoGLM_GUI/static/assets/index-CxWwh1VO.js +1 -0
- AutoGLM_GUI/static/assets/{index-CssG-3TH.js → index-SysdKciY.js} +5 -5
- AutoGLM_GUI/static/assets/label-DTUnzN4B.js +1 -0
- AutoGLM_GUI/static/assets/{logs-eoFxn5of.js → logs-BIhnDizW.js} +1 -1
- AutoGLM_GUI/static/assets/{popover-DLsuV5Sx.js → popover-CikYqu2P.js} +1 -1
- AutoGLM_GUI/static/assets/scheduled-tasks-B-KBsGbl.js +1 -0
- AutoGLM_GUI/static/assets/{textarea-BX6y7uM5.js → textarea-knJZrz77.js} +1 -1
- AutoGLM_GUI/static/assets/workflows-DzcSYwLZ.js +1 -0
- AutoGLM_GUI/static/index.html +2 -2
- {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/METADATA +58 -7
- autoglm_gui-1.5.2.dist-info/RECORD +119 -0
- AutoGLM_GUI/device_adapter.py +0 -263
- AutoGLM_GUI/static/assets/chat-C0L2gQYG.js +0 -129
- AutoGLM_GUI/static/assets/history-DFBv7TGc.js +0 -1
- AutoGLM_GUI/static/assets/index-Bzyv2yQ2.css +0 -1
- AutoGLM_GUI/static/assets/index-CmZSnDqc.js +0 -1
- AutoGLM_GUI/static/assets/label-BCUzE_nm.js +0 -1
- AutoGLM_GUI/static/assets/scheduled-tasks-MyqGJvy_.js +0 -1
- AutoGLM_GUI/static/assets/square-pen-zGWYrdfj.js +0 -1
- AutoGLM_GUI/static/assets/workflows-CYFs6ssC.js +0 -1
- autoglm_gui-1.5.0.dist-info/RECORD +0 -157
- mai_agent/base.py +0 -137
- mai_agent/mai_grounding_agent.py +0 -263
- mai_agent/mai_naivigation_agent.py +0 -526
- mai_agent/prompt.py +0 -148
- mai_agent/unified_memory.py +0 -67
- mai_agent/utils.py +0 -73
- phone_agent/__init__.py +0 -12
- phone_agent/actions/__init__.py +0 -5
- phone_agent/actions/handler.py +0 -400
- phone_agent/actions/handler_ios.py +0 -278
- phone_agent/adb/__init__.py +0 -51
- phone_agent/adb/connection.py +0 -358
- phone_agent/adb/device.py +0 -253
- phone_agent/adb/input.py +0 -108
- phone_agent/adb/screenshot.py +0 -108
- phone_agent/agent.py +0 -253
- phone_agent/agent_ios.py +0 -277
- phone_agent/config/__init__.py +0 -53
- phone_agent/config/apps.py +0 -227
- phone_agent/config/apps_harmonyos.py +0 -256
- phone_agent/config/apps_ios.py +0 -339
- phone_agent/config/i18n.py +0 -81
- phone_agent/config/prompts.py +0 -80
- phone_agent/config/prompts_en.py +0 -79
- phone_agent/config/prompts_zh.py +0 -82
- phone_agent/config/timing.py +0 -167
- phone_agent/device_factory.py +0 -166
- phone_agent/hdc/__init__.py +0 -53
- phone_agent/hdc/connection.py +0 -384
- phone_agent/hdc/device.py +0 -269
- phone_agent/hdc/input.py +0 -145
- phone_agent/hdc/screenshot.py +0 -127
- phone_agent/model/__init__.py +0 -5
- phone_agent/model/client.py +0 -290
- phone_agent/xctest/__init__.py +0 -47
- phone_agent/xctest/connection.py +0 -379
- phone_agent/xctest/device.py +0 -472
- phone_agent/xctest/input.py +0 -311
- phone_agent/xctest/screenshot.py +0 -226
- {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/WHEEL +0 -0
- {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/entry_points.txt +0 -0
- {autoglm_gui-1.5.0.dist-info → autoglm_gui-1.5.2.dist-info}/licenses/LICENSE +0 -0
phone_agent/adb/device.py
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
"""Device control utilities for Android automation."""
|
|
2
|
-
|
|
3
|
-
import subprocess
|
|
4
|
-
import time
|
|
5
|
-
|
|
6
|
-
from phone_agent.config.apps import APP_PACKAGES
|
|
7
|
-
from phone_agent.config.timing import TIMING_CONFIG
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def get_current_app(device_id: str | None = None) -> str:
|
|
11
|
-
"""
|
|
12
|
-
Get the currently focused app name.
|
|
13
|
-
|
|
14
|
-
Args:
|
|
15
|
-
device_id: Optional ADB device ID for multi-device setups.
|
|
16
|
-
|
|
17
|
-
Returns:
|
|
18
|
-
The app name if recognized, otherwise "System Home".
|
|
19
|
-
"""
|
|
20
|
-
adb_prefix = _get_adb_prefix(device_id)
|
|
21
|
-
|
|
22
|
-
result = subprocess.run(
|
|
23
|
-
adb_prefix + ["shell", "dumpsys", "window"],
|
|
24
|
-
capture_output=True,
|
|
25
|
-
text=True,
|
|
26
|
-
encoding="utf-8",
|
|
27
|
-
)
|
|
28
|
-
output = result.stdout
|
|
29
|
-
if not output:
|
|
30
|
-
raise ValueError("No output from dumpsys window")
|
|
31
|
-
|
|
32
|
-
# Parse window focus info
|
|
33
|
-
for line in output.split("\n"):
|
|
34
|
-
if "mCurrentFocus" in line or "mFocusedApp" in line:
|
|
35
|
-
for app_name, package in APP_PACKAGES.items():
|
|
36
|
-
if package in line:
|
|
37
|
-
return app_name
|
|
38
|
-
|
|
39
|
-
return "System Home"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def tap(
|
|
43
|
-
x: int, y: int, device_id: str | None = None, delay: float | None = None
|
|
44
|
-
) -> None:
|
|
45
|
-
"""
|
|
46
|
-
Tap at the specified coordinates.
|
|
47
|
-
|
|
48
|
-
Args:
|
|
49
|
-
x: X coordinate.
|
|
50
|
-
y: Y coordinate.
|
|
51
|
-
device_id: Optional ADB device ID.
|
|
52
|
-
delay: Delay in seconds after tap. If None, uses configured default.
|
|
53
|
-
"""
|
|
54
|
-
if delay is None:
|
|
55
|
-
delay = TIMING_CONFIG.device.default_tap_delay
|
|
56
|
-
|
|
57
|
-
adb_prefix = _get_adb_prefix(device_id)
|
|
58
|
-
|
|
59
|
-
subprocess.run(
|
|
60
|
-
adb_prefix + ["shell", "input", "tap", str(x), str(y)], capture_output=True
|
|
61
|
-
)
|
|
62
|
-
time.sleep(delay)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def double_tap(
|
|
66
|
-
x: int, y: int, device_id: str | None = None, delay: float | None = None
|
|
67
|
-
) -> None:
|
|
68
|
-
"""
|
|
69
|
-
Double tap at the specified coordinates.
|
|
70
|
-
|
|
71
|
-
Args:
|
|
72
|
-
x: X coordinate.
|
|
73
|
-
y: Y coordinate.
|
|
74
|
-
device_id: Optional ADB device ID.
|
|
75
|
-
delay: Delay in seconds after double tap. If None, uses configured default.
|
|
76
|
-
"""
|
|
77
|
-
if delay is None:
|
|
78
|
-
delay = TIMING_CONFIG.device.default_double_tap_delay
|
|
79
|
-
|
|
80
|
-
adb_prefix = _get_adb_prefix(device_id)
|
|
81
|
-
|
|
82
|
-
subprocess.run(
|
|
83
|
-
adb_prefix + ["shell", "input", "tap", str(x), str(y)], capture_output=True
|
|
84
|
-
)
|
|
85
|
-
time.sleep(TIMING_CONFIG.device.double_tap_interval)
|
|
86
|
-
subprocess.run(
|
|
87
|
-
adb_prefix + ["shell", "input", "tap", str(x), str(y)], capture_output=True
|
|
88
|
-
)
|
|
89
|
-
time.sleep(delay)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def long_press(
|
|
93
|
-
x: int,
|
|
94
|
-
y: int,
|
|
95
|
-
duration_ms: int = 3000,
|
|
96
|
-
device_id: str | None = None,
|
|
97
|
-
delay: float | None = None,
|
|
98
|
-
) -> None:
|
|
99
|
-
"""
|
|
100
|
-
Long press at the specified coordinates.
|
|
101
|
-
|
|
102
|
-
Args:
|
|
103
|
-
x: X coordinate.
|
|
104
|
-
y: Y coordinate.
|
|
105
|
-
duration_ms: Duration of press in milliseconds.
|
|
106
|
-
device_id: Optional ADB device ID.
|
|
107
|
-
delay: Delay in seconds after long press. If None, uses configured default.
|
|
108
|
-
"""
|
|
109
|
-
if delay is None:
|
|
110
|
-
delay = TIMING_CONFIG.device.default_long_press_delay
|
|
111
|
-
|
|
112
|
-
adb_prefix = _get_adb_prefix(device_id)
|
|
113
|
-
|
|
114
|
-
subprocess.run(
|
|
115
|
-
adb_prefix
|
|
116
|
-
+ ["shell", "input", "swipe", str(x), str(y), str(x), str(y), str(duration_ms)],
|
|
117
|
-
capture_output=True,
|
|
118
|
-
)
|
|
119
|
-
time.sleep(delay)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def swipe(
|
|
123
|
-
start_x: int,
|
|
124
|
-
start_y: int,
|
|
125
|
-
end_x: int,
|
|
126
|
-
end_y: int,
|
|
127
|
-
duration_ms: int | None = None,
|
|
128
|
-
device_id: str | None = None,
|
|
129
|
-
delay: float | None = None,
|
|
130
|
-
) -> None:
|
|
131
|
-
"""
|
|
132
|
-
Swipe from start to end coordinates.
|
|
133
|
-
|
|
134
|
-
Args:
|
|
135
|
-
start_x: Starting X coordinate.
|
|
136
|
-
start_y: Starting Y coordinate.
|
|
137
|
-
end_x: Ending X coordinate.
|
|
138
|
-
end_y: Ending Y coordinate.
|
|
139
|
-
duration_ms: Duration of swipe in milliseconds (auto-calculated if None).
|
|
140
|
-
device_id: Optional ADB device ID.
|
|
141
|
-
delay: Delay in seconds after swipe. If None, uses configured default.
|
|
142
|
-
"""
|
|
143
|
-
if delay is None:
|
|
144
|
-
delay = TIMING_CONFIG.device.default_swipe_delay
|
|
145
|
-
|
|
146
|
-
adb_prefix = _get_adb_prefix(device_id)
|
|
147
|
-
|
|
148
|
-
if duration_ms is None:
|
|
149
|
-
# Calculate duration based on distance
|
|
150
|
-
dist_sq = (start_x - end_x) ** 2 + (start_y - end_y) ** 2
|
|
151
|
-
duration_ms = int(dist_sq / 1000)
|
|
152
|
-
duration_ms = max(1000, min(duration_ms, 2000)) # Clamp between 1000-2000ms
|
|
153
|
-
|
|
154
|
-
subprocess.run(
|
|
155
|
-
adb_prefix
|
|
156
|
-
+ [
|
|
157
|
-
"shell",
|
|
158
|
-
"input",
|
|
159
|
-
"swipe",
|
|
160
|
-
str(start_x),
|
|
161
|
-
str(start_y),
|
|
162
|
-
str(end_x),
|
|
163
|
-
str(end_y),
|
|
164
|
-
str(duration_ms),
|
|
165
|
-
],
|
|
166
|
-
capture_output=True,
|
|
167
|
-
)
|
|
168
|
-
time.sleep(delay)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def back(device_id: str | None = None, delay: float | None = None) -> None:
|
|
172
|
-
"""
|
|
173
|
-
Press the back button.
|
|
174
|
-
|
|
175
|
-
Args:
|
|
176
|
-
device_id: Optional ADB device ID.
|
|
177
|
-
delay: Delay in seconds after pressing back. If None, uses configured default.
|
|
178
|
-
"""
|
|
179
|
-
if delay is None:
|
|
180
|
-
delay = TIMING_CONFIG.device.default_back_delay
|
|
181
|
-
|
|
182
|
-
adb_prefix = _get_adb_prefix(device_id)
|
|
183
|
-
|
|
184
|
-
subprocess.run(
|
|
185
|
-
adb_prefix + ["shell", "input", "keyevent", "4"], capture_output=True
|
|
186
|
-
)
|
|
187
|
-
time.sleep(delay)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def home(device_id: str | None = None, delay: float | None = None) -> None:
|
|
191
|
-
"""
|
|
192
|
-
Press the home button.
|
|
193
|
-
|
|
194
|
-
Args:
|
|
195
|
-
device_id: Optional ADB device ID.
|
|
196
|
-
delay: Delay in seconds after pressing home. If None, uses configured default.
|
|
197
|
-
"""
|
|
198
|
-
if delay is None:
|
|
199
|
-
delay = TIMING_CONFIG.device.default_home_delay
|
|
200
|
-
|
|
201
|
-
adb_prefix = _get_adb_prefix(device_id)
|
|
202
|
-
|
|
203
|
-
subprocess.run(
|
|
204
|
-
adb_prefix + ["shell", "input", "keyevent", "KEYCODE_HOME"], capture_output=True
|
|
205
|
-
)
|
|
206
|
-
time.sleep(delay)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def launch_app(
|
|
210
|
-
app_name: str, device_id: str | None = None, delay: float | None = None
|
|
211
|
-
) -> bool:
|
|
212
|
-
"""
|
|
213
|
-
Launch an app by name.
|
|
214
|
-
|
|
215
|
-
Args:
|
|
216
|
-
app_name: The app name (must be in APP_PACKAGES).
|
|
217
|
-
device_id: Optional ADB device ID.
|
|
218
|
-
delay: Delay in seconds after launching. If None, uses configured default.
|
|
219
|
-
|
|
220
|
-
Returns:
|
|
221
|
-
True if app was launched, False if app not found.
|
|
222
|
-
"""
|
|
223
|
-
if delay is None:
|
|
224
|
-
delay = TIMING_CONFIG.device.default_launch_delay
|
|
225
|
-
|
|
226
|
-
if app_name not in APP_PACKAGES:
|
|
227
|
-
return False
|
|
228
|
-
|
|
229
|
-
adb_prefix = _get_adb_prefix(device_id)
|
|
230
|
-
package = APP_PACKAGES[app_name]
|
|
231
|
-
|
|
232
|
-
subprocess.run(
|
|
233
|
-
adb_prefix
|
|
234
|
-
+ [
|
|
235
|
-
"shell",
|
|
236
|
-
"monkey",
|
|
237
|
-
"-p",
|
|
238
|
-
package,
|
|
239
|
-
"-c",
|
|
240
|
-
"android.intent.category.LAUNCHER",
|
|
241
|
-
"1",
|
|
242
|
-
],
|
|
243
|
-
capture_output=True,
|
|
244
|
-
)
|
|
245
|
-
time.sleep(delay)
|
|
246
|
-
return True
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def _get_adb_prefix(device_id: str | None) -> list:
|
|
250
|
-
"""Get ADB command prefix with optional device specifier."""
|
|
251
|
-
if device_id:
|
|
252
|
-
return ["adb", "-s", device_id]
|
|
253
|
-
return ["adb"]
|
phone_agent/adb/input.py
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
"""Input utilities for Android device text input."""
|
|
2
|
-
|
|
3
|
-
import base64
|
|
4
|
-
import subprocess
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def type_text(text: str, device_id: str | None = None) -> None:
|
|
8
|
-
"""
|
|
9
|
-
Type text into the currently focused input field using ADB Keyboard.
|
|
10
|
-
|
|
11
|
-
Args:
|
|
12
|
-
text: The text to type.
|
|
13
|
-
device_id: Optional ADB device ID for multi-device setups.
|
|
14
|
-
|
|
15
|
-
Note:
|
|
16
|
-
Requires ADB Keyboard to be installed on the device.
|
|
17
|
-
See: https://github.com/nicnocquee/AdbKeyboard
|
|
18
|
-
"""
|
|
19
|
-
adb_prefix = _get_adb_prefix(device_id)
|
|
20
|
-
encoded_text = base64.b64encode(text.encode("utf-8")).decode("utf-8")
|
|
21
|
-
|
|
22
|
-
subprocess.run(
|
|
23
|
-
adb_prefix
|
|
24
|
-
+ [
|
|
25
|
-
"shell",
|
|
26
|
-
"am",
|
|
27
|
-
"broadcast",
|
|
28
|
-
"-a",
|
|
29
|
-
"ADB_INPUT_B64",
|
|
30
|
-
"--es",
|
|
31
|
-
"msg",
|
|
32
|
-
encoded_text,
|
|
33
|
-
],
|
|
34
|
-
capture_output=True,
|
|
35
|
-
text=True,
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def clear_text(device_id: str | None = None) -> None:
|
|
40
|
-
"""
|
|
41
|
-
Clear text in the currently focused input field.
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
device_id: Optional ADB device ID for multi-device setups.
|
|
45
|
-
"""
|
|
46
|
-
adb_prefix = _get_adb_prefix(device_id)
|
|
47
|
-
|
|
48
|
-
subprocess.run(
|
|
49
|
-
adb_prefix + ["shell", "am", "broadcast", "-a", "ADB_CLEAR_TEXT"],
|
|
50
|
-
capture_output=True,
|
|
51
|
-
text=True,
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def detect_and_set_adb_keyboard(device_id: str | None = None) -> str:
|
|
56
|
-
"""
|
|
57
|
-
Detect current keyboard and switch to ADB Keyboard if needed.
|
|
58
|
-
|
|
59
|
-
Args:
|
|
60
|
-
device_id: Optional ADB device ID for multi-device setups.
|
|
61
|
-
|
|
62
|
-
Returns:
|
|
63
|
-
The original keyboard IME identifier for later restoration.
|
|
64
|
-
"""
|
|
65
|
-
adb_prefix = _get_adb_prefix(device_id)
|
|
66
|
-
|
|
67
|
-
# Get current IME
|
|
68
|
-
result = subprocess.run(
|
|
69
|
-
adb_prefix + ["shell", "settings", "get", "secure", "default_input_method"],
|
|
70
|
-
capture_output=True,
|
|
71
|
-
text=True,
|
|
72
|
-
)
|
|
73
|
-
current_ime = (result.stdout + result.stderr).strip()
|
|
74
|
-
|
|
75
|
-
# Switch to ADB Keyboard if not already set
|
|
76
|
-
if "com.android.adbkeyboard/.AdbIME" not in current_ime:
|
|
77
|
-
subprocess.run(
|
|
78
|
-
adb_prefix + ["shell", "ime", "set", "com.android.adbkeyboard/.AdbIME"],
|
|
79
|
-
capture_output=True,
|
|
80
|
-
text=True,
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
# Warm up the keyboard
|
|
84
|
-
type_text("", device_id)
|
|
85
|
-
|
|
86
|
-
return current_ime
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def restore_keyboard(ime: str, device_id: str | None = None) -> None:
|
|
90
|
-
"""
|
|
91
|
-
Restore the original keyboard IME.
|
|
92
|
-
|
|
93
|
-
Args:
|
|
94
|
-
ime: The IME identifier to restore.
|
|
95
|
-
device_id: Optional ADB device ID for multi-device setups.
|
|
96
|
-
"""
|
|
97
|
-
adb_prefix = _get_adb_prefix(device_id)
|
|
98
|
-
|
|
99
|
-
subprocess.run(
|
|
100
|
-
adb_prefix + ["shell", "ime", "set", ime], capture_output=True, text=True
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def _get_adb_prefix(device_id: str | None) -> list:
|
|
105
|
-
"""Get ADB command prefix with optional device specifier."""
|
|
106
|
-
if device_id:
|
|
107
|
-
return ["adb", "-s", device_id]
|
|
108
|
-
return ["adb"]
|
phone_agent/adb/screenshot.py
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
"""Screenshot utilities for capturing Android device screen."""
|
|
2
|
-
|
|
3
|
-
import base64
|
|
4
|
-
import os
|
|
5
|
-
import subprocess
|
|
6
|
-
import tempfile
|
|
7
|
-
import uuid
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
from io import BytesIO
|
|
10
|
-
|
|
11
|
-
from PIL import Image
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass
|
|
15
|
-
class Screenshot:
|
|
16
|
-
"""Represents a captured screenshot."""
|
|
17
|
-
|
|
18
|
-
base64_data: str
|
|
19
|
-
width: int
|
|
20
|
-
height: int
|
|
21
|
-
is_sensitive: bool = False
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def get_screenshot(device_id: str | None = None, timeout: int = 10) -> Screenshot:
|
|
25
|
-
"""
|
|
26
|
-
Capture a screenshot from the connected Android device.
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
device_id: Optional ADB device ID for multi-device setups.
|
|
30
|
-
timeout: Timeout in seconds for screenshot operations.
|
|
31
|
-
|
|
32
|
-
Returns:
|
|
33
|
-
Screenshot object containing base64 data and dimensions.
|
|
34
|
-
|
|
35
|
-
Note:
|
|
36
|
-
If the screenshot fails (e.g., on sensitive screens like payment pages),
|
|
37
|
-
a black fallback image is returned with is_sensitive=True.
|
|
38
|
-
"""
|
|
39
|
-
temp_path = os.path.join(tempfile.gettempdir(), f"screenshot_{uuid.uuid4()}.png")
|
|
40
|
-
adb_prefix = _get_adb_prefix(device_id)
|
|
41
|
-
|
|
42
|
-
try:
|
|
43
|
-
# Execute screenshot command
|
|
44
|
-
result = subprocess.run(
|
|
45
|
-
adb_prefix + ["shell", "screencap", "-p", "/sdcard/tmp.png"],
|
|
46
|
-
capture_output=True,
|
|
47
|
-
text=True,
|
|
48
|
-
timeout=timeout,
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
# Check for screenshot failure (sensitive screen)
|
|
52
|
-
output = result.stdout + result.stderr
|
|
53
|
-
if "Status: -1" in output or "Failed" in output:
|
|
54
|
-
return _create_fallback_screenshot(is_sensitive=True)
|
|
55
|
-
|
|
56
|
-
# Pull screenshot to local temp path
|
|
57
|
-
subprocess.run(
|
|
58
|
-
adb_prefix + ["pull", "/sdcard/tmp.png", temp_path],
|
|
59
|
-
capture_output=True,
|
|
60
|
-
text=True,
|
|
61
|
-
timeout=5,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
if not os.path.exists(temp_path):
|
|
65
|
-
return _create_fallback_screenshot(is_sensitive=False)
|
|
66
|
-
|
|
67
|
-
# Read and encode image
|
|
68
|
-
img = Image.open(temp_path)
|
|
69
|
-
width, height = img.size
|
|
70
|
-
|
|
71
|
-
buffered = BytesIO()
|
|
72
|
-
img.save(buffered, format="PNG")
|
|
73
|
-
base64_data = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
|
74
|
-
|
|
75
|
-
# Cleanup
|
|
76
|
-
os.remove(temp_path)
|
|
77
|
-
|
|
78
|
-
return Screenshot(
|
|
79
|
-
base64_data=base64_data, width=width, height=height, is_sensitive=False
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
except Exception as e:
|
|
83
|
-
print(f"Screenshot error: {e}")
|
|
84
|
-
return _create_fallback_screenshot(is_sensitive=False)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _get_adb_prefix(device_id: str | None) -> list:
|
|
88
|
-
"""Get ADB command prefix with optional device specifier."""
|
|
89
|
-
if device_id:
|
|
90
|
-
return ["adb", "-s", device_id]
|
|
91
|
-
return ["adb"]
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def _create_fallback_screenshot(is_sensitive: bool) -> Screenshot:
|
|
95
|
-
"""Create a black fallback image when screenshot fails."""
|
|
96
|
-
default_width, default_height = 1080, 2400
|
|
97
|
-
|
|
98
|
-
black_img = Image.new("RGB", (default_width, default_height), color="black")
|
|
99
|
-
buffered = BytesIO()
|
|
100
|
-
black_img.save(buffered, format="PNG")
|
|
101
|
-
base64_data = base64.b64encode(buffered.getvalue()).decode("utf-8")
|
|
102
|
-
|
|
103
|
-
return Screenshot(
|
|
104
|
-
base64_data=base64_data,
|
|
105
|
-
width=default_width,
|
|
106
|
-
height=default_height,
|
|
107
|
-
is_sensitive=is_sensitive,
|
|
108
|
-
)
|