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
droidrun/macro/cli.py
ADDED
@@ -0,0 +1,228 @@
|
|
1
|
+
"""
|
2
|
+
Command-line interface for DroidRun macro replay.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import asyncio
|
6
|
+
import click
|
7
|
+
import logging
|
8
|
+
import os
|
9
|
+
from typing import Optional
|
10
|
+
from rich.console import Console
|
11
|
+
from rich.table import Table
|
12
|
+
from droidrun.macro.replay import MacroPlayer, replay_macro_file, replay_macro_folder
|
13
|
+
from droidrun.agent.utils.trajectory import Trajectory
|
14
|
+
from adbutils import adb
|
15
|
+
|
16
|
+
console = Console()
|
17
|
+
|
18
|
+
|
19
|
+
def configure_logging(debug: bool = False):
|
20
|
+
"""Configure logging for the macro CLI."""
|
21
|
+
logger = logging.getLogger("droidrun-macro")
|
22
|
+
logger.handlers = []
|
23
|
+
|
24
|
+
handler = logging.StreamHandler()
|
25
|
+
|
26
|
+
if debug:
|
27
|
+
level = logging.DEBUG
|
28
|
+
formatter = logging.Formatter("%(levelname)s %(name)s %(message)s", "%H:%M:%S")
|
29
|
+
else:
|
30
|
+
level = logging.INFO
|
31
|
+
formatter = logging.Formatter("%(message)s", "%H:%M:%S")
|
32
|
+
|
33
|
+
handler.setFormatter(formatter)
|
34
|
+
logger.addHandler(handler)
|
35
|
+
logger.setLevel(level)
|
36
|
+
logger.propagate = False
|
37
|
+
|
38
|
+
return logger
|
39
|
+
|
40
|
+
|
41
|
+
@click.group()
|
42
|
+
def macro_cli():
|
43
|
+
"""Replay recorded automation sequences."""
|
44
|
+
pass
|
45
|
+
|
46
|
+
|
47
|
+
@macro_cli.command()
|
48
|
+
@click.argument("path", type=click.Path(exists=True))
|
49
|
+
@click.option("--device", "-d", help="Device serial number", default=None)
|
50
|
+
@click.option("--delay", "-t", help="Delay between actions (seconds)", default=1.0, type=float)
|
51
|
+
@click.option("--start-from", "-s", help="Start from step number (1-based)", default=1, type=int)
|
52
|
+
@click.option("--max-steps", "-m", help="Maximum steps to execute", default=None, type=int)
|
53
|
+
@click.option("--debug", is_flag=True, help="Enable debug logging", default=False)
|
54
|
+
@click.option("--dry-run", is_flag=True, help="Show actions without executing", default=False)
|
55
|
+
def replay(path: str, device: Optional[str], delay: float, start_from: int, max_steps: Optional[int], debug: bool, dry_run: bool):
|
56
|
+
"""Replay a macro from a file or trajectory folder."""
|
57
|
+
logger = configure_logging(debug)
|
58
|
+
|
59
|
+
logger.info("🎬 DroidRun Macro Replay")
|
60
|
+
|
61
|
+
# Convert start_from from 1-based to 0-based
|
62
|
+
start_from_zero = max(0, start_from - 1)
|
63
|
+
|
64
|
+
if device is None:
|
65
|
+
logger.info("🔍 Finding connected device...")
|
66
|
+
devices = adb.list()
|
67
|
+
if not devices:
|
68
|
+
raise ValueError("No connected devices found.")
|
69
|
+
device = devices[0].serial
|
70
|
+
logger.info(f"📱 Using device: {device}")
|
71
|
+
else:
|
72
|
+
logger.info(f"📱 Using device: {device}")
|
73
|
+
|
74
|
+
asyncio.run(_replay_async(path, device, delay, start_from_zero, max_steps, dry_run, logger))
|
75
|
+
|
76
|
+
|
77
|
+
async def _replay_async(path: str, device: str, delay: float, start_from: int, max_steps: Optional[int], dry_run: bool, logger: logging.Logger):
|
78
|
+
"""Async function to handle macro replay."""
|
79
|
+
try:
|
80
|
+
if os.path.isfile(path):
|
81
|
+
logger.info(f"📄 Loading macro from file: {path}")
|
82
|
+
player = MacroPlayer(device_serial=device, delay_between_actions=delay)
|
83
|
+
macro_data = player.load_macro_from_file(path)
|
84
|
+
elif os.path.isdir(path):
|
85
|
+
logger.info(f"📁 Loading macro from folder: {path}")
|
86
|
+
player = MacroPlayer(device_serial=device, delay_between_actions=delay)
|
87
|
+
macro_data = player.load_macro_from_folder(path)
|
88
|
+
else:
|
89
|
+
logger.error(f"❌ Invalid path: {path}")
|
90
|
+
return
|
91
|
+
|
92
|
+
if not macro_data:
|
93
|
+
logger.error("❌ Failed to load macro data")
|
94
|
+
return
|
95
|
+
|
96
|
+
# Show macro information
|
97
|
+
description = macro_data.get("description", "No description")
|
98
|
+
total_actions = macro_data.get("total_actions", 0)
|
99
|
+
version = macro_data.get("version", "unknown")
|
100
|
+
|
101
|
+
logger.info("📋 Macro Information:")
|
102
|
+
logger.info(f" Description: {description}")
|
103
|
+
logger.info(f" Version: {version}")
|
104
|
+
logger.info(f" Total actions: {total_actions}")
|
105
|
+
logger.info(f" Device: {device}")
|
106
|
+
logger.info(f" Delay between actions: {delay}s")
|
107
|
+
|
108
|
+
if start_from > 0:
|
109
|
+
logger.info(f" Starting from step: {start_from + 1}")
|
110
|
+
if max_steps:
|
111
|
+
logger.info(f" Maximum steps: {max_steps}")
|
112
|
+
|
113
|
+
if dry_run:
|
114
|
+
logger.info("🔍 DRY RUN MODE - Actions will be shown but not executed")
|
115
|
+
await _show_dry_run(macro_data, start_from, max_steps, logger)
|
116
|
+
else:
|
117
|
+
logger.info("▶️ Starting macro replay...")
|
118
|
+
success = await player.replay_macro(macro_data, start_from_step=start_from, max_steps=max_steps)
|
119
|
+
|
120
|
+
if success:
|
121
|
+
logger.info("🎉 Macro replay completed successfully!")
|
122
|
+
else:
|
123
|
+
logger.error("💥 Macro replay completed with errors")
|
124
|
+
|
125
|
+
except Exception as e:
|
126
|
+
logger.error(f"💥 Error: {e}")
|
127
|
+
if logger.isEnabledFor(logging.DEBUG):
|
128
|
+
import traceback
|
129
|
+
logger.debug(traceback.format_exc())
|
130
|
+
|
131
|
+
|
132
|
+
async def _show_dry_run(macro_data: dict, start_from: int, max_steps: Optional[int], logger: logging.Logger):
|
133
|
+
"""Show what actions would be executed in dry run mode."""
|
134
|
+
actions = macro_data.get("actions", [])
|
135
|
+
|
136
|
+
# Apply filters
|
137
|
+
if start_from > 0:
|
138
|
+
actions = actions[start_from:]
|
139
|
+
if max_steps:
|
140
|
+
actions = actions[:max_steps]
|
141
|
+
|
142
|
+
logger.info(f"📋 Found {len(actions)} actions to execute:")
|
143
|
+
|
144
|
+
table = Table(title="Actions to Execute")
|
145
|
+
table.add_column("Step", style="cyan")
|
146
|
+
table.add_column("Type", style="green")
|
147
|
+
table.add_column("Details", style="white")
|
148
|
+
table.add_column("Description", style="yellow")
|
149
|
+
|
150
|
+
for i, action in enumerate(actions, start=start_from + 1):
|
151
|
+
action_type = action.get("action_type", action.get("type", "unknown"))
|
152
|
+
details = ""
|
153
|
+
|
154
|
+
if action_type == "tap":
|
155
|
+
x, y = action.get("x", 0), action.get("y", 0)
|
156
|
+
element_text = action.get("element_text", "")
|
157
|
+
details = f"({x}, {y}) - '{element_text}'"
|
158
|
+
elif action_type == "swipe":
|
159
|
+
start_x, start_y = action.get("start_x", 0), action.get("start_y", 0)
|
160
|
+
end_x, end_y = action.get("end_x", 0), action.get("end_y", 0)
|
161
|
+
details = f"({start_x}, {start_y}) → ({end_x}, {end_y})"
|
162
|
+
elif action_type == "input_text":
|
163
|
+
text = action.get("text", "")
|
164
|
+
details = f"'{text}'"
|
165
|
+
elif action_type == "key_press":
|
166
|
+
key_name = action.get("key_name", "UNKNOWN")
|
167
|
+
details = f"{key_name}"
|
168
|
+
|
169
|
+
description = action.get("description", "")
|
170
|
+
table.add_row(str(i), action_type, details, description[:50] + "..." if len(description) > 50 else description)
|
171
|
+
|
172
|
+
# Still use console for table display as it's structured data
|
173
|
+
console.print(table)
|
174
|
+
|
175
|
+
|
176
|
+
@macro_cli.command()
|
177
|
+
@click.argument("directory", type=click.Path(exists=True), default="trajectories")
|
178
|
+
@click.option("--debug", is_flag=True, help="Enable debug logging", default=False)
|
179
|
+
def list(directory: str, debug: bool):
|
180
|
+
"""List available trajectory folders in a directory."""
|
181
|
+
logger = configure_logging(debug)
|
182
|
+
|
183
|
+
logger.info(f"📁 Scanning directory: {directory}")
|
184
|
+
|
185
|
+
try:
|
186
|
+
folders = []
|
187
|
+
for item in os.listdir(directory):
|
188
|
+
item_path = os.path.join(directory, item)
|
189
|
+
if os.path.isdir(item_path):
|
190
|
+
macro_file = os.path.join(item_path, "macro.json")
|
191
|
+
if os.path.exists(macro_file):
|
192
|
+
# Load macro info
|
193
|
+
try:
|
194
|
+
macro_data = Trajectory.load_macro_sequence(item_path)
|
195
|
+
description = macro_data.get("description", "No description")
|
196
|
+
total_actions = macro_data.get("total_actions", 0)
|
197
|
+
folders.append((item, description, total_actions))
|
198
|
+
except Exception as e:
|
199
|
+
logger.debug(f"Error loading macro from {item}: {e}")
|
200
|
+
folders.append((item, "Error loading", 0))
|
201
|
+
|
202
|
+
if not folders:
|
203
|
+
logger.info("📭 No trajectory folders found")
|
204
|
+
return
|
205
|
+
|
206
|
+
logger.info(f"🎯 Found {len(folders)} trajectory(s):")
|
207
|
+
|
208
|
+
table = Table(title=f"Available Trajectories in {directory}")
|
209
|
+
table.add_column("Folder", style="cyan")
|
210
|
+
table.add_column("Description", style="white")
|
211
|
+
table.add_column("Actions", style="green")
|
212
|
+
|
213
|
+
for folder, description, actions in sorted(folders):
|
214
|
+
table.add_row(folder, description[:80] + "..." if len(description) > 80 else description, str(actions))
|
215
|
+
|
216
|
+
# Still use console for table display as it's structured data
|
217
|
+
console.print(table)
|
218
|
+
logger.info(f"💡 Use 'droidrun-macro replay {directory}/<folder>' to replay a trajectory")
|
219
|
+
|
220
|
+
except Exception as e:
|
221
|
+
logger.error(f"💥 Error: {e}")
|
222
|
+
if logger.isEnabledFor(logging.DEBUG):
|
223
|
+
import traceback
|
224
|
+
logger.debug(traceback.format_exc())
|
225
|
+
|
226
|
+
|
227
|
+
if __name__ == "__main__":
|
228
|
+
macro_cli()
|
droidrun/macro/replay.py
ADDED
@@ -0,0 +1,309 @@
|
|
1
|
+
"""
|
2
|
+
Macro Replay Module - Replay recorded UI automation sequences.
|
3
|
+
|
4
|
+
This module provides functionality to load and replay macro JSON files
|
5
|
+
that were generated during DroidAgent trajectory recording.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import json
|
9
|
+
import asyncio
|
10
|
+
import logging
|
11
|
+
import time
|
12
|
+
import os
|
13
|
+
from typing import Dict, List, Any, Optional
|
14
|
+
from droidrun.tools.adb import AdbTools
|
15
|
+
from droidrun.agent.utils.trajectory import Trajectory
|
16
|
+
|
17
|
+
logger = logging.getLogger("droidrun-macro")
|
18
|
+
|
19
|
+
|
20
|
+
class MacroPlayer:
|
21
|
+
"""
|
22
|
+
A class for loading and replaying DroidRun macro sequences.
|
23
|
+
|
24
|
+
This player can execute recorded UI actions (taps, swipes, text input, key presses)
|
25
|
+
on Android devices using AdbTools.
|
26
|
+
"""
|
27
|
+
|
28
|
+
def __init__(self, device_serial: str = None, delay_between_actions: float = 1.0):
|
29
|
+
"""
|
30
|
+
Initialize the MacroPlayer.
|
31
|
+
|
32
|
+
Args:
|
33
|
+
device_serial: Serial number of the target device. If None, will use first available device.
|
34
|
+
delay_between_actions: Delay in seconds between each action (default: 1.0s)
|
35
|
+
"""
|
36
|
+
self.device_serial = device_serial
|
37
|
+
self.delay_between_actions = delay_between_actions
|
38
|
+
self.adb_tools = None
|
39
|
+
|
40
|
+
def _initialize_tools(self) -> AdbTools:
|
41
|
+
"""Initialize ADB tools for the target device."""
|
42
|
+
if self.adb_tools is None:
|
43
|
+
self.adb_tools = AdbTools(serial=self.device_serial)
|
44
|
+
logger.info(f"🤖 Initialized ADB tools for device: {self.device_serial}")
|
45
|
+
return self.adb_tools
|
46
|
+
|
47
|
+
def load_macro_from_file(self, macro_file_path: str) -> Dict[str, Any]:
|
48
|
+
"""
|
49
|
+
Load macro data from a JSON file.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
macro_file_path: Path to the macro JSON file
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
Dictionary containing the macro data
|
56
|
+
"""
|
57
|
+
return Trajectory.load_macro_sequence(macro_file_path)
|
58
|
+
|
59
|
+
def load_macro_from_folder(self, trajectory_folder: str) -> Dict[str, Any]:
|
60
|
+
"""
|
61
|
+
Load macro data from a trajectory folder.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
trajectory_folder: Path to the trajectory folder containing macro.json
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
Dictionary containing the macro data
|
68
|
+
"""
|
69
|
+
return Trajectory.load_macro_sequence(trajectory_folder)
|
70
|
+
|
71
|
+
def replay_action(self, action: Dict[str, Any]) -> bool:
|
72
|
+
"""
|
73
|
+
Replay a single action.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
action: Action dictionary containing type and parameters
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
True if action was executed successfully, False otherwise
|
80
|
+
"""
|
81
|
+
tools = self._initialize_tools()
|
82
|
+
action_type = action.get("action_type", action.get("type", "unknown"))
|
83
|
+
|
84
|
+
try:
|
85
|
+
|
86
|
+
if action_type == "start_app":
|
87
|
+
package = action.get("package")
|
88
|
+
activity = action.get("activity", None)
|
89
|
+
tools.start_app(package, activity)
|
90
|
+
return True
|
91
|
+
|
92
|
+
elif action_type == "tap":
|
93
|
+
x = action.get("x", 0)
|
94
|
+
y = action.get("y", 0)
|
95
|
+
element_text = action.get("element_text", "")
|
96
|
+
|
97
|
+
logger.info(f"🫰 Tapping at ({x}, {y}) - Element: '{element_text}'")
|
98
|
+
result = tools.tap_by_coordinates(x, y)
|
99
|
+
logger.debug(f" Result: {result}")
|
100
|
+
return True
|
101
|
+
|
102
|
+
elif action_type == "swipe":
|
103
|
+
start_x = action.get("start_x", 0)
|
104
|
+
start_y = action.get("start_y", 0)
|
105
|
+
end_x = action.get("end_x", 0)
|
106
|
+
end_y = action.get("end_y", 0)
|
107
|
+
duration_ms = action.get("duration_ms", 300)
|
108
|
+
|
109
|
+
logger.info(
|
110
|
+
f"👆 Swiping from ({start_x}, {start_y}) to ({end_x}, {end_y}) in {duration_ms} milliseconds"
|
111
|
+
)
|
112
|
+
result = tools.swipe(start_x, start_y, end_x, end_y, duration_ms)
|
113
|
+
logger.debug(f" Result: {result}")
|
114
|
+
return True
|
115
|
+
|
116
|
+
elif action_type == "drag":
|
117
|
+
start_x = action.get("start_x", 0)
|
118
|
+
start_y = action.get("start_y", 0)
|
119
|
+
end_x = action.get("end_x", 0)
|
120
|
+
end_y = action.get("end_y", 0)
|
121
|
+
duration_ms = action.get("duration_ms", 300)
|
122
|
+
|
123
|
+
logger.info(
|
124
|
+
f"👆 Dragging from ({start_x}, {start_y}) to ({end_x}, {end_y}) in {duration_ms} milliseconds"
|
125
|
+
)
|
126
|
+
result = tools.drag(start_x, start_y, end_x, end_y, duration_ms)
|
127
|
+
logger.debug(f" Result: {result}")
|
128
|
+
return True
|
129
|
+
|
130
|
+
elif action_type == "input_text":
|
131
|
+
text = action.get("text", "")
|
132
|
+
|
133
|
+
logger.info(f"⌨️ Inputting text: '{text}'")
|
134
|
+
result = tools.input_text(text)
|
135
|
+
logger.debug(f" Result: {result}")
|
136
|
+
return True
|
137
|
+
|
138
|
+
elif action_type == "key_press":
|
139
|
+
keycode = action.get("keycode", 0)
|
140
|
+
key_name = action.get("key_name", "UNKNOWN")
|
141
|
+
|
142
|
+
logger.info(f"🔘 Pressing key: {key_name} (keycode: {keycode})")
|
143
|
+
result = tools.press_key(keycode)
|
144
|
+
logger.debug(f" Result: {result}")
|
145
|
+
return True
|
146
|
+
|
147
|
+
elif action_type == "back":
|
148
|
+
logger.info(f"⬅️ Pressing back button")
|
149
|
+
result = tools.back()
|
150
|
+
logger.debug(f" Result: {result}")
|
151
|
+
return True
|
152
|
+
|
153
|
+
else:
|
154
|
+
logger.warning(f"⚠️ Unknown action type: {action_type}")
|
155
|
+
return False
|
156
|
+
|
157
|
+
except Exception as e:
|
158
|
+
logger.error(f"❌ Error executing action {action_type}: {e}")
|
159
|
+
return False
|
160
|
+
|
161
|
+
async def replay_macro(
|
162
|
+
self,
|
163
|
+
macro_data: Dict[str, Any],
|
164
|
+
start_from_step: int = 0,
|
165
|
+
max_steps: Optional[int] = None,
|
166
|
+
) -> bool:
|
167
|
+
"""
|
168
|
+
Replay a complete macro sequence.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
macro_data: Macro data dictionary loaded from JSON
|
172
|
+
start_from_step: Step number to start from (0-based, default: 0)
|
173
|
+
max_steps: Maximum number of steps to execute (default: all)
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
True if all actions were executed successfully, False otherwise
|
177
|
+
"""
|
178
|
+
if not macro_data or "actions" not in macro_data:
|
179
|
+
logger.error("❌ Invalid macro data - no actions found")
|
180
|
+
return False
|
181
|
+
|
182
|
+
actions = macro_data["actions"]
|
183
|
+
description = macro_data.get("description", "Unknown macro")
|
184
|
+
total_actions = len(actions)
|
185
|
+
|
186
|
+
# Apply start_from_step and max_steps filters
|
187
|
+
if start_from_step > 0:
|
188
|
+
actions = actions[start_from_step:]
|
189
|
+
logger.info(f"📍 Starting from step {start_from_step + 1}")
|
190
|
+
|
191
|
+
if max_steps is not None:
|
192
|
+
actions = actions[:max_steps]
|
193
|
+
logger.info(f"🎯 Limiting to {max_steps} steps")
|
194
|
+
|
195
|
+
logger.info(f"🎬 Starting macro replay: '{description}'")
|
196
|
+
logger.info(f"📊 Total actions to execute: {len(actions)} / {total_actions}")
|
197
|
+
|
198
|
+
success_count = 0
|
199
|
+
failed_count = 0
|
200
|
+
|
201
|
+
for i, action in enumerate(actions, start=start_from_step + 1):
|
202
|
+
action_type = action.get("action_type", action.get("type", "unknown"))
|
203
|
+
description_text = action.get("description", "")
|
204
|
+
|
205
|
+
logger.info(f"\n📍 Step {i}/{total_actions}: {action_type}")
|
206
|
+
if description_text:
|
207
|
+
logger.info(f" Description: {description_text}")
|
208
|
+
|
209
|
+
# Execute the action
|
210
|
+
success = self.replay_action(action)
|
211
|
+
|
212
|
+
if success:
|
213
|
+
success_count += 1
|
214
|
+
logger.info(f" ✅ Action completed successfully")
|
215
|
+
else:
|
216
|
+
failed_count += 1
|
217
|
+
logger.error(f" ❌ Action failed")
|
218
|
+
|
219
|
+
# Wait between actions (except for the last one)
|
220
|
+
if i < len(actions):
|
221
|
+
logger.debug(f" ⏳ Waiting {self.delay_between_actions}s...")
|
222
|
+
await asyncio.sleep(self.delay_between_actions)
|
223
|
+
|
224
|
+
# Summary
|
225
|
+
total_executed = success_count + failed_count
|
226
|
+
success_rate = (
|
227
|
+
(success_count / total_executed * 100) if total_executed > 0 else 0
|
228
|
+
)
|
229
|
+
|
230
|
+
logger.info(f"\n🎉 Macro replay completed!")
|
231
|
+
logger.info(
|
232
|
+
f"📊 Success: {success_count}/{total_executed} ({success_rate:.1f}%)"
|
233
|
+
)
|
234
|
+
|
235
|
+
if failed_count > 0:
|
236
|
+
logger.warning(f"⚠️ Failed actions: {failed_count}")
|
237
|
+
|
238
|
+
return failed_count == 0
|
239
|
+
|
240
|
+
|
241
|
+
# Utility functions for convenience
|
242
|
+
|
243
|
+
|
244
|
+
async def replay_macro_file(
|
245
|
+
macro_file_path: str,
|
246
|
+
device_serial: str = None,
|
247
|
+
delay_between_actions: float = 1.0,
|
248
|
+
start_from_step: int = 0,
|
249
|
+
max_steps: Optional[int] = None,
|
250
|
+
) -> bool:
|
251
|
+
"""
|
252
|
+
Convenience function to replay a macro from a file.
|
253
|
+
|
254
|
+
Args:
|
255
|
+
macro_file_path: Path to the macro JSON file
|
256
|
+
device_serial: Target device serial (optional)
|
257
|
+
delay_between_actions: Delay between actions in seconds
|
258
|
+
start_from_step: Step to start from (0-based)
|
259
|
+
max_steps: Maximum steps to execute
|
260
|
+
|
261
|
+
Returns:
|
262
|
+
True if replay was successful, False otherwise
|
263
|
+
"""
|
264
|
+
player = MacroPlayer(
|
265
|
+
device_serial=device_serial, delay_between_actions=delay_between_actions
|
266
|
+
)
|
267
|
+
|
268
|
+
try:
|
269
|
+
macro_data = player.load_macro_from_file(macro_file_path)
|
270
|
+
return await player.replay_macro(
|
271
|
+
macro_data, start_from_step=start_from_step, max_steps=max_steps
|
272
|
+
)
|
273
|
+
except Exception as e:
|
274
|
+
logger.error(f"❌ Error replaying macro file {macro_file_path}: {e}")
|
275
|
+
return False
|
276
|
+
|
277
|
+
|
278
|
+
async def replay_macro_folder(
|
279
|
+
trajectory_folder: str,
|
280
|
+
device_serial: str = None,
|
281
|
+
delay_between_actions: float = 1.0,
|
282
|
+
start_from_step: int = 0,
|
283
|
+
max_steps: Optional[int] = None,
|
284
|
+
) -> bool:
|
285
|
+
"""
|
286
|
+
Convenience function to replay a macro from a trajectory folder.
|
287
|
+
|
288
|
+
Args:
|
289
|
+
trajectory_folder: Path to the trajectory folder containing macro.json
|
290
|
+
device_serial: Target device serial (optional)
|
291
|
+
delay_between_actions: Delay between actions in seconds
|
292
|
+
start_from_step: Step to start from (0-based)
|
293
|
+
max_steps: Maximum steps to execute
|
294
|
+
|
295
|
+
Returns:
|
296
|
+
True if replay was successful, False otherwise
|
297
|
+
"""
|
298
|
+
player = MacroPlayer(
|
299
|
+
device_serial=device_serial, delay_between_actions=delay_between_actions
|
300
|
+
)
|
301
|
+
|
302
|
+
try:
|
303
|
+
macro_data = player.load_macro_from_folder(trajectory_folder)
|
304
|
+
return await player.replay_macro(
|
305
|
+
macro_data, start_from_step=start_from_step, max_steps=max_steps
|
306
|
+
)
|
307
|
+
except Exception as e:
|
308
|
+
logger.error(f"❌ Error replaying macro folder {trajectory_folder}: {e}")
|
309
|
+
return False
|
droidrun/portal.py
CHANGED
@@ -2,8 +2,7 @@ import requests
|
|
2
2
|
import tempfile
|
3
3
|
import os
|
4
4
|
import contextlib
|
5
|
-
from
|
6
|
-
import asyncio
|
5
|
+
from adbutils import adb, AdbDevice
|
7
6
|
|
8
7
|
REPO = "droidrun/droidrun-portal"
|
9
8
|
ASSET_NAME = "droidrun-portal"
|
@@ -74,15 +73,15 @@ def download_portal_apk(debug: bool = False):
|
|
74
73
|
os.unlink(tmp.name)
|
75
74
|
|
76
75
|
|
77
|
-
|
78
|
-
|
76
|
+
def enable_portal_accessibility(device: AdbDevice):
|
77
|
+
device.shell(
|
79
78
|
f"settings put secure enabled_accessibility_services {A11Y_SERVICE_NAME}"
|
80
79
|
)
|
81
|
-
|
80
|
+
device.shell("settings put secure accessibility_enabled 1")
|
82
81
|
|
83
82
|
|
84
|
-
|
85
|
-
a11y_services =
|
83
|
+
def check_portal_accessibility(device: AdbDevice, debug: bool = False) -> bool:
|
84
|
+
a11y_services = device.shell(
|
86
85
|
"settings get secure enabled_accessibility_services"
|
87
86
|
)
|
88
87
|
if not A11Y_SERVICE_NAME in a11y_services:
|
@@ -90,7 +89,7 @@ async def check_portal_accessibility(device: Device, debug: bool = False) -> boo
|
|
90
89
|
print(a11y_services)
|
91
90
|
return False
|
92
91
|
|
93
|
-
a11y_enabled =
|
92
|
+
a11y_enabled = device.shell("settings get secure accessibility_enabled")
|
94
93
|
if a11y_enabled != "1":
|
95
94
|
if debug:
|
96
95
|
print(a11y_enabled)
|
@@ -99,12 +98,12 @@ async def check_portal_accessibility(device: Device, debug: bool = False) -> boo
|
|
99
98
|
return True
|
100
99
|
|
101
100
|
|
102
|
-
|
101
|
+
def ping_portal(device: AdbDevice, debug: bool = False):
|
103
102
|
"""
|
104
103
|
Ping the Droidrun Portal to check if it is installed and accessible.
|
105
104
|
"""
|
106
105
|
try:
|
107
|
-
packages =
|
106
|
+
packages = device.list_packages()
|
108
107
|
except Exception as e:
|
109
108
|
raise Exception(f"Failed to list packages: {e}")
|
110
109
|
|
@@ -113,14 +112,14 @@ async def ping_portal(device: Device, debug: bool = False):
|
|
113
112
|
print(packages)
|
114
113
|
raise Exception("Portal is not installed on the device")
|
115
114
|
|
116
|
-
if not
|
117
|
-
|
115
|
+
if not check_portal_accessibility(device, debug):
|
116
|
+
device.shell("am start -a android.settings.ACCESSIBILITY_SETTINGS")
|
118
117
|
raise Exception(
|
119
118
|
"Droidrun Portal is not enabled as an accessibility service on the device"
|
120
119
|
)
|
121
120
|
|
122
121
|
try:
|
123
|
-
state =
|
122
|
+
state = device.shell(
|
124
123
|
"content query --uri content://com.droidrun.portal/state"
|
125
124
|
)
|
126
125
|
if not "Row: 0 result=" in state:
|
@@ -130,10 +129,10 @@ async def ping_portal(device: Device, debug: bool = False):
|
|
130
129
|
raise Exception(f"Droidrun Portal is not reachable: {e}")
|
131
130
|
|
132
131
|
|
133
|
-
|
134
|
-
device =
|
135
|
-
|
132
|
+
def test():
|
133
|
+
device = adb.device()
|
134
|
+
ping_portal(device, debug=False)
|
136
135
|
|
137
136
|
|
138
137
|
if __name__ == "__main__":
|
139
|
-
|
138
|
+
test()
|
droidrun/telemetry/tracker.py
CHANGED
@@ -45,6 +45,7 @@ print_telemetry_message()
|
|
45
45
|
def get_user_id() -> str:
|
46
46
|
try:
|
47
47
|
if not USER_ID_PATH.exists():
|
48
|
+
USER_ID_PATH.parent.mkdir(parents=True, exist_ok=True)
|
48
49
|
USER_ID_PATH.touch()
|
49
50
|
USER_ID_PATH.write_text(str(uuid4()))
|
50
51
|
logger.debug(f"User ID: {USER_ID_PATH.read_text()}")
|
@@ -54,7 +55,7 @@ def get_user_id() -> str:
|
|
54
55
|
return "unknown"
|
55
56
|
|
56
57
|
|
57
|
-
def capture(event: TelemetryEvent):
|
58
|
+
def capture(event: TelemetryEvent, user_id: str | None = None):
|
58
59
|
try:
|
59
60
|
if not is_telemetry_enabled():
|
60
61
|
logger.debug(f"Telemetry disabled, skipping capture of {event}")
|
@@ -66,7 +67,7 @@ def capture(event: TelemetryEvent):
|
|
66
67
|
**event_data,
|
67
68
|
}
|
68
69
|
|
69
|
-
posthog.capture(event_name, distinct_id=get_user_id(), properties=properties)
|
70
|
+
posthog.capture(event_name, distinct_id=user_id or get_user_id(), properties=properties)
|
70
71
|
logger.debug(f"Captured event: {event_name} with properties: {event}")
|
71
72
|
except Exception as e:
|
72
73
|
logger.error(f"Error capturing event: {e}")
|