droidrun 0.3.2__py3-none-any.whl → 0.3.4__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.
- droidrun/__init__.py +6 -2
- droidrun/agent/codeact/codeact_agent.py +20 -14
- droidrun/agent/common/events.py +44 -1
- droidrun/agent/context/personas/__init__.py +2 -0
- droidrun/agent/context/personas/big_agent.py +96 -0
- droidrun/agent/context/personas/ui_expert.py +1 -0
- droidrun/agent/context/task_manager.py +8 -3
- droidrun/agent/droid/droid_agent.py +50 -16
- droidrun/agent/droid/events.py +1 -0
- droidrun/agent/planner/planner_agent.py +19 -14
- droidrun/agent/utils/chat_utils.py +1 -1
- droidrun/agent/utils/executer.py +17 -1
- droidrun/agent/utils/trajectory.py +258 -11
- droidrun/cli/main.py +108 -44
- droidrun/macro/__init__.py +14 -0
- droidrun/macro/__main__.py +10 -0
- droidrun/macro/cli.py +228 -0
- droidrun/macro/replay.py +309 -0
- droidrun/portal.py +37 -22
- droidrun/telemetry/events.py +1 -1
- droidrun/telemetry/tracker.py +3 -2
- droidrun/tools/adb.py +641 -185
- droidrun/tools/ios.py +163 -163
- droidrun/tools/tools.py +60 -14
- {droidrun-0.3.2.dist-info → droidrun-0.3.4.dist-info}/METADATA +20 -8
- droidrun-0.3.4.dist-info/RECORD +54 -0
- droidrun/adb/__init__.py +0 -13
- droidrun/adb/device.py +0 -345
- droidrun/adb/manager.py +0 -93
- droidrun/adb/wrapper.py +0 -226
- droidrun-0.3.2.dist-info/RECORD +0 -53
- {droidrun-0.3.2.dist-info → droidrun-0.3.4.dist-info}/WHEEL +0 -0
- {droidrun-0.3.2.dist-info → droidrun-0.3.4.dist-info}/entry_points.txt +0 -0
- {droidrun-0.3.2.dist-info → droidrun-0.3.4.dist-info}/licenses/LICENSE +0 -0
@@ -18,10 +18,24 @@ logger = logging.getLogger("droidrun")
|
|
18
18
|
|
19
19
|
class Trajectory:
|
20
20
|
|
21
|
-
def __init__(self):
|
22
|
-
"""Initializes an empty trajectory class.
|
21
|
+
def __init__(self, goal: str = None):
|
22
|
+
"""Initializes an empty trajectory class.
|
23
|
+
|
24
|
+
Args:
|
25
|
+
goal: The goal/prompt that this trajectory is trying to achieve
|
26
|
+
"""
|
23
27
|
self.events: List[Event] = []
|
24
28
|
self.screenshots: List[bytes] = []
|
29
|
+
self.macro: List[Event] = []
|
30
|
+
self.goal = goal or "DroidRun automation sequence"
|
31
|
+
|
32
|
+
def set_goal(self, goal: str) -> None:
|
33
|
+
"""Update the goal/description for this trajectory.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
goal: The new goal/prompt description
|
37
|
+
"""
|
38
|
+
self.goal = goal
|
25
39
|
|
26
40
|
|
27
41
|
def create_screenshot_gif(self, output_path: str, duration: int = 1000) -> str:
|
@@ -33,9 +47,10 @@ class Trajectory:
|
|
33
47
|
duration: Duration for each frame in milliseconds
|
34
48
|
|
35
49
|
Returns:
|
36
|
-
Path to the created GIF file
|
50
|
+
Path to the created GIF file, or None if no screenshots available
|
37
51
|
"""
|
38
52
|
if len(self.screenshots) == 0:
|
53
|
+
logger.info("📷 No screenshots available for GIF creation")
|
39
54
|
return None
|
40
55
|
|
41
56
|
images = []
|
@@ -62,17 +77,20 @@ class Trajectory:
|
|
62
77
|
) -> str:
|
63
78
|
"""
|
64
79
|
Save trajectory steps to a JSON file and create a GIF of screenshots if available.
|
80
|
+
Also saves the macro sequence as a separate file for replay.
|
81
|
+
Creates a dedicated folder for each trajectory containing all related files.
|
65
82
|
|
66
83
|
Args:
|
67
|
-
directory:
|
84
|
+
directory: Base directory to save the trajectory files
|
68
85
|
|
69
86
|
Returns:
|
70
|
-
Path to the
|
87
|
+
Path to the trajectory folder
|
71
88
|
"""
|
72
89
|
os.makedirs(directory, exist_ok=True)
|
73
90
|
|
74
91
|
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
75
|
-
|
92
|
+
trajectory_folder = os.path.join(directory, f"trajectory_{timestamp}")
|
93
|
+
os.makedirs(trajectory_folder, exist_ok=True)
|
76
94
|
|
77
95
|
def make_serializable(obj):
|
78
96
|
"""Recursively make objects JSON serializable."""
|
@@ -100,6 +118,7 @@ class Trajectory:
|
|
100
118
|
else:
|
101
119
|
return obj
|
102
120
|
|
121
|
+
# Save main trajectory events
|
103
122
|
serializable_events = []
|
104
123
|
for event in self.events:
|
105
124
|
event_dict = {
|
@@ -109,13 +128,209 @@ class Trajectory:
|
|
109
128
|
}
|
110
129
|
serializable_events.append(event_dict)
|
111
130
|
|
112
|
-
|
113
|
-
with open(
|
131
|
+
trajectory_json_path = os.path.join(trajectory_folder, "trajectory.json")
|
132
|
+
with open(trajectory_json_path, "w") as f:
|
114
133
|
json.dump(serializable_events, f, indent=2)
|
115
134
|
|
116
|
-
|
135
|
+
# Save macro sequence as a separate file for replay
|
136
|
+
if self.macro:
|
137
|
+
macro_data = []
|
138
|
+
for macro_event in self.macro:
|
139
|
+
macro_dict = {
|
140
|
+
"type": macro_event.__class__.__name__,
|
141
|
+
**{k: make_serializable(v) for k, v in macro_event.__dict__.items()
|
142
|
+
if not k.startswith('_')}
|
143
|
+
}
|
144
|
+
macro_data.append(macro_dict)
|
145
|
+
|
146
|
+
macro_json_path = os.path.join(trajectory_folder, "macro.json")
|
147
|
+
with open(macro_json_path, "w") as f:
|
148
|
+
json.dump({
|
149
|
+
"version": "1.0",
|
150
|
+
"description": self.goal,
|
151
|
+
"timestamp": timestamp,
|
152
|
+
"total_actions": len(macro_data),
|
153
|
+
"actions": macro_data
|
154
|
+
}, f, indent=2)
|
155
|
+
|
156
|
+
logger.info(f"💾 Saved macro sequence with {len(macro_data)} actions to {macro_json_path}")
|
157
|
+
|
158
|
+
# Create screenshot GIF
|
159
|
+
gif_path = self.create_screenshot_gif(os.path.join(trajectory_folder, "screenshots"))
|
160
|
+
if gif_path:
|
161
|
+
logger.info(f"🎬 Saved screenshot GIF to {gif_path}")
|
162
|
+
|
163
|
+
logger.info(f"📁 Trajectory saved to folder: {trajectory_folder}")
|
164
|
+
return trajectory_folder
|
165
|
+
|
166
|
+
@staticmethod
|
167
|
+
def load_trajectory_folder(trajectory_folder: str) -> Dict[str, Any]:
|
168
|
+
"""
|
169
|
+
Load trajectory data from a trajectory folder.
|
170
|
+
|
171
|
+
Args:
|
172
|
+
trajectory_folder: Path to the trajectory folder
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
Dictionary containing trajectory data, macro data, and file paths
|
176
|
+
"""
|
177
|
+
result = {
|
178
|
+
"trajectory_data": None,
|
179
|
+
"macro_data": None,
|
180
|
+
"gif_path": None,
|
181
|
+
"folder_path": trajectory_folder
|
182
|
+
}
|
183
|
+
|
184
|
+
try:
|
185
|
+
# Load main trajectory
|
186
|
+
trajectory_json_path = os.path.join(trajectory_folder, "trajectory.json")
|
187
|
+
if os.path.exists(trajectory_json_path):
|
188
|
+
with open(trajectory_json_path, "r") as f:
|
189
|
+
result["trajectory_data"] = json.load(f)
|
190
|
+
logger.info(f"📖 Loaded trajectory data from {trajectory_json_path}")
|
191
|
+
|
192
|
+
# Load macro sequence
|
193
|
+
macro_json_path = os.path.join(trajectory_folder, "macro.json")
|
194
|
+
if os.path.exists(macro_json_path):
|
195
|
+
with open(macro_json_path, "r") as f:
|
196
|
+
result["macro_data"] = json.load(f)
|
197
|
+
logger.info(f"📖 Loaded macro data from {macro_json_path}")
|
198
|
+
|
199
|
+
# Check for GIF
|
200
|
+
gif_path = os.path.join(trajectory_folder, "screenshots.gif")
|
201
|
+
if os.path.exists(gif_path):
|
202
|
+
result["gif_path"] = gif_path
|
203
|
+
logger.info(f"🎬 Found screenshot GIF at {gif_path}")
|
204
|
+
|
205
|
+
return result
|
206
|
+
|
207
|
+
except Exception as e:
|
208
|
+
logger.error(f"❌ Error loading trajectory folder {trajectory_folder}: {e}")
|
209
|
+
return result
|
210
|
+
|
211
|
+
@staticmethod
|
212
|
+
def load_macro_sequence(macro_file_path: str) -> Dict[str, Any]:
|
213
|
+
"""
|
214
|
+
Load a macro sequence from a saved macro file.
|
215
|
+
|
216
|
+
Args:
|
217
|
+
macro_file_path: Path to the macro JSON file (can be full path or trajectory folder)
|
218
|
+
|
219
|
+
Returns:
|
220
|
+
Dictionary containing the macro sequence data
|
221
|
+
"""
|
222
|
+
# Check if it's a folder path - if so, look for macro.json inside
|
223
|
+
if os.path.isdir(macro_file_path):
|
224
|
+
macro_file_path = os.path.join(macro_file_path, "macro.json")
|
225
|
+
|
226
|
+
try:
|
227
|
+
with open(macro_file_path, "r") as f:
|
228
|
+
macro_data = json.load(f)
|
229
|
+
|
230
|
+
logger.info(f"📖 Loaded macro sequence with {macro_data.get('total_actions', 0)} actions from {macro_file_path}")
|
231
|
+
return macro_data
|
232
|
+
except FileNotFoundError:
|
233
|
+
logger.error(f"❌ Macro file not found: {macro_file_path}")
|
234
|
+
return {}
|
235
|
+
except json.JSONDecodeError as e:
|
236
|
+
logger.error(f"❌ Error parsing macro file {macro_file_path}: {e}")
|
237
|
+
return {}
|
238
|
+
|
239
|
+
@staticmethod
|
240
|
+
def get_macro_summary(macro_data: Dict[str, Any]) -> Dict[str, Any]:
|
241
|
+
"""
|
242
|
+
Get a summary of a macro sequence.
|
243
|
+
|
244
|
+
Args:
|
245
|
+
macro_data: The macro data dictionary
|
246
|
+
|
247
|
+
Returns:
|
248
|
+
Dictionary with statistics about the macro
|
249
|
+
"""
|
250
|
+
if not macro_data or "actions" not in macro_data:
|
251
|
+
return {"error": "Invalid macro data"}
|
252
|
+
|
253
|
+
actions = macro_data["actions"]
|
254
|
+
|
255
|
+
# Count action types
|
256
|
+
action_types = {}
|
257
|
+
for action in actions:
|
258
|
+
action_type = action.get("action_type", "unknown")
|
259
|
+
action_types[action_type] = action_types.get(action_type, 0) + 1
|
260
|
+
|
261
|
+
# Calculate duration if timestamps are available
|
262
|
+
timestamps = [action.get("timestamp") for action in actions if action.get("timestamp")]
|
263
|
+
duration = max(timestamps) - min(timestamps) if len(timestamps) > 1 else 0
|
264
|
+
|
265
|
+
return {
|
266
|
+
"version": macro_data.get("version", "unknown"),
|
267
|
+
"description": macro_data.get("description", "No description"),
|
268
|
+
"total_actions": len(actions),
|
269
|
+
"action_types": action_types,
|
270
|
+
"duration_seconds": round(duration, 2) if duration > 0 else None,
|
271
|
+
"timestamp": macro_data.get("timestamp", "unknown")
|
272
|
+
}
|
117
273
|
|
118
|
-
|
274
|
+
@staticmethod
|
275
|
+
def print_macro_summary(macro_file_path: str) -> None:
|
276
|
+
"""
|
277
|
+
Print a summary of a macro sequence.
|
278
|
+
|
279
|
+
Args:
|
280
|
+
macro_file_path: Path to the macro JSON file or trajectory folder
|
281
|
+
"""
|
282
|
+
macro_data = Trajectory.load_macro_sequence(macro_file_path)
|
283
|
+
if not macro_data:
|
284
|
+
print("❌ Could not load macro data")
|
285
|
+
return
|
286
|
+
|
287
|
+
summary = Trajectory.get_macro_summary(macro_data)
|
288
|
+
|
289
|
+
print("=== Macro Summary ===")
|
290
|
+
print(f"File: {macro_file_path}")
|
291
|
+
print(f"Version: {summary.get('version', 'unknown')}")
|
292
|
+
print(f"Description: {summary.get('description', 'No description')}")
|
293
|
+
print(f"Timestamp: {summary.get('timestamp', 'unknown')}")
|
294
|
+
print(f"Total actions: {summary.get('total_actions', 0)}")
|
295
|
+
if summary.get('duration_seconds'):
|
296
|
+
print(f"Duration: {summary['duration_seconds']} seconds")
|
297
|
+
print("Action breakdown:")
|
298
|
+
for action_type, count in summary.get('action_types', {}).items():
|
299
|
+
print(f" - {action_type}: {count}")
|
300
|
+
print("=====================")
|
301
|
+
|
302
|
+
@staticmethod
|
303
|
+
def print_trajectory_folder_summary(trajectory_folder: str) -> None:
|
304
|
+
"""
|
305
|
+
Print a comprehensive summary of a trajectory folder.
|
306
|
+
|
307
|
+
Args:
|
308
|
+
trajectory_folder: Path to the trajectory folder
|
309
|
+
"""
|
310
|
+
folder_data = Trajectory.load_trajectory_folder(trajectory_folder)
|
311
|
+
|
312
|
+
print("=== Trajectory Folder Summary ===")
|
313
|
+
print(f"Folder: {trajectory_folder}")
|
314
|
+
print(f"Trajectory data: {'✅ Available' if folder_data['trajectory_data'] else '❌ Missing'}")
|
315
|
+
print(f"Macro data: {'✅ Available' if folder_data['macro_data'] else '❌ Missing'}")
|
316
|
+
print(f"Screenshot GIF: {'✅ Available' if folder_data['gif_path'] else '❌ Missing'}")
|
317
|
+
|
318
|
+
if folder_data['macro_data']:
|
319
|
+
print("\n--- Macro Summary ---")
|
320
|
+
summary = Trajectory.get_macro_summary(folder_data['macro_data'])
|
321
|
+
print(f"Description: {summary.get('description', 'No description')}")
|
322
|
+
print(f"Total actions: {summary.get('total_actions', 0)}")
|
323
|
+
if summary.get('duration_seconds'):
|
324
|
+
print(f"Duration: {summary['duration_seconds']} seconds")
|
325
|
+
print("Action breakdown:")
|
326
|
+
for action_type, count in summary.get('action_types', {}).items():
|
327
|
+
print(f" - {action_type}: {count}")
|
328
|
+
|
329
|
+
if folder_data['trajectory_data']:
|
330
|
+
print(f"\n--- Trajectory Summary ---")
|
331
|
+
print(f"Total events: {len(folder_data['trajectory_data'])}")
|
332
|
+
|
333
|
+
print("=================================")
|
119
334
|
|
120
335
|
def get_trajectory_statistics(trajectory_data: Dict[str, Any]) -> Dict[str, Any]:
|
121
336
|
"""
|
@@ -181,4 +396,36 @@ class Trajectory:
|
|
181
396
|
print(f"Execution steps: {stats['execution_steps']}")
|
182
397
|
print(f"Successful executions: {stats['successful_executions']}")
|
183
398
|
print(f"Failed executions: {stats['failed_executions']}")
|
184
|
-
print("==========================")
|
399
|
+
print("==========================")
|
400
|
+
|
401
|
+
|
402
|
+
# Example usage:
|
403
|
+
"""
|
404
|
+
# Save a trajectory with a specific goal (automatically creates folder structure)
|
405
|
+
trajectory = Trajectory(goal="Open settings and check battery level")
|
406
|
+
# ... add events and screenshots to trajectory ...
|
407
|
+
folder_path = trajectory.save_trajectory()
|
408
|
+
|
409
|
+
# Or update the goal later
|
410
|
+
trajectory.set_goal("Navigate to Settings and find device info")
|
411
|
+
|
412
|
+
# Load entire trajectory folder
|
413
|
+
folder_data = Trajectory.load_trajectory_folder(folder_path)
|
414
|
+
trajectory_events = folder_data['trajectory_data']
|
415
|
+
macro_actions = folder_data['macro_data']
|
416
|
+
gif_path = folder_data['gif_path']
|
417
|
+
|
418
|
+
# Load just the macro from folder
|
419
|
+
macro_data = Trajectory.load_macro_sequence(folder_path)
|
420
|
+
|
421
|
+
# Print summaries
|
422
|
+
Trajectory.print_trajectory_folder_summary(folder_path)
|
423
|
+
Trajectory.print_macro_summary(folder_path)
|
424
|
+
|
425
|
+
# Example folder structure created:
|
426
|
+
# trajectories/
|
427
|
+
# └── trajectory_20250108_143052/
|
428
|
+
# ├── trajectory.json # Full trajectory events
|
429
|
+
# ├── macro.json # Macro sequence with goal as description
|
430
|
+
# └── screenshots.gif # Screenshot animation
|
431
|
+
"""
|
droidrun/cli/main.py
CHANGED
@@ -9,10 +9,11 @@ import logging
|
|
9
9
|
import warnings
|
10
10
|
from contextlib import nullcontext
|
11
11
|
from rich.console import Console
|
12
|
+
from adbutils import adb
|
12
13
|
from droidrun.agent.droid import DroidAgent
|
13
14
|
from droidrun.agent.utils.llm_picker import load_llm
|
14
|
-
from droidrun.adb import DeviceManager
|
15
15
|
from droidrun.tools import AdbTools, IOSTools
|
16
|
+
from droidrun.agent.context.personas import DEFAULT, BIG_AGENT
|
16
17
|
from functools import wraps
|
17
18
|
from droidrun.cli.logs import LogHandler
|
18
19
|
from droidrun.telemetry import print_telemetry_message
|
@@ -21,7 +22,10 @@ from droidrun.portal import (
|
|
21
22
|
enable_portal_accessibility,
|
22
23
|
PORTAL_PACKAGE_NAME,
|
23
24
|
ping_portal,
|
25
|
+
ping_portal_tcp,
|
26
|
+
ping_portal_content,
|
24
27
|
)
|
28
|
+
from droidrun.macro.cli import macro_cli
|
25
29
|
|
26
30
|
# Suppress all warnings
|
27
31
|
warnings.filterwarnings("ignore")
|
@@ -29,7 +33,6 @@ os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
29
33
|
os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false"
|
30
34
|
|
31
35
|
console = Console()
|
32
|
-
device_manager = DeviceManager()
|
33
36
|
|
34
37
|
|
35
38
|
def configure_logging(goal: str, debug: bool):
|
@@ -38,7 +41,7 @@ def configure_logging(goal: str, debug: bool):
|
|
38
41
|
|
39
42
|
handler = LogHandler(goal)
|
40
43
|
handler.setFormatter(
|
41
|
-
logging.Formatter("%(levelname)s %(message)s", "%H:%M:%S")
|
44
|
+
logging.Formatter("%(levelname)s %(name)s %(message)s", "%H:%M:%S")
|
42
45
|
if debug
|
43
46
|
else logging.Formatter("%(message)s", "%H:%M:%S")
|
44
47
|
)
|
@@ -47,6 +50,12 @@ def configure_logging(goal: str, debug: bool):
|
|
47
50
|
logger.setLevel(logging.DEBUG if debug else logging.INFO)
|
48
51
|
logger.propagate = False
|
49
52
|
|
53
|
+
if debug:
|
54
|
+
tools_logger = logging.getLogger("droidrun-tools")
|
55
|
+
tools_logger.addHandler(handler)
|
56
|
+
tools_logger.propagate = False
|
57
|
+
tools_logger.setLevel(logging.DEBUG if debug else logging.INFO)
|
58
|
+
|
50
59
|
return handler
|
51
60
|
|
52
61
|
|
@@ -72,8 +81,10 @@ async def run_command(
|
|
72
81
|
reflection: bool,
|
73
82
|
tracing: bool,
|
74
83
|
debug: bool,
|
75
|
-
|
84
|
+
use_tcp: bool,
|
85
|
+
save_trajectory: str = "none",
|
76
86
|
ios: bool = False,
|
87
|
+
allow_drag: bool = False,
|
77
88
|
**kwargs,
|
78
89
|
):
|
79
90
|
"""Run a command on your Android device using natural language."""
|
@@ -95,8 +106,8 @@ async def run_command(
|
|
95
106
|
# Device setup
|
96
107
|
if device is None and not ios:
|
97
108
|
logger.info("🔍 Finding connected device...")
|
98
|
-
|
99
|
-
devices =
|
109
|
+
|
110
|
+
devices = adb.list()
|
100
111
|
if not devices:
|
101
112
|
raise ValueError("No connected devices found.")
|
102
113
|
device = devices[0].serial
|
@@ -108,7 +119,16 @@ async def run_command(
|
|
108
119
|
else:
|
109
120
|
logger.info(f"📱 Using device: {device}")
|
110
121
|
|
111
|
-
tools =
|
122
|
+
tools = (
|
123
|
+
AdbTools(serial=device, use_tcp=use_tcp)
|
124
|
+
if not ios
|
125
|
+
else IOSTools(url=device)
|
126
|
+
)
|
127
|
+
# Set excluded tools based on CLI flags
|
128
|
+
excluded_tools = [] if allow_drag else ["drag"]
|
129
|
+
|
130
|
+
# Select personas based on --drag flag
|
131
|
+
personas = [BIG_AGENT] if allow_drag else [DEFAULT]
|
112
132
|
|
113
133
|
# LLM setup
|
114
134
|
log_handler.update_step("Initializing LLM...")
|
@@ -134,6 +154,8 @@ async def run_command(
|
|
134
154
|
goal=command,
|
135
155
|
llm=llm,
|
136
156
|
tools=tools,
|
157
|
+
personas=personas,
|
158
|
+
excluded_tools=excluded_tools,
|
137
159
|
max_steps=steps,
|
138
160
|
timeout=1000,
|
139
161
|
vision=vision,
|
@@ -237,11 +259,17 @@ class DroidRunCLI(click.Group):
|
|
237
259
|
"--debug", is_flag=True, help="Enable verbose debug logging", default=False
|
238
260
|
)
|
239
261
|
@click.option(
|
240
|
-
"--
|
262
|
+
"--use-tcp",
|
241
263
|
is_flag=True,
|
242
|
-
help="
|
264
|
+
help="Use TCP communication for device control",
|
243
265
|
default=False,
|
244
266
|
)
|
267
|
+
@click.option(
|
268
|
+
"--save-trajectory",
|
269
|
+
type=click.Choice(["none", "step", "action"]),
|
270
|
+
help="Trajectory saving level: none (no saving), step (save per step), action (save per action)",
|
271
|
+
default="none",
|
272
|
+
)
|
245
273
|
@click.group(cls=DroidRunCLI)
|
246
274
|
def cli(
|
247
275
|
device: str | None,
|
@@ -256,7 +284,8 @@ def cli(
|
|
256
284
|
reflection: bool,
|
257
285
|
tracing: bool,
|
258
286
|
debug: bool,
|
259
|
-
|
287
|
+
use_tcp: bool,
|
288
|
+
save_trajectory: str,
|
260
289
|
):
|
261
290
|
"""DroidRun - Control your Android device through LLM agents."""
|
262
291
|
pass
|
@@ -311,10 +340,23 @@ def cli(
|
|
311
340
|
@click.option(
|
312
341
|
"--debug", is_flag=True, help="Enable verbose debug logging", default=False
|
313
342
|
)
|
343
|
+
@click.option(
|
344
|
+
"--use-tcp",
|
345
|
+
is_flag=True,
|
346
|
+
help="Use TCP communication for device control",
|
347
|
+
default=False,
|
348
|
+
)
|
314
349
|
@click.option(
|
315
350
|
"--save-trajectory",
|
351
|
+
type=click.Choice(["none", "step", "action"]),
|
352
|
+
help="Trajectory saving level: none (no saving), step (save per step), action (save per action)",
|
353
|
+
default="none",
|
354
|
+
)
|
355
|
+
@click.option(
|
356
|
+
"--drag",
|
357
|
+
"allow_drag",
|
316
358
|
is_flag=True,
|
317
|
-
help="
|
359
|
+
help="Enable drag tool",
|
318
360
|
default=False,
|
319
361
|
)
|
320
362
|
@click.option("--ios", is_flag=True, help="Run on iOS device", default=False)
|
@@ -332,7 +374,9 @@ def run(
|
|
332
374
|
reflection: bool,
|
333
375
|
tracing: bool,
|
334
376
|
debug: bool,
|
335
|
-
|
377
|
+
use_tcp: bool,
|
378
|
+
save_trajectory: str,
|
379
|
+
allow_drag: bool,
|
336
380
|
ios: bool,
|
337
381
|
):
|
338
382
|
"""Run a command on your Android device using natural language."""
|
@@ -350,18 +394,19 @@ def run(
|
|
350
394
|
reflection,
|
351
395
|
tracing,
|
352
396
|
debug,
|
397
|
+
use_tcp,
|
353
398
|
temperature=temperature,
|
354
399
|
save_trajectory=save_trajectory,
|
400
|
+
allow_drag=allow_drag,
|
355
401
|
ios=ios,
|
356
402
|
)
|
357
403
|
|
358
404
|
|
359
405
|
@cli.command()
|
360
|
-
|
361
|
-
async def devices():
|
406
|
+
def devices():
|
362
407
|
"""List connected Android devices."""
|
363
408
|
try:
|
364
|
-
devices =
|
409
|
+
devices = adb.list()
|
365
410
|
if not devices:
|
366
411
|
console.print("[yellow]No devices connected.[/]")
|
367
412
|
return
|
@@ -375,27 +420,24 @@ async def devices():
|
|
375
420
|
|
376
421
|
@cli.command()
|
377
422
|
@click.argument("serial")
|
378
|
-
|
379
|
-
@coro
|
380
|
-
async def connect(serial: str, port: int):
|
423
|
+
def connect(serial: str):
|
381
424
|
"""Connect to a device over TCP/IP."""
|
382
425
|
try:
|
383
|
-
device =
|
384
|
-
if device:
|
385
|
-
console.print(f"[green]Successfully connected to {serial}
|
426
|
+
device = adb.connect(serial)
|
427
|
+
if device.count("already connected"):
|
428
|
+
console.print(f"[green]Successfully connected to {serial}[/]")
|
386
429
|
else:
|
387
|
-
console.print(f"[red]Failed to connect to {serial}:{
|
430
|
+
console.print(f"[red]Failed to connect to {serial}: {device}[/]")
|
388
431
|
except Exception as e:
|
389
432
|
console.print(f"[red]Error connecting to device: {e}[/]")
|
390
433
|
|
391
434
|
|
392
435
|
@cli.command()
|
393
436
|
@click.argument("serial")
|
394
|
-
|
395
|
-
async def disconnect(serial: str):
|
437
|
+
def disconnect(serial: str):
|
396
438
|
"""Disconnect from a device."""
|
397
439
|
try:
|
398
|
-
success =
|
440
|
+
success = adb.disconnect(serial, raise_error=True)
|
399
441
|
if success:
|
400
442
|
console.print(f"[green]Successfully disconnected from {serial}[/]")
|
401
443
|
else:
|
@@ -406,16 +448,19 @@ async def disconnect(serial: str):
|
|
406
448
|
|
407
449
|
@cli.command()
|
408
450
|
@click.option("--device", "-d", help="Device serial number or IP address", default=None)
|
409
|
-
@click.option(
|
451
|
+
@click.option(
|
452
|
+
"--path",
|
453
|
+
help="Path to the Droidrun Portal APK to install on the device. If not provided, the latest portal apk version will be downloaded and installed.",
|
454
|
+
default=None,
|
455
|
+
)
|
410
456
|
@click.option(
|
411
457
|
"--debug", is_flag=True, help="Enable verbose debug logging", default=False
|
412
458
|
)
|
413
|
-
|
414
|
-
async def setup(path: str | None, device: str | None, debug: bool):
|
459
|
+
def setup(path: str | None, device: str | None, debug: bool):
|
415
460
|
"""Install and enable the DroidRun Portal on a device."""
|
416
461
|
try:
|
417
462
|
if not device:
|
418
|
-
devices =
|
463
|
+
devices = adb.list()
|
419
464
|
if not devices:
|
420
465
|
console.print("[yellow]No devices connected.[/]")
|
421
466
|
return
|
@@ -423,7 +468,7 @@ async def setup(path: str | None, device: str | None, debug: bool):
|
|
423
468
|
device = devices[0].serial
|
424
469
|
console.print(f"[blue]Using device:[/] {device}")
|
425
470
|
|
426
|
-
device_obj =
|
471
|
+
device_obj = adb.device(device)
|
427
472
|
if not device_obj:
|
428
473
|
console.print(
|
429
474
|
f"[bold red]Error:[/] Could not get device object for {device}"
|
@@ -443,18 +488,20 @@ async def setup(path: str | None, device: str | None, debug: bool):
|
|
443
488
|
return
|
444
489
|
|
445
490
|
console.print(f"[bold blue]Step 1/2: Installing APK:[/] {apk_path}")
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
491
|
+
try:
|
492
|
+
device_obj.install(
|
493
|
+
apk_path, uninstall=True, flags=["-g"], silent=not debug
|
494
|
+
)
|
495
|
+
except Exception as e:
|
496
|
+
console.print(f"[bold red]Installation failed:[/] {e}")
|
450
497
|
return
|
451
|
-
|
452
|
-
|
498
|
+
|
499
|
+
console.print(f"[bold green]Installation successful![/]")
|
453
500
|
|
454
501
|
console.print(f"[bold blue]Step 2/2: Enabling accessibility service[/]")
|
455
502
|
|
456
503
|
try:
|
457
|
-
|
504
|
+
enable_portal_accessibility(device_obj)
|
458
505
|
|
459
506
|
console.print("[green]Accessibility service enabled successfully![/]")
|
460
507
|
console.print(
|
@@ -469,9 +516,7 @@ async def setup(path: str | None, device: str | None, debug: bool):
|
|
469
516
|
"[yellow]Opening accessibility settings for manual configuration...[/]"
|
470
517
|
)
|
471
518
|
|
472
|
-
|
473
|
-
"am start -a android.settings.ACCESSIBILITY_SETTINGS"
|
474
|
-
)
|
519
|
+
device_obj.shell("am start -a android.settings.ACCESSIBILITY_SETTINGS")
|
475
520
|
|
476
521
|
console.print(
|
477
522
|
"\n[yellow]Please complete the following steps on your device:[/]"
|
@@ -500,19 +545,30 @@ async def setup(path: str | None, device: str | None, debug: bool):
|
|
500
545
|
|
501
546
|
@cli.command()
|
502
547
|
@click.option("--device", "-d", help="Device serial number or IP address", default=None)
|
548
|
+
@click.option(
|
549
|
+
"--use-tcp",
|
550
|
+
is_flag=True,
|
551
|
+
help="Use TCP communication for device control",
|
552
|
+
default=False,
|
553
|
+
)
|
503
554
|
@click.option(
|
504
555
|
"--debug", is_flag=True, help="Enable verbose debug logging", default=False
|
505
556
|
)
|
506
|
-
|
507
|
-
async def ping(device: str | None, debug: bool):
|
557
|
+
def ping(device: str | None, use_tcp: bool, debug: bool):
|
508
558
|
"""Ping a device to check if it is ready and accessible."""
|
509
559
|
try:
|
510
|
-
device_obj =
|
560
|
+
device_obj = adb.device(device)
|
511
561
|
if not device_obj:
|
512
562
|
console.print(f"[bold red]Error:[/] Could not find device {device}")
|
513
563
|
return
|
514
564
|
|
515
|
-
|
565
|
+
ping_portal(device_obj, debug)
|
566
|
+
|
567
|
+
if use_tcp:
|
568
|
+
ping_portal_tcp(device_obj, debug)
|
569
|
+
else:
|
570
|
+
ping_portal_content(device_obj, debug)
|
571
|
+
|
516
572
|
console.print(
|
517
573
|
"[bold green]Portal is installed and accessible. You're good to go![/]"
|
518
574
|
)
|
@@ -524,6 +580,10 @@ async def ping(device: str | None, debug: bool):
|
|
524
580
|
traceback.print_exc()
|
525
581
|
|
526
582
|
|
583
|
+
# Add macro commands as a subgroup
|
584
|
+
cli.add_command(macro_cli, name="macro")
|
585
|
+
|
586
|
+
|
527
587
|
if __name__ == "__main__":
|
528
588
|
command = "Open the settings app"
|
529
589
|
device = None
|
@@ -537,9 +597,11 @@ if __name__ == "__main__":
|
|
537
597
|
reflection = False
|
538
598
|
tracing = True
|
539
599
|
debug = True
|
600
|
+
use_tcp = True
|
540
601
|
base_url = None
|
541
602
|
api_base = None
|
542
603
|
ios = False
|
604
|
+
allow_drag = False
|
543
605
|
run_command(
|
544
606
|
command=command,
|
545
607
|
device=device,
|
@@ -552,8 +614,10 @@ if __name__ == "__main__":
|
|
552
614
|
reflection=reflection,
|
553
615
|
tracing=tracing,
|
554
616
|
debug=debug,
|
617
|
+
use_tcp=use_tcp,
|
555
618
|
base_url=base_url,
|
556
619
|
api_base=api_base,
|
557
620
|
api_key=api_key,
|
621
|
+
allow_drag=allow_drag,
|
558
622
|
ios=ios,
|
559
623
|
)
|