droidrun 0.3.2__py3-none-any.whl → 0.3.3__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 +7 -6
- 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/droid/droid_agent.py +15 -6
- droidrun/agent/planner/planner_agent.py +2 -2
- droidrun/agent/utils/executer.py +10 -2
- droidrun/agent/utils/trajectory.py +258 -11
- droidrun/cli/main.py +73 -36
- 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 +16 -17
- droidrun/telemetry/tracker.py +3 -2
- droidrun/tools/adb.py +668 -200
- droidrun/tools/ios.py +163 -163
- droidrun/tools/tools.py +32 -14
- {droidrun-0.3.2.dist-info → droidrun-0.3.3.dist-info}/METADATA +21 -8
- {droidrun-0.3.2.dist-info → droidrun-0.3.3.dist-info}/RECORD +25 -24
- 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 → droidrun-0.3.3.dist-info}/WHEEL +0 -0
- {droidrun-0.3.2.dist-info → droidrun-0.3.3.dist-info}/entry_points.txt +0 -0
- {droidrun-0.3.2.dist-info → droidrun-0.3.3.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
|
@@ -22,6 +23,7 @@ from droidrun.portal import (
|
|
22
23
|
PORTAL_PACKAGE_NAME,
|
23
24
|
ping_portal,
|
24
25
|
)
|
26
|
+
from droidrun.macro.cli import macro_cli
|
25
27
|
|
26
28
|
# Suppress all warnings
|
27
29
|
warnings.filterwarnings("ignore")
|
@@ -29,7 +31,6 @@ os.environ["TOKENIZERS_PARALLELISM"] = "false"
|
|
29
31
|
os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false"
|
30
32
|
|
31
33
|
console = Console()
|
32
|
-
device_manager = DeviceManager()
|
33
34
|
|
34
35
|
|
35
36
|
def configure_logging(goal: str, debug: bool):
|
@@ -38,7 +39,7 @@ def configure_logging(goal: str, debug: bool):
|
|
38
39
|
|
39
40
|
handler = LogHandler(goal)
|
40
41
|
handler.setFormatter(
|
41
|
-
logging.Formatter("%(levelname)s %(message)s", "%H:%M:%S")
|
42
|
+
logging.Formatter("%(levelname)s %(name)s %(message)s", "%H:%M:%S")
|
42
43
|
if debug
|
43
44
|
else logging.Formatter("%(message)s", "%H:%M:%S")
|
44
45
|
)
|
@@ -47,6 +48,12 @@ def configure_logging(goal: str, debug: bool):
|
|
47
48
|
logger.setLevel(logging.DEBUG if debug else logging.INFO)
|
48
49
|
logger.propagate = False
|
49
50
|
|
51
|
+
if debug:
|
52
|
+
tools_logger = logging.getLogger("droidrun-tools")
|
53
|
+
tools_logger.addHandler(handler)
|
54
|
+
tools_logger.propagate = False
|
55
|
+
tools_logger.setLevel(logging.DEBUG if debug else logging.INFO)
|
56
|
+
|
50
57
|
return handler
|
51
58
|
|
52
59
|
|
@@ -71,9 +78,11 @@ async def run_command(
|
|
71
78
|
reasoning: bool,
|
72
79
|
reflection: bool,
|
73
80
|
tracing: bool,
|
74
|
-
debug: bool,
|
81
|
+
debug: bool,
|
82
|
+
use_tcp: bool,
|
75
83
|
save_trajectory: bool = False,
|
76
84
|
ios: bool = False,
|
85
|
+
allow_drag: bool = False,
|
77
86
|
**kwargs,
|
78
87
|
):
|
79
88
|
"""Run a command on your Android device using natural language."""
|
@@ -95,8 +104,8 @@ async def run_command(
|
|
95
104
|
# Device setup
|
96
105
|
if device is None and not ios:
|
97
106
|
logger.info("🔍 Finding connected device...")
|
98
|
-
|
99
|
-
devices =
|
107
|
+
|
108
|
+
devices = adb.list()
|
100
109
|
if not devices:
|
101
110
|
raise ValueError("No connected devices found.")
|
102
111
|
device = devices[0].serial
|
@@ -108,7 +117,12 @@ async def run_command(
|
|
108
117
|
else:
|
109
118
|
logger.info(f"📱 Using device: {device}")
|
110
119
|
|
111
|
-
tools = AdbTools(serial=device) if not ios else IOSTools(url=device)
|
120
|
+
tools = AdbTools(serial=device, use_tcp=use_tcp) if not ios else IOSTools(url=device)
|
121
|
+
# Set excluded tools based on CLI flags
|
122
|
+
excluded_tools = [] if allow_drag else ["drag"]
|
123
|
+
|
124
|
+
# Select personas based on --drag flag
|
125
|
+
personas = [BIG_AGENT] if allow_drag else [DEFAULT]
|
112
126
|
|
113
127
|
# LLM setup
|
114
128
|
log_handler.update_step("Initializing LLM...")
|
@@ -130,10 +144,13 @@ async def run_command(
|
|
130
144
|
if tracing:
|
131
145
|
logger.info("🔍 Tracing enabled")
|
132
146
|
|
147
|
+
|
133
148
|
droid_agent = DroidAgent(
|
134
149
|
goal=command,
|
135
150
|
llm=llm,
|
136
151
|
tools=tools,
|
152
|
+
personas=personas,
|
153
|
+
excluded_tools=excluded_tools,
|
137
154
|
max_steps=steps,
|
138
155
|
timeout=1000,
|
139
156
|
vision=vision,
|
@@ -236,6 +253,9 @@ class DroidRunCLI(click.Group):
|
|
236
253
|
@click.option(
|
237
254
|
"--debug", is_flag=True, help="Enable verbose debug logging", default=False
|
238
255
|
)
|
256
|
+
@click.option(
|
257
|
+
"--use-tcp", is_flag=True, help="Use TCP communication for device control", default=False
|
258
|
+
)
|
239
259
|
@click.option(
|
240
260
|
"--save-trajectory",
|
241
261
|
is_flag=True,
|
@@ -256,6 +276,7 @@ def cli(
|
|
256
276
|
reflection: bool,
|
257
277
|
tracing: bool,
|
258
278
|
debug: bool,
|
279
|
+
use_tcp: bool,
|
259
280
|
save_trajectory: bool,
|
260
281
|
):
|
261
282
|
"""DroidRun - Control your Android device through LLM agents."""
|
@@ -311,12 +332,22 @@ def cli(
|
|
311
332
|
@click.option(
|
312
333
|
"--debug", is_flag=True, help="Enable verbose debug logging", default=False
|
313
334
|
)
|
335
|
+
@click.option(
|
336
|
+
"--use-tcp", is_flag=True, help="Use TCP communication for device control", default=False
|
337
|
+
)
|
314
338
|
@click.option(
|
315
339
|
"--save-trajectory",
|
316
340
|
is_flag=True,
|
317
341
|
help="Save agent trajectory to file",
|
318
342
|
default=False,
|
319
343
|
)
|
344
|
+
@click.option(
|
345
|
+
"--drag",
|
346
|
+
"allow_drag",
|
347
|
+
is_flag=True,
|
348
|
+
help="Enable drag tool",
|
349
|
+
default=False,
|
350
|
+
)
|
320
351
|
@click.option("--ios", is_flag=True, help="Run on iOS device", default=False)
|
321
352
|
def run(
|
322
353
|
command: str,
|
@@ -332,7 +363,9 @@ def run(
|
|
332
363
|
reflection: bool,
|
333
364
|
tracing: bool,
|
334
365
|
debug: bool,
|
366
|
+
use_tcp: bool,
|
335
367
|
save_trajectory: bool,
|
368
|
+
allow_drag: bool,
|
336
369
|
ios: bool,
|
337
370
|
):
|
338
371
|
"""Run a command on your Android device using natural language."""
|
@@ -350,18 +383,19 @@ def run(
|
|
350
383
|
reflection,
|
351
384
|
tracing,
|
352
385
|
debug,
|
386
|
+
use_tcp,
|
353
387
|
temperature=temperature,
|
354
388
|
save_trajectory=save_trajectory,
|
389
|
+
allow_drag=allow_drag,
|
355
390
|
ios=ios,
|
356
391
|
)
|
357
392
|
|
358
393
|
|
359
394
|
@cli.command()
|
360
|
-
|
361
|
-
async def devices():
|
395
|
+
def devices():
|
362
396
|
"""List connected Android devices."""
|
363
397
|
try:
|
364
|
-
devices =
|
398
|
+
devices = adb.list()
|
365
399
|
if not devices:
|
366
400
|
console.print("[yellow]No devices connected.[/]")
|
367
401
|
return
|
@@ -375,27 +409,24 @@ async def devices():
|
|
375
409
|
|
376
410
|
@cli.command()
|
377
411
|
@click.argument("serial")
|
378
|
-
|
379
|
-
@coro
|
380
|
-
async def connect(serial: str, port: int):
|
412
|
+
def connect(serial: str):
|
381
413
|
"""Connect to a device over TCP/IP."""
|
382
414
|
try:
|
383
|
-
device =
|
384
|
-
if device:
|
385
|
-
console.print(f"[green]Successfully connected to {serial}
|
415
|
+
device = adb.connect(serial)
|
416
|
+
if device.count("already connected"):
|
417
|
+
console.print(f"[green]Successfully connected to {serial}[/]")
|
386
418
|
else:
|
387
|
-
console.print(f"[red]Failed to connect to {serial}:{
|
419
|
+
console.print(f"[red]Failed to connect to {serial}: {device}[/]")
|
388
420
|
except Exception as e:
|
389
421
|
console.print(f"[red]Error connecting to device: {e}[/]")
|
390
422
|
|
391
423
|
|
392
424
|
@cli.command()
|
393
425
|
@click.argument("serial")
|
394
|
-
|
395
|
-
async def disconnect(serial: str):
|
426
|
+
def disconnect(serial: str):
|
396
427
|
"""Disconnect from a device."""
|
397
428
|
try:
|
398
|
-
success =
|
429
|
+
success = adb.disconnect(serial, raise_error=True)
|
399
430
|
if success:
|
400
431
|
console.print(f"[green]Successfully disconnected from {serial}[/]")
|
401
432
|
else:
|
@@ -410,12 +441,11 @@ async def disconnect(serial: str):
|
|
410
441
|
@click.option(
|
411
442
|
"--debug", is_flag=True, help="Enable verbose debug logging", default=False
|
412
443
|
)
|
413
|
-
|
414
|
-
async def setup(path: str | None, device: str | None, debug: bool):
|
444
|
+
def setup(path: str | None, device: str | None, debug: bool):
|
415
445
|
"""Install and enable the DroidRun Portal on a device."""
|
416
446
|
try:
|
417
447
|
if not device:
|
418
|
-
devices =
|
448
|
+
devices = adb.list()
|
419
449
|
if not devices:
|
420
450
|
console.print("[yellow]No devices connected.[/]")
|
421
451
|
return
|
@@ -423,7 +453,7 @@ async def setup(path: str | None, device: str | None, debug: bool):
|
|
423
453
|
device = devices[0].serial
|
424
454
|
console.print(f"[blue]Using device:[/] {device}")
|
425
455
|
|
426
|
-
device_obj =
|
456
|
+
device_obj = adb.device(device)
|
427
457
|
if not device_obj:
|
428
458
|
console.print(
|
429
459
|
f"[bold red]Error:[/] Could not get device object for {device}"
|
@@ -443,18 +473,18 @@ async def setup(path: str | None, device: str | None, debug: bool):
|
|
443
473
|
return
|
444
474
|
|
445
475
|
console.print(f"[bold blue]Step 1/2: Installing APK:[/] {apk_path}")
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
console.print(f"[bold red]Installation failed:[/] {
|
476
|
+
try:
|
477
|
+
device_obj.install(apk_path, uninstall=True, flags=["-g"], silent=not debug)
|
478
|
+
except Exception as e:
|
479
|
+
console.print(f"[bold red]Installation failed:[/] {e}")
|
450
480
|
return
|
451
|
-
|
452
|
-
|
481
|
+
|
482
|
+
console.print(f"[bold green]Installation successful![/]")
|
453
483
|
|
454
484
|
console.print(f"[bold blue]Step 2/2: Enabling accessibility service[/]")
|
455
485
|
|
456
486
|
try:
|
457
|
-
|
487
|
+
enable_portal_accessibility(device_obj)
|
458
488
|
|
459
489
|
console.print("[green]Accessibility service enabled successfully![/]")
|
460
490
|
console.print(
|
@@ -469,7 +499,7 @@ async def setup(path: str | None, device: str | None, debug: bool):
|
|
469
499
|
"[yellow]Opening accessibility settings for manual configuration...[/]"
|
470
500
|
)
|
471
501
|
|
472
|
-
|
502
|
+
device_obj.shell(
|
473
503
|
"am start -a android.settings.ACCESSIBILITY_SETTINGS"
|
474
504
|
)
|
475
505
|
|
@@ -503,16 +533,15 @@ async def setup(path: str | None, device: str | None, debug: bool):
|
|
503
533
|
@click.option(
|
504
534
|
"--debug", is_flag=True, help="Enable verbose debug logging", default=False
|
505
535
|
)
|
506
|
-
|
507
|
-
async def ping(device: str | None, debug: bool):
|
536
|
+
def ping(device: str | None, debug: bool):
|
508
537
|
"""Ping a device to check if it is ready and accessible."""
|
509
538
|
try:
|
510
|
-
device_obj =
|
539
|
+
device_obj = adb.device(device)
|
511
540
|
if not device_obj:
|
512
541
|
console.print(f"[bold red]Error:[/] Could not find device {device}")
|
513
542
|
return
|
514
543
|
|
515
|
-
|
544
|
+
ping_portal(device_obj, debug)
|
516
545
|
console.print(
|
517
546
|
"[bold green]Portal is installed and accessible. You're good to go![/]"
|
518
547
|
)
|
@@ -524,6 +553,10 @@ async def ping(device: str | None, debug: bool):
|
|
524
553
|
traceback.print_exc()
|
525
554
|
|
526
555
|
|
556
|
+
# Add macro commands as a subgroup
|
557
|
+
cli.add_command(macro_cli, name="macro")
|
558
|
+
|
559
|
+
|
527
560
|
if __name__ == "__main__":
|
528
561
|
command = "Open the settings app"
|
529
562
|
device = None
|
@@ -537,9 +570,11 @@ if __name__ == "__main__":
|
|
537
570
|
reflection = False
|
538
571
|
tracing = True
|
539
572
|
debug = True
|
573
|
+
use_tcp = True
|
540
574
|
base_url = None
|
541
575
|
api_base = None
|
542
576
|
ios = False
|
577
|
+
allow_drag = False
|
543
578
|
run_command(
|
544
579
|
command=command,
|
545
580
|
device=device,
|
@@ -552,8 +587,10 @@ if __name__ == "__main__":
|
|
552
587
|
reflection=reflection,
|
553
588
|
tracing=tracing,
|
554
589
|
debug=debug,
|
590
|
+
use_tcp=use_tcp,
|
555
591
|
base_url=base_url,
|
556
592
|
api_base=api_base,
|
557
593
|
api_key=api_key,
|
594
|
+
allow_drag=allow_drag,
|
558
595
|
ios=ios,
|
559
596
|
)
|
@@ -0,0 +1,14 @@
|
|
1
|
+
"""
|
2
|
+
DroidRun Macro Module - Record and replay UI automation sequences.
|
3
|
+
|
4
|
+
This module provides functionality to replay macro sequences that were
|
5
|
+
recorded during DroidAgent execution.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from .replay import MacroPlayer, replay_macro_file, replay_macro_folder
|
9
|
+
|
10
|
+
__all__ = [
|
11
|
+
"MacroPlayer",
|
12
|
+
"replay_macro_file",
|
13
|
+
"replay_macro_folder"
|
14
|
+
]
|