-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdevice_controller.py
More file actions
292 lines (218 loc) · 10 KB
/
Copy pathdevice_controller.py
File metadata and controls
292 lines (218 loc) · 10 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
#!/usr/bin/env python3
"""
Device Controller — Android System Automation
System-level control via ADB commands. No root required.
Covers navigation, volume, brightness, screenshot, notifications,
app management, connectivity, media, and device info.
"""
import subprocess
import time
import os
from typing import Optional
from config_loader import CONFIG
DEVICE_SERIAL = CONFIG.get("device_serial", "")
def _serial_flag() -> str:
return f"-s {DEVICE_SERIAL} " if DEVICE_SERIAL else ""
def adb(cmd: str, timeout: int = 30) -> str:
"""Execute ADB command and return stdout."""
full_cmd = f"adb {_serial_flag()}{cmd}"
result = subprocess.run(
full_cmd, shell=True, capture_output=True, text=True, timeout=timeout
)
return result.stdout.strip() + result.stderr.strip()
class DeviceController:
"""Android device system controller."""
def __init__(self, serial: str = ""):
self.serial = serial or DEVICE_SERIAL
# ── Navigation ──────────────────────────────────────────
def press_back(self):
adb("shell input keyevent 4")
def press_home(self):
adb("shell input keyevent 3")
def press_recent(self):
adb("shell input keyevent 187")
def lock_screen(self):
adb("shell input keyevent 26")
def wake_screen(self):
adb("shell input keyevent 26")
def is_screen_on(self) -> bool:
out = adb("shell dumpsys power | grep 'mWakefulness'")
return "Awake" in out or "ON" in out
# ── Volume ──────────────────────────────────────────────
def set_media_volume(self, level: int):
"""Set media volume (0-15 range typical)."""
adb(f"shell media volume --show --stream 3 --set {level}")
def set_ring_volume(self, level: int):
"""Set ring volume (0-7 range typical)."""
adb(f"shell cmd audio set-stream-volume 2 {level}")
def volume_up(self, times: int = 1):
for _ in range(times):
adb("shell input keyevent 24")
time.sleep(0.1)
def volume_down(self, times: int = 1):
for _ in range(times):
adb("shell input keyevent 25")
time.sleep(0.1)
def mute(self):
adb("shell cmd audio set-master-mute true")
def unmute(self):
adb("shell cmd audio set-master-mute false")
# ── Brightness ──────────────────────────────────────────
def set_brightness(self, level: int):
"""
Set screen brightness (0-255).
Disable auto-brightness first: set_auto_brightness(false)
"""
adb(f"shell settings put system screen_brightness {level}")
def get_brightness(self) -> int:
out = adb("shell settings get system screen_brightness")
try:
return int(out)
except ValueError:
return -1
def set_auto_brightness(self, enabled: bool):
val = 1 if enabled else 0
adb(f"shell settings put system screen_brightness_mode {val}")
# ── Screenshot & Screenrecord ───────────────────────────
def screenshot(self, path: Optional[str] = None) -> str:
"""Screenshot and pull to local. Returns local path."""
remote = "/sdcard/automation_screenshot.png"
adb(f"shell screencap -p {remote}")
if path is None:
path = f"screenshot_{int(time.time())}.png"
adb(f"pull {remote} {path}")
return path
def start_screenrecord(self, path: str = "/sdcard/automation_record.mp4"):
"""Start screenrecord (background, max 3 min)."""
adb("shell screenrecord --time-limit 180 " + path, timeout=5)
def stop_screenrecord(self):
adb("shell pkill -INT screenrecord")
# ── Notifications ───────────────────────────────────────
def open_notification(self):
adb("shell cmd statusbar expand-notifications")
def open_quick_settings(self):
adb("shell cmd statusbar expand-settings")
def collapse_statusbar(self):
adb("shell cmd statusbar collapse")
def clear_all_notifications(self):
adb("shell service call notification 1")
# ── App management ──────────────────────────────────────
def launch_app(self, package: str, activity: Optional[str] = None):
if activity:
adb(f"shell am start -n {package}/{activity}")
else:
adb(f"shell monkey -p {package} -c android.intent.category.LAUNCHER 1")
def force_stop(self, package: str):
adb(f"shell am force-stop {package}")
def clear_app_data(self, package: str):
adb(f"shell pm clear {package}")
def get_current_app(self) -> dict:
out = adb("shell dumpsys activity activities | grep mResumedActivity")
result = {"package": "", "activity": ""}
if "packageName=" in out:
result["package"] = out.split("packageName=")[1].split(",")[0].strip()
if "mResumedActivity" in out:
parts = out.split(" ")
for p in parts:
if "/" in p and "." in p:
result["activity"] = p.strip()
break
return result
def list_running_apps(self) -> list:
out = adb("shell ps -A | awk '{print $9}' | sort | uniq")
return [line for line in out.splitlines() if "." in line]
# ── Connectivity ────────────────────────────────────────
def set_wifi(self, enabled: bool):
val = "enable" if enabled else "disable"
adb(f"shell svc wifi {val}")
def set_bluetooth(self, enabled: bool):
val = "enable" if enabled else "disable"
adb(f"shell svc bluetooth {val}")
def set_airplane_mode(self, enabled: bool):
val = 1 if enabled else 0
adb(f"shell settings put global airplane_mode_on {val}")
adb("shell am broadcast -a android.intent.action.AIRPLANE_MODE")
# ── Media ───────────────────────────────────────────────
def media_play_pause(self):
adb("shell input keyevent 85")
def media_next(self):
adb("shell input keyevent 87")
def media_prev(self):
adb("shell input keyevent 88")
# ── Phone & SMS ─────────────────────────────────────────
def dial_number(self, number: str):
"""Open dialer (does not auto-call)."""
safe = number.replace(" ", "").replace("-", "")
adb(f'shell am start -a android.intent.action.DIAL -d "tel:{safe}"')
def open_sms_compose(self, number: str, body: str = ""):
safe = number.replace(" ", "").replace("-", "")
cmd = f'shell am start -a android.intent.action.SENDTO -d "sms:{safe}"'
if body:
cmd += f' --es sms_body "{body}"'
adb(cmd)
# ── IME ─────────────────────────────────────────────────
def set_ime(self, ime_id: str):
adb(f"shell ime set {ime_id}")
def get_current_ime(self) -> str:
return adb("shell settings get secure default_input_method")
def list_imes(self) -> list:
out = adb("shell ime list -a -s")
return [line.strip() for line in out.splitlines() if line.strip()]
# ── Device info ─────────────────────────────────────────
def get_battery(self) -> dict:
out = adb("shell dumpsys battery")
info = {}
for line in out.splitlines():
if "level:" in line:
info["level"] = int(line.split(":")[1].strip())
elif "status:" in line:
info["status"] = line.split(":")[1].strip()
elif "temperature:" in line:
info["temperature"] = int(line.split(":")[1].strip()) / 10
return info
def get_storage(self) -> dict:
out = adb("shell df /data | tail -1")
parts = out.split()
if len(parts) >= 4:
return {
"total": parts[1],
"used": parts[2],
"available": parts[3],
}
return {}
def get_memory(self) -> dict:
out = adb("shell cat /proc/meminfo | head -4")
info = {}
for line in out.splitlines():
if "MemTotal" in line:
info["total"] = line.split(":")[1].strip()
elif "MemFree" in line:
info["free"] = line.split(":")[1].strip()
elif "MemAvailable" in line:
info["available"] = line.split(":")[1].strip()
return info
# ── Self-test ─────────────────────────────────────────────
def self_test():
print("=" * 50)
print("Device Controller Self-Test")
print("=" * 50)
ctrl = DeviceController()
print("\n[Test 1] Device info")
print(f" Battery: {ctrl.get_battery()}")
print(f" Storage: {ctrl.get_storage()}")
print(f" Memory: {ctrl.get_memory()}")
print(f" Screen: {'On' if ctrl.is_screen_on() else 'Off'}")
print("[Test 1] OK")
print("\n[Test 2] IME list")
print(f" Current: {ctrl.get_current_ime()}")
print(f" Available: {ctrl.list_imes()}")
print("[Test 2] OK")
print("\n[Test 3] Screenshot")
path = ctrl.screenshot("device_test_screenshot.png")
print(f" Saved to: {path}")
print("[Test 3] OK")
print("\n" + "=" * 50)
print("All tests complete")
print("=" * 50)
if __name__ == "__main__":
self_test()