meccatronis commited on
Commit
1ec3d37
·
verified ·
1 Parent(s): 3b89f39

Upload core/adb_manager.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. core/adb_manager.py +535 -0
core/adb_manager.py ADDED
@@ -0,0 +1,535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ADB Manager Module
3
+ ==================
4
+
5
+ Handles all ADB (Android Debug Bridge) connections and operations.
6
+ Provides methods for device detection, connection management, and command execution.
7
+ """
8
+
9
+ import subprocess
10
+ import logging
11
+ import os
12
+ import time
13
+ import re
14
+ from typing import Optional, List, Dict, Tuple, Any
15
+ from dataclasses import dataclass
16
+ from enum import Enum
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class DeviceState(Enum):
22
+ """Device connection states"""
23
+ CONNECTED = "device"
24
+ UNAUTHORIZED = "unauthorized"
25
+ OFFLINE = "offline"
26
+ RECOVERY = "recovery"
27
+ SIDELOAD = "sideload"
28
+ BOOTLOADER = "bootloader"
29
+ UNKNOWN = "unknown"
30
+
31
+
32
+ @dataclass
33
+ class DeviceInfo:
34
+ """Data class for device information"""
35
+ serial: str
36
+ state: DeviceState
37
+ model: str = ""
38
+ manufacturer: str = ""
39
+ android_version: str = ""
40
+ sdk_version: str = ""
41
+ product: str = ""
42
+ is_rooted: bool = False
43
+ storage_total: int = 0
44
+ storage_free: int = 0
45
+
46
+
47
+ class ADBManager:
48
+ """
49
+ Manages ADB connections and operations for Android devices.
50
+
51
+ This class provides a high-level interface for:
52
+ - Device detection and connection
53
+ - File operations (push, pull, list)
54
+ - Shell command execution
55
+ - Device information retrieval
56
+ """
57
+
58
+ def __init__(self, adb_path: Optional[str] = None, timeout: int = 30):
59
+ """
60
+ Initialize ADB Manager.
61
+
62
+ Args:
63
+ adb_path: Path to ADB executable. If None, uses system PATH.
64
+ timeout: Default timeout for ADB operations in seconds.
65
+ """
66
+ self.adb_path = adb_path or self._find_adb()
67
+ self.timeout = timeout
68
+ self.current_device: Optional[str] = None
69
+ self._connected_devices: Dict[str, DeviceInfo] = {}
70
+
71
+ # Verify ADB is available
72
+ if not self._verify_adb():
73
+ raise RuntimeError("ADB not found. Please install Android SDK Platform Tools.")
74
+
75
+ def _find_adb(self) -> str:
76
+ """Find ADB executable in system PATH or common locations."""
77
+ # Check system PATH first
78
+ try:
79
+ result = subprocess.run(
80
+ ["which", "adb"],
81
+ capture_output=True,
82
+ text=True,
83
+ timeout=5
84
+ )
85
+ if result.returncode == 0:
86
+ return result.stdout.strip()
87
+ except Exception:
88
+ pass
89
+
90
+ # Check common locations
91
+ common_paths = [
92
+ "/usr/bin/adb",
93
+ "/usr/local/bin/adb",
94
+ os.path.expanduser("~/Android/Sdk/platform-tools/adb"),
95
+ os.path.expanduser("~/.android/platform-tools/adb"),
96
+ "/opt/android-sdk/platform-tools/adb",
97
+ ]
98
+
99
+ for path in common_paths:
100
+ if os.path.isfile(path) and os.access(path, os.X_OK):
101
+ return path
102
+
103
+ return "adb" # Fallback to PATH
104
+
105
+ def _verify_adb(self) -> bool:
106
+ """Verify ADB is installed and accessible."""
107
+ try:
108
+ result = subprocess.run(
109
+ [self.adb_path, "version"],
110
+ capture_output=True,
111
+ text=True,
112
+ timeout=10
113
+ )
114
+ return result.returncode == 0
115
+ except Exception as e:
116
+ logger.error(f"ADB verification failed: {e}")
117
+ return False
118
+
119
+ def _run_adb(self, args: List[str], device: Optional[str] = None,
120
+ timeout: Optional[int] = None) -> Tuple[int, str, str]:
121
+ """
122
+ Execute an ADB command.
123
+
124
+ Args:
125
+ args: List of command arguments
126
+ device: Target device serial (uses current_device if None)
127
+ timeout: Command timeout in seconds
128
+
129
+ Returns:
130
+ Tuple of (return_code, stdout, stderr)
131
+ """
132
+ cmd = [self.adb_path]
133
+
134
+ # Add device selector if specified
135
+ target_device = device or self.current_device
136
+ if target_device:
137
+ cmd.extend(["-s", target_device])
138
+
139
+ cmd.extend(args)
140
+
141
+ try:
142
+ result = subprocess.run(
143
+ cmd,
144
+ capture_output=True,
145
+ text=True,
146
+ timeout=timeout or self.timeout
147
+ )
148
+ return result.returncode, result.stdout, result.stderr
149
+ except subprocess.TimeoutExpired:
150
+ logger.error(f"ADB command timed out: {' '.join(cmd)}")
151
+ return -1, "", "Command timed out"
152
+ except Exception as e:
153
+ logger.error(f"ADB command failed: {e}")
154
+ return -1, "", str(e)
155
+
156
+ def start_server(self) -> bool:
157
+ """Start the ADB server."""
158
+ code, _, _ = self._run_adb(["start-server"])
159
+ return code == 0
160
+
161
+ def stop_server(self) -> bool:
162
+ """Stop the ADB server."""
163
+ code, _, _ = self._run_adb(["kill-server"])
164
+ return code == 0
165
+
166
+ def restart_server(self) -> bool:
167
+ """Restart the ADB server."""
168
+ self.stop_server()
169
+ time.sleep(1)
170
+ return self.start_server()
171
+
172
+ def list_devices(self) -> List[DeviceInfo]:
173
+ """
174
+ List all connected Android devices.
175
+
176
+ Returns:
177
+ List of DeviceInfo objects for connected devices
178
+ """
179
+ code, stdout, _ = self._run_adb(["devices", "-l"])
180
+
181
+ if code != 0:
182
+ return []
183
+
184
+ devices = []
185
+ lines = stdout.strip().split('\n')[1:] # Skip header
186
+
187
+ for line in lines:
188
+ if not line.strip():
189
+ continue
190
+
191
+ parts = line.split()
192
+ if len(parts) < 2:
193
+ continue
194
+
195
+ serial = parts[0]
196
+ state_str = parts[1]
197
+
198
+ # Parse state
199
+ try:
200
+ state = DeviceState(state_str)
201
+ except ValueError:
202
+ state = DeviceState.UNKNOWN
203
+
204
+ # Parse additional info from the line
205
+ model = ""
206
+ product = ""
207
+ for part in parts[2:]:
208
+ if part.startswith("model:"):
209
+ model = part.split(":")[1]
210
+ elif part.startswith("product:"):
211
+ product = part.split(":")[1]
212
+
213
+ device_info = DeviceInfo(
214
+ serial=serial,
215
+ state=state,
216
+ model=model,
217
+ product=product
218
+ )
219
+
220
+ # Get detailed info if device is connected
221
+ if state == DeviceState.CONNECTED:
222
+ device_info = self._get_device_details(serial, device_info)
223
+
224
+ devices.append(device_info)
225
+ self._connected_devices[serial] = device_info
226
+
227
+ return devices
228
+
229
+ def _get_device_details(self, serial: str, info: DeviceInfo) -> DeviceInfo:
230
+ """Get detailed information about a connected device."""
231
+ # Get manufacturer
232
+ code, stdout, _ = self._run_adb(
233
+ ["shell", "getprop", "ro.product.manufacturer"],
234
+ device=serial
235
+ )
236
+ if code == 0:
237
+ info.manufacturer = stdout.strip()
238
+
239
+ # Get Android version
240
+ code, stdout, _ = self._run_adb(
241
+ ["shell", "getprop", "ro.build.version.release"],
242
+ device=serial
243
+ )
244
+ if code == 0:
245
+ info.android_version = stdout.strip()
246
+
247
+ # Get SDK version
248
+ code, stdout, _ = self._run_adb(
249
+ ["shell", "getprop", "ro.build.version.sdk"],
250
+ device=serial
251
+ )
252
+ if code == 0:
253
+ info.sdk_version = stdout.strip()
254
+
255
+ # Check if rooted
256
+ info.is_rooted = self._check_root(serial)
257
+
258
+ # Get storage info
259
+ info.storage_total, info.storage_free = self._get_storage_info(serial)
260
+
261
+ return info
262
+
263
+ def _check_root(self, serial: str) -> bool:
264
+ """Check if device has root access."""
265
+ # Try su command
266
+ code, stdout, _ = self._run_adb(
267
+ ["shell", "su", "-c", "id"],
268
+ device=serial,
269
+ timeout=5
270
+ )
271
+ if code == 0 and "uid=0" in stdout:
272
+ return True
273
+
274
+ # Check for common root indicators
275
+ code, stdout, _ = self._run_adb(
276
+ ["shell", "ls", "/system/xbin/su"],
277
+ device=serial,
278
+ timeout=5
279
+ )
280
+ if code == 0 and "No such file" not in stdout:
281
+ return True
282
+
283
+ return False
284
+
285
+ def _get_storage_info(self, serial: str) -> Tuple[int, int]:
286
+ """Get storage information (total, free) in bytes."""
287
+ code, stdout, _ = self._run_adb(
288
+ ["shell", "df", "/sdcard"],
289
+ device=serial
290
+ )
291
+
292
+ if code != 0:
293
+ return 0, 0
294
+
295
+ lines = stdout.strip().split('\n')
296
+ if len(lines) < 2:
297
+ return 0, 0
298
+
299
+ # Parse df output
300
+ parts = lines[1].split()
301
+ if len(parts) >= 4:
302
+ try:
303
+ # Values are typically in KB
304
+ total = int(parts[1]) * 1024
305
+ free = int(parts[3]) * 1024
306
+ return total, free
307
+ except ValueError:
308
+ pass
309
+
310
+ return 0, 0
311
+
312
+ def connect_device(self, serial: str) -> bool:
313
+ """
314
+ Set the current device for operations.
315
+
316
+ Args:
317
+ serial: Device serial number
318
+
319
+ Returns:
320
+ True if device is connected and ready
321
+ """
322
+ devices = self.list_devices()
323
+ for device in devices:
324
+ if device.serial == serial and device.state == DeviceState.CONNECTED:
325
+ self.current_device = serial
326
+ logger.info(f"Connected to device: {serial}")
327
+ return True
328
+
329
+ logger.warning(f"Device not found or not ready: {serial}")
330
+ return False
331
+
332
+ def connect_wireless(self, ip: str, port: int = 5555) -> bool:
333
+ """
334
+ Connect to a device over WiFi.
335
+
336
+ Args:
337
+ ip: Device IP address
338
+ port: ADB port (default 5555)
339
+
340
+ Returns:
341
+ True if connection successful
342
+ """
343
+ address = f"{ip}:{port}"
344
+ code, stdout, _ = self._run_adb(["connect", address])
345
+
346
+ if code == 0 and "connected" in stdout.lower():
347
+ self.current_device = address
348
+ logger.info(f"Connected wirelessly to: {address}")
349
+ return True
350
+
351
+ return False
352
+
353
+ def disconnect_wireless(self, ip: Optional[str] = None) -> bool:
354
+ """Disconnect from wireless device."""
355
+ if ip:
356
+ code, _, _ = self._run_adb(["disconnect", ip])
357
+ else:
358
+ code, _, _ = self._run_adb(["disconnect"])
359
+ return code == 0
360
+
361
+ def shell(self, command: str, as_root: bool = False) -> Tuple[bool, str]:
362
+ """
363
+ Execute a shell command on the device.
364
+
365
+ Args:
366
+ command: Shell command to execute
367
+ as_root: Execute with root privileges
368
+
369
+ Returns:
370
+ Tuple of (success, output)
371
+ """
372
+ if as_root:
373
+ command = f"su -c '{command}'"
374
+
375
+ code, stdout, stderr = self._run_adb(["shell", command])
376
+
377
+ if code == 0:
378
+ return True, stdout
379
+ else:
380
+ return False, stderr or stdout
381
+
382
+ def pull_file(self, remote_path: str, local_path: str,
383
+ preserve_timestamp: bool = True) -> bool:
384
+ """
385
+ Pull a file from the device.
386
+
387
+ Args:
388
+ remote_path: Path on device
389
+ local_path: Local destination path
390
+ preserve_timestamp: Preserve file timestamps
391
+
392
+ Returns:
393
+ True if successful
394
+ """
395
+ args = ["pull"]
396
+ if preserve_timestamp:
397
+ args.append("-a")
398
+ args.extend([remote_path, local_path])
399
+
400
+ code, _, _ = self._run_adb(args)
401
+ return code == 0
402
+
403
+ def push_file(self, local_path: str, remote_path: str) -> bool:
404
+ """
405
+ Push a file to the device.
406
+
407
+ Args:
408
+ local_path: Local file path
409
+ remote_path: Destination path on device
410
+
411
+ Returns:
412
+ True if successful
413
+ """
414
+ code, _, _ = self._run_adb(["push", local_path, remote_path])
415
+ return code == 0
416
+
417
+ def list_directory(self, path: str, as_root: bool = False) -> List[Dict[str, Any]]:
418
+ """
419
+ List contents of a directory on the device.
420
+
421
+ Args:
422
+ path: Directory path on device
423
+ as_root: Use root privileges
424
+
425
+ Returns:
426
+ List of file/directory information dictionaries
427
+ """
428
+ command = f"ls -la {path}"
429
+ success, output = self.shell(command, as_root=as_root)
430
+
431
+ if not success:
432
+ return []
433
+
434
+ entries = []
435
+ for line in output.strip().split('\n'):
436
+ if not line or line.startswith('total'):
437
+ continue
438
+
439
+ # Parse ls -la output
440
+ parts = line.split()
441
+ if len(parts) < 8:
442
+ continue
443
+
444
+ entry = {
445
+ 'permissions': parts[0],
446
+ 'links': parts[1],
447
+ 'owner': parts[2],
448
+ 'group': parts[3],
449
+ 'size': int(parts[4]) if parts[4].isdigit() else 0,
450
+ 'date': f"{parts[5]} {parts[6]}",
451
+ 'name': ' '.join(parts[7:]),
452
+ 'is_directory': parts[0].startswith('d'),
453
+ 'is_link': parts[0].startswith('l'),
454
+ }
455
+ entries.append(entry)
456
+
457
+ return entries
458
+
459
+ def file_exists(self, path: str, as_root: bool = False) -> bool:
460
+ """Check if a file exists on the device."""
461
+ success, output = self.shell(f"[ -e {path} ] && echo 'exists'", as_root=as_root)
462
+ return success and 'exists' in output
463
+
464
+ def get_file_size(self, path: str, as_root: bool = False) -> int:
465
+ """Get the size of a file on the device."""
466
+ success, output = self.shell(f"stat -c %s {path}", as_root=as_root)
467
+ if success:
468
+ try:
469
+ return int(output.strip())
470
+ except ValueError:
471
+ pass
472
+ return 0
473
+
474
+ def create_backup(self, output_path: str, include_apk: bool = True,
475
+ include_shared: bool = True) -> bool:
476
+ """
477
+ Create a full device backup.
478
+
479
+ Args:
480
+ output_path: Path for backup file
481
+ include_apk: Include APK files
482
+ include_shared: Include shared storage
483
+
484
+ Returns:
485
+ True if backup started successfully
486
+ """
487
+ args = ["backup", "-f", output_path]
488
+ if include_apk:
489
+ args.append("-apk")
490
+ if include_shared:
491
+ args.append("-shared")
492
+ args.append("-all")
493
+
494
+ code, _, _ = self._run_adb(args, timeout=3600) # 1 hour timeout
495
+ return code == 0
496
+
497
+ def get_device_info(self) -> Optional[DeviceInfo]:
498
+ """Get information about the currently connected device."""
499
+ if self.current_device and self.current_device in self._connected_devices:
500
+ return self._connected_devices[self.current_device]
501
+ return None
502
+
503
+ def reboot(self, mode: str = "normal") -> bool:
504
+ """
505
+ Reboot the device.
506
+
507
+ Args:
508
+ mode: Reboot mode ('normal', 'recovery', 'bootloader')
509
+
510
+ Returns:
511
+ True if reboot command sent successfully
512
+ """
513
+ if mode == "normal":
514
+ code, _, _ = self._run_adb(["reboot"])
515
+ elif mode == "recovery":
516
+ code, _, _ = self._run_adb(["reboot", "recovery"])
517
+ elif mode == "bootloader":
518
+ code, _, _ = self._run_adb(["reboot", "bootloader"])
519
+ else:
520
+ return False
521
+
522
+ return code == 0
523
+
524
+ def wait_for_device(self, timeout: int = 60) -> bool:
525
+ """
526
+ Wait for a device to be connected.
527
+
528
+ Args:
529
+ timeout: Maximum wait time in seconds
530
+
531
+ Returns:
532
+ True if device connected within timeout
533
+ """
534
+ code, _, _ = self._run_adb(["wait-for-device"], timeout=timeout)
535
+ return code == 0