droidrun 0.3.0__py3-none-any.whl → 0.3.1__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/agent/codeact/codeact_agent.py +20 -11
- droidrun/agent/context/personas/default.py +1 -1
- droidrun/agent/droid/droid_agent.py +6 -1
- droidrun/agent/planner/planner_agent.py +32 -12
- droidrun/agent/utils/chat_utils.py +4 -7
- droidrun/cli/main.py +42 -13
- droidrun/tools/adb.py +219 -291
- droidrun/tools/ios.py +4 -2
- droidrun/tools/tools.py +1 -5
- {droidrun-0.3.0.dist-info → droidrun-0.3.1.dist-info}/METADATA +3 -2
- {droidrun-0.3.0.dist-info → droidrun-0.3.1.dist-info}/RECORD +14 -16
- droidrun/agent/context/todo.txt +0 -4
- droidrun/run.py +0 -105
- {droidrun-0.3.0.dist-info → droidrun-0.3.1.dist-info}/WHEEL +0 -0
- {droidrun-0.3.0.dist-info → droidrun-0.3.1.dist-info}/entry_points.txt +0 -0
- {droidrun-0.3.0.dist-info → droidrun-0.3.1.dist-info}/licenses/LICENSE +0 -0
droidrun/tools/adb.py
CHANGED
@@ -10,11 +10,13 @@ import tempfile
|
|
10
10
|
import asyncio
|
11
11
|
import aiofiles
|
12
12
|
import contextlib
|
13
|
+
import logging
|
13
14
|
from typing import Optional, Dict, Tuple, List, Any
|
14
15
|
from droidrun.adb.device import Device
|
15
16
|
from droidrun.adb.manager import DeviceManager
|
16
17
|
from droidrun.tools.tools import Tools
|
17
18
|
|
19
|
+
logger = logging.getLogger("droidrun-adb-tools")
|
18
20
|
|
19
21
|
class AdbTools(Tools):
|
20
22
|
"""Core UI interaction tools for Android device control."""
|
@@ -74,142 +76,50 @@ class AdbTools(Tools):
|
|
74
76
|
apps.append({"package": package.strip(), "path": path.strip()})
|
75
77
|
return apps
|
76
78
|
|
77
|
-
|
79
|
+
def _parse_content_provider_output(self, raw_output: str) -> Optional[Dict[str, Any]]:
|
78
80
|
"""
|
79
|
-
|
80
|
-
|
81
|
-
This function interacts with the TopViewService app installed on the device
|
82
|
-
to capture UI elements. The service writes UI data to a JSON file on the device,
|
83
|
-
which is then pulled to the host. If no elements are found initially, it will
|
84
|
-
retry for up to 30 seconds.
|
85
|
-
|
81
|
+
Parse the raw ADB content provider output and extract JSON data.
|
82
|
+
|
86
83
|
Args:
|
87
|
-
|
88
|
-
|
84
|
+
raw_output (str): Raw output from ADB content query command
|
85
|
+
|
89
86
|
Returns:
|
90
|
-
|
91
|
-
"""
|
87
|
+
dict: Parsed JSON data or None if parsing failed
|
88
|
+
"""
|
89
|
+
# The ADB content query output format is: "Row: 0 result={json_data}"
|
90
|
+
# We need to extract the JSON part after "result="
|
91
|
+
lines = raw_output.strip().split('\n')
|
92
|
+
|
93
|
+
for line in lines:
|
94
|
+
line = line.strip()
|
95
|
+
|
96
|
+
# Look for lines that contain "result=" pattern
|
97
|
+
if "result=" in line:
|
98
|
+
# Extract everything after "result="
|
99
|
+
result_start = line.find("result=") + 7
|
100
|
+
json_str = line[result_start:]
|
101
|
+
|
102
|
+
try:
|
103
|
+
# Parse the JSON string
|
104
|
+
json_data = json.loads(json_str)
|
105
|
+
return json_data
|
106
|
+
except json.JSONDecodeError:
|
107
|
+
continue
|
108
|
+
|
109
|
+
# Fallback: try to parse lines that start with { or [
|
110
|
+
elif line.startswith('{') or line.startswith('['):
|
111
|
+
try:
|
112
|
+
json_data = json.loads(line)
|
113
|
+
return json_data
|
114
|
+
except json.JSONDecodeError:
|
115
|
+
continue
|
116
|
+
|
117
|
+
# If no valid JSON found in individual lines, try the entire output
|
92
118
|
try:
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
if not device:
|
98
|
-
raise ValueError(f"Device {serial} not found")
|
99
|
-
else:
|
100
|
-
device = await self.get_device()
|
101
|
-
|
102
|
-
# Create a temporary file for the JSON
|
103
|
-
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as temp:
|
104
|
-
local_path = temp.name
|
105
|
-
|
106
|
-
try:
|
107
|
-
# Set retry parameters
|
108
|
-
max_total_time = 30 # Maximum total time to try in seconds
|
109
|
-
retry_interval = 1.0 # Time between retries in seconds
|
110
|
-
start_total_time = asyncio.get_event_loop().time()
|
111
|
-
|
112
|
-
while True:
|
113
|
-
# Check if we've exceeded total time
|
114
|
-
current_time = asyncio.get_event_loop().time()
|
115
|
-
if current_time - start_total_time > max_total_time:
|
116
|
-
raise ValueError(
|
117
|
-
f"Failed to get UI elements after {max_total_time} seconds of retries"
|
118
|
-
)
|
119
|
-
|
120
|
-
# Clear logcat to make it easier to find our output
|
121
|
-
await device._adb.shell(device._serial, "logcat -c")
|
122
|
-
|
123
|
-
# Trigger the custom service via broadcast to get only interactive elements
|
124
|
-
await device._adb.shell(
|
125
|
-
device._serial,
|
126
|
-
"am broadcast -a com.droidrun.portal.GET_ELEMENTS",
|
127
|
-
)
|
128
|
-
|
129
|
-
# Poll for the JSON file path
|
130
|
-
start_time = asyncio.get_event_loop().time()
|
131
|
-
max_wait_time = 10 # Maximum wait time in seconds
|
132
|
-
poll_interval = 0.2 # Check every 200ms
|
133
|
-
|
134
|
-
device_path = None
|
135
|
-
while asyncio.get_event_loop().time() - start_time < max_wait_time:
|
136
|
-
# Check logcat for the file path
|
137
|
-
logcat_output = await device._adb.shell(
|
138
|
-
device._serial,
|
139
|
-
'logcat -d | grep "DROIDRUN_FILE" | grep "JSON data written to" | tail -1',
|
140
|
-
)
|
141
|
-
|
142
|
-
# Parse the file path if present
|
143
|
-
match = re.search(r"JSON data written to: (.*)", logcat_output)
|
144
|
-
if match:
|
145
|
-
device_path = match.group(1).strip()
|
146
|
-
break
|
147
|
-
|
148
|
-
# Wait before polling again
|
149
|
-
await asyncio.sleep(poll_interval)
|
150
|
-
|
151
|
-
# Check if we found the file path
|
152
|
-
if not device_path:
|
153
|
-
await asyncio.sleep(retry_interval)
|
154
|
-
continue
|
155
|
-
|
156
|
-
# Pull the JSON file from the device
|
157
|
-
await device._adb.pull_file(device._serial, device_path, local_path)
|
158
|
-
|
159
|
-
# Read the JSON file
|
160
|
-
async with aiofiles.open(local_path, "r", encoding="utf-8") as f:
|
161
|
-
json_content = await f.read()
|
162
|
-
|
163
|
-
# Try to parse the JSON
|
164
|
-
try:
|
165
|
-
ui_data = json.loads(json_content)
|
166
|
-
|
167
|
-
# Filter out the "type" attribute from all elements
|
168
|
-
filtered_data = []
|
169
|
-
for element in ui_data:
|
170
|
-
# Create a copy of the element without the "type" attribute
|
171
|
-
filtered_element = {
|
172
|
-
k: v for k, v in element.items() if k != "type"
|
173
|
-
}
|
174
|
-
|
175
|
-
# Also filter children if present
|
176
|
-
if "children" in filtered_element:
|
177
|
-
filtered_element["children"] = [
|
178
|
-
{k: v for k, v in child.items() if k != "type"}
|
179
|
-
for child in filtered_element["children"]
|
180
|
-
]
|
181
|
-
|
182
|
-
filtered_data.append(filtered_element)
|
183
|
-
|
184
|
-
# If we got elements, store them and return
|
185
|
-
if filtered_data:
|
186
|
-
# Store the filtered UI data in cache
|
187
|
-
global CLICKABLE_ELEMENTS_CACHE
|
188
|
-
CLICKABLE_ELEMENTS_CACHE = filtered_data
|
189
|
-
|
190
|
-
# Add a small sleep to ensure UI is fully loaded/processed
|
191
|
-
await asyncio.sleep(0.5) # 500ms sleep
|
192
|
-
|
193
|
-
# Convert the dictionary to a JSON string before returning
|
194
|
-
|
195
|
-
return filtered_data
|
196
|
-
|
197
|
-
# If no elements found, wait and retry
|
198
|
-
await asyncio.sleep(retry_interval)
|
199
|
-
|
200
|
-
except json.JSONDecodeError:
|
201
|
-
# If JSON parsing failed, wait and retry
|
202
|
-
await asyncio.sleep(retry_interval)
|
203
|
-
continue
|
204
|
-
|
205
|
-
except Exception as e:
|
206
|
-
# Clean up in case of error
|
207
|
-
with contextlib.suppress(OSError):
|
208
|
-
os.unlink(local_path)
|
209
|
-
raise ValueError(f"Error retrieving clickable elements: {e}")
|
210
|
-
|
211
|
-
except Exception as e:
|
212
|
-
raise ValueError(f"Error getting clickable elements: {e}")
|
119
|
+
json_data = json.loads(raw_output.strip())
|
120
|
+
return json_data
|
121
|
+
except json.JSONDecodeError:
|
122
|
+
return None
|
213
123
|
|
214
124
|
async def tap_by_index(self, index: int, serial: Optional[str] = None) -> str:
|
215
125
|
"""
|
@@ -250,15 +160,15 @@ class AdbTools(Tools):
|
|
250
160
|
|
251
161
|
try:
|
252
162
|
# Check if we have cached elements
|
253
|
-
if not
|
254
|
-
return "Error: No UI elements cached. Call
|
163
|
+
if not self.clickable_elements_cache:
|
164
|
+
return "Error: No UI elements cached. Call get_state first."
|
255
165
|
|
256
166
|
# Find the element with the given index (including in children)
|
257
|
-
element = find_element_by_index(
|
167
|
+
element = find_element_by_index(self.clickable_elements_cache, index)
|
258
168
|
|
259
169
|
if not element:
|
260
170
|
# List available indices to help the user
|
261
|
-
indices = sorted(collect_all_indices(
|
171
|
+
indices = sorted(collect_all_indices(self.clickable_elements_cache))
|
262
172
|
indices_str = ", ".join(str(idx) for idx in indices[:20])
|
263
173
|
if len(indices) > 20:
|
264
174
|
indices_str += f"... and {len(indices) - 20} more"
|
@@ -285,8 +195,7 @@ class AdbTools(Tools):
|
|
285
195
|
|
286
196
|
# Get the device and tap at the coordinates
|
287
197
|
if serial:
|
288
|
-
|
289
|
-
device = await device_manager.get_device(serial)
|
198
|
+
device = await self.device_manager.get_device(serial)
|
290
199
|
if not device:
|
291
200
|
return f"Error: Device {serial} not found"
|
292
201
|
else:
|
@@ -333,8 +242,7 @@ class AdbTools(Tools):
|
|
333
242
|
"""
|
334
243
|
try:
|
335
244
|
if self.serial:
|
336
|
-
|
337
|
-
device = await device_manager.get_device(self.serial)
|
245
|
+
device = await self.device_manager.get_device(self.serial)
|
338
246
|
if not device:
|
339
247
|
return f"Error: Device {self.serial} not found"
|
340
248
|
else:
|
@@ -380,8 +288,7 @@ class AdbTools(Tools):
|
|
380
288
|
"""
|
381
289
|
try:
|
382
290
|
if self.serial:
|
383
|
-
|
384
|
-
device = await device_manager.get_device(self.serial)
|
291
|
+
device = await self.device_manager.get_device(self.serial)
|
385
292
|
if not device:
|
386
293
|
return f"Error: Device {self.serial} not found"
|
387
294
|
else:
|
@@ -408,8 +315,7 @@ class AdbTools(Tools):
|
|
408
315
|
"""
|
409
316
|
try:
|
410
317
|
if serial:
|
411
|
-
|
412
|
-
device = await device_manager.get_device(serial)
|
318
|
+
device = await self.device_manager.get_device(serial)
|
413
319
|
if not device:
|
414
320
|
return f"Error: Device {serial} not found"
|
415
321
|
else:
|
@@ -432,14 +338,14 @@ class AdbTools(Tools):
|
|
432
338
|
)
|
433
339
|
|
434
340
|
# Wait for keyboard to change
|
435
|
-
await asyncio.sleep(
|
341
|
+
await asyncio.sleep(1)
|
436
342
|
|
437
343
|
# Encode the text to Base64
|
438
344
|
import base64
|
439
345
|
|
440
346
|
encoded_text = base64.b64encode(text.encode()).decode()
|
441
347
|
|
442
|
-
cmd = f'
|
348
|
+
cmd = f'content insert --uri "content://com.droidrun.portal/keyboard/input" --bind base64_text:s:"{encoded_text}"'
|
443
349
|
await device._adb.shell(device._serial, cmd)
|
444
350
|
|
445
351
|
# Wait for text input to complete
|
@@ -462,8 +368,7 @@ class AdbTools(Tools):
|
|
462
368
|
"""
|
463
369
|
try:
|
464
370
|
if self.serial:
|
465
|
-
|
466
|
-
device = await device_manager.get_device(self.serial)
|
371
|
+
device = await self.device_manager.get_device(self.serial)
|
467
372
|
if not device:
|
468
373
|
return f"Error: Device {self.serial} not found"
|
469
374
|
else:
|
@@ -479,6 +384,7 @@ class AdbTools(Tools):
|
|
479
384
|
Press a key on the Android device.
|
480
385
|
|
481
386
|
Common keycodes:
|
387
|
+
- 3: HOME
|
482
388
|
- 4: BACK
|
483
389
|
- 66: ENTER
|
484
390
|
- 67: DELETE
|
@@ -488,8 +394,7 @@ class AdbTools(Tools):
|
|
488
394
|
"""
|
489
395
|
try:
|
490
396
|
if self.serial:
|
491
|
-
|
492
|
-
device = await device_manager.get_device(self.serial)
|
397
|
+
device = await self.device_manager.get_device(self.serial)
|
493
398
|
if not device:
|
494
399
|
return f"Error: Device {self.serial} not found"
|
495
400
|
else:
|
@@ -498,6 +403,7 @@ class AdbTools(Tools):
|
|
498
403
|
key_names = {
|
499
404
|
66: "ENTER",
|
500
405
|
4: "BACK",
|
406
|
+
3: "HOME",
|
501
407
|
67: "DELETE",
|
502
408
|
}
|
503
409
|
key_name = key_names.get(keycode, str(keycode))
|
@@ -517,8 +423,7 @@ class AdbTools(Tools):
|
|
517
423
|
"""
|
518
424
|
try:
|
519
425
|
if self.serial:
|
520
|
-
|
521
|
-
device = await device_manager.get_device(self.serial)
|
426
|
+
device = await self.device_manager.get_device(self.serial)
|
522
427
|
if not device:
|
523
428
|
return f"Error: Device {self.serial} not found"
|
524
429
|
else:
|
@@ -542,8 +447,7 @@ class AdbTools(Tools):
|
|
542
447
|
"""
|
543
448
|
try:
|
544
449
|
if self.serial:
|
545
|
-
|
546
|
-
device = await device_manager.get_device(self.serial)
|
450
|
+
device = await self.device_manager.get_device(self.serial)
|
547
451
|
if not device:
|
548
452
|
return f"Error: Device {self.serial} not found"
|
549
453
|
else:
|
@@ -565,8 +469,7 @@ class AdbTools(Tools):
|
|
565
469
|
"""
|
566
470
|
try:
|
567
471
|
if self.serial:
|
568
|
-
|
569
|
-
device = await device_manager.get_device(self.serial)
|
472
|
+
device = await self.device_manager.get_device(self.serial)
|
570
473
|
if not device:
|
571
474
|
raise ValueError(f"Device {self.serial} not found")
|
572
475
|
else:
|
@@ -598,8 +501,7 @@ class AdbTools(Tools):
|
|
598
501
|
"""
|
599
502
|
try:
|
600
503
|
if self.serial:
|
601
|
-
|
602
|
-
device = await device_manager.get_device(self.serial)
|
504
|
+
device = await self.device_manager.get_device(self.serial)
|
603
505
|
if not device:
|
604
506
|
raise ValueError(f"Device {self.serial} not found")
|
605
507
|
else:
|
@@ -616,7 +518,8 @@ class AdbTools(Tools):
|
|
616
518
|
packages = self.parse_package_list(output)
|
617
519
|
# Format package list for better readability
|
618
520
|
package_list = [pack["package"] for pack in packages]
|
619
|
-
|
521
|
+
for package in package_list:
|
522
|
+
print(package)
|
620
523
|
return package_list
|
621
524
|
except ValueError as e:
|
622
525
|
raise ValueError(f"Error listing packages: {str(e)}")
|
@@ -669,87 +572,87 @@ class AdbTools(Tools):
|
|
669
572
|
"""
|
670
573
|
try:
|
671
574
|
# Get the device
|
672
|
-
|
673
|
-
device = await device_manager.get_device(self.serial)
|
575
|
+
device = await self.device_manager.get_device(self.serial)
|
674
576
|
if not device:
|
675
577
|
raise ValueError(f"Device {self.serial} not found")
|
676
578
|
|
677
579
|
# Create a temporary file for the JSON
|
678
|
-
with tempfile.NamedTemporaryFile(suffix=".json"
|
580
|
+
with tempfile.NamedTemporaryFile(suffix=".json") as temp:
|
679
581
|
local_path = temp.name
|
680
582
|
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
device._serial,
|
688
|
-
"am broadcast -a com.droidrun.portal.GET_ALL_ELEMENTS",
|
689
|
-
)
|
690
|
-
|
691
|
-
# Poll for the JSON file path
|
692
|
-
start_time = asyncio.get_event_loop().time()
|
693
|
-
max_wait_time = 10 # Maximum wait time in seconds
|
694
|
-
poll_interval = 0.2 # Check every 200ms
|
695
|
-
|
696
|
-
device_path = None
|
697
|
-
while asyncio.get_event_loop().time() - start_time < max_wait_time:
|
698
|
-
# Check logcat for the file path
|
699
|
-
logcat_output = await device._adb.shell(
|
583
|
+
try:
|
584
|
+
# Clear logcat to make it easier to find our output
|
585
|
+
await device._adb.shell(device._serial, "logcat -c")
|
586
|
+
|
587
|
+
# Trigger the custom service via broadcast to get ALL elements
|
588
|
+
await device._adb.shell(
|
700
589
|
device._serial,
|
701
|
-
|
590
|
+
"am broadcast -a com.droidrun.portal.GET_ALL_ELEMENTS",
|
702
591
|
)
|
703
592
|
|
704
|
-
#
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
593
|
+
# Poll for the JSON file path
|
594
|
+
start_time = asyncio.get_event_loop().time()
|
595
|
+
max_wait_time = 10 # Maximum wait time in seconds
|
596
|
+
poll_interval = 0.2 # Check every 200ms
|
597
|
+
|
598
|
+
device_path = None
|
599
|
+
while asyncio.get_event_loop().time() - start_time < max_wait_time:
|
600
|
+
# Check logcat for the file path
|
601
|
+
logcat_output = await device._adb.shell(
|
602
|
+
device._serial,
|
603
|
+
'logcat -d | grep "DROIDRUN_FILE" | grep "JSON data written to" | tail -1',
|
604
|
+
)
|
605
|
+
|
606
|
+
# Parse the file path if present
|
607
|
+
match = re.search(r"JSON data written to: (.*)", logcat_output)
|
608
|
+
if match:
|
609
|
+
device_path = match.group(1).strip()
|
610
|
+
break
|
709
611
|
|
710
|
-
|
711
|
-
|
612
|
+
# Wait before polling again
|
613
|
+
await asyncio.sleep(poll_interval)
|
712
614
|
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
615
|
+
# Check if we found the file path
|
616
|
+
if not device_path:
|
617
|
+
raise ValueError(
|
618
|
+
f"Failed to find the JSON file path in logcat after {max_wait_time} seconds"
|
619
|
+
)
|
718
620
|
|
719
|
-
|
720
|
-
|
621
|
+
logger.debug(f"Pulling file from {device_path} to {local_path}")
|
622
|
+
# Pull the JSON file from the device
|
623
|
+
await device._adb.pull_file(device._serial, device_path, local_path)
|
721
624
|
|
722
|
-
|
723
|
-
|
724
|
-
|
625
|
+
# Read the JSON file
|
626
|
+
async with aiofiles.open(local_path, "r", encoding="utf-8") as f:
|
627
|
+
json_content = await f.read()
|
725
628
|
|
726
|
-
|
727
|
-
|
728
|
-
|
629
|
+
# Clean up the temporary file
|
630
|
+
with contextlib.suppress(OSError):
|
631
|
+
os.unlink(local_path)
|
729
632
|
|
730
|
-
|
731
|
-
|
633
|
+
# Try to parse the JSON
|
634
|
+
import json
|
732
635
|
|
733
|
-
|
734
|
-
|
636
|
+
try:
|
637
|
+
ui_data = json.loads(json_content)
|
735
638
|
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
639
|
+
return {
|
640
|
+
"all_elements": ui_data,
|
641
|
+
"count": (
|
642
|
+
len(ui_data)
|
643
|
+
if isinstance(ui_data, list)
|
644
|
+
else sum(1 for _ in ui_data.get("elements", []))
|
645
|
+
),
|
646
|
+
"message": "Retrieved all UI elements from the device screen",
|
647
|
+
}
|
648
|
+
except json.JSONDecodeError:
|
649
|
+
raise ValueError("Failed to parse UI elements JSON data")
|
747
650
|
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
651
|
+
except Exception as e:
|
652
|
+
# Clean up in case of error
|
653
|
+
with contextlib.suppress(OSError):
|
654
|
+
os.unlink(local_path)
|
655
|
+
raise ValueError(f"Error retrieving all UI elements: {e}")
|
753
656
|
|
754
657
|
except Exception as e:
|
755
658
|
raise ValueError(f"Error getting all UI elements: {e}")
|
@@ -773,75 +676,6 @@ class AdbTools(Tools):
|
|
773
676
|
self.reason = reason
|
774
677
|
self.finished = True
|
775
678
|
|
776
|
-
async def get_phone_state(self, serial: Optional[str] = None) -> Dict[str, Any]:
|
777
|
-
"""
|
778
|
-
Get the current phone state including current activity and keyboard visibility.
|
779
|
-
|
780
|
-
Args:
|
781
|
-
serial: Optional device serial number
|
782
|
-
|
783
|
-
Returns:
|
784
|
-
Dictionary with current phone state information
|
785
|
-
"""
|
786
|
-
try:
|
787
|
-
# Get the device
|
788
|
-
if serial:
|
789
|
-
device_manager = DeviceManager()
|
790
|
-
device = await device_manager.get_device(serial)
|
791
|
-
if not device:
|
792
|
-
raise ValueError(f"Device {serial} not found")
|
793
|
-
else:
|
794
|
-
device = await self.get_device()
|
795
|
-
|
796
|
-
# Clear logcat to make it easier to find our output
|
797
|
-
await device._adb.shell(device._serial, "logcat -c")
|
798
|
-
|
799
|
-
# Trigger the custom service via broadcast to get phone state
|
800
|
-
await device._adb.shell(
|
801
|
-
device._serial, "am broadcast -a com.droidrun.portal.GET_PHONE_STATE"
|
802
|
-
)
|
803
|
-
|
804
|
-
# Poll for the phone state data in logcat
|
805
|
-
start_time = asyncio.get_event_loop().time()
|
806
|
-
max_wait_time = 10 # Maximum wait time in seconds
|
807
|
-
poll_interval = 0.2 # Check every 200ms
|
808
|
-
|
809
|
-
while asyncio.get_event_loop().time() - start_time < max_wait_time:
|
810
|
-
# Check logcat for the phone state data
|
811
|
-
logcat_output = await device._adb.shell(
|
812
|
-
device._serial,
|
813
|
-
'logcat -d | grep "DROIDRUN_PHONE_STATE_DATA" | tail -1',
|
814
|
-
)
|
815
|
-
|
816
|
-
# Parse the JSON data if present
|
817
|
-
if "CHUNK|" in logcat_output:
|
818
|
-
# Format: DROIDRUN_PHONE_STATE_DATA: CHUNK|0|1|{json_data}
|
819
|
-
# Extract the JSON part after the last |
|
820
|
-
parts = logcat_output.split("|")
|
821
|
-
if len(parts) >= 4:
|
822
|
-
json_data = "|".join(
|
823
|
-
parts[3:]
|
824
|
-
) # In case JSON contains | characters
|
825
|
-
try:
|
826
|
-
phone_state = json.loads(json_data)
|
827
|
-
return phone_state
|
828
|
-
except json.JSONDecodeError:
|
829
|
-
# If JSON parsing failed, wait and retry
|
830
|
-
await asyncio.sleep(poll_interval)
|
831
|
-
continue
|
832
|
-
|
833
|
-
# Wait before polling again
|
834
|
-
await asyncio.sleep(poll_interval)
|
835
|
-
|
836
|
-
# If we couldn't get the phone state, return error
|
837
|
-
return {
|
838
|
-
"error": "Timeout",
|
839
|
-
"message": f"Failed to get phone state data after {max_wait_time} seconds",
|
840
|
-
}
|
841
|
-
|
842
|
-
except Exception as e:
|
843
|
-
return {"error": str(e), "message": f"Error getting phone state: {str(e)}"}
|
844
|
-
|
845
679
|
async def remember(self, information: str) -> str:
|
846
680
|
"""
|
847
681
|
Store important information to remember for future context.
|
@@ -877,3 +711,97 @@ class AdbTools(Tools):
|
|
877
711
|
List of stored memory items
|
878
712
|
"""
|
879
713
|
return self.memory.copy()
|
714
|
+
|
715
|
+
async def get_state(self, serial: Optional[str] = None) -> Dict[str, Any]:
|
716
|
+
"""
|
717
|
+
Get both the a11y tree and phone state in a single call using the combined /state endpoint.
|
718
|
+
|
719
|
+
Args:
|
720
|
+
serial: Optional device serial number
|
721
|
+
|
722
|
+
Returns:
|
723
|
+
Dictionary containing both 'a11y_tree' and 'phone_state' data
|
724
|
+
"""
|
725
|
+
|
726
|
+
try:
|
727
|
+
if serial:
|
728
|
+
device = await self.device_manager.get_device(serial)
|
729
|
+
if not device:
|
730
|
+
raise ValueError(f"Device {serial} not found")
|
731
|
+
else:
|
732
|
+
device = await self.get_device()
|
733
|
+
|
734
|
+
adb_output = await device._adb.shell(
|
735
|
+
device._serial,
|
736
|
+
'content query --uri content://com.droidrun.portal/state'
|
737
|
+
)
|
738
|
+
|
739
|
+
state_data = self._parse_content_provider_output(adb_output)
|
740
|
+
|
741
|
+
if state_data is None:
|
742
|
+
return {
|
743
|
+
"error": "Parse Error",
|
744
|
+
"message": "Failed to parse state data from ContentProvider response"
|
745
|
+
}
|
746
|
+
|
747
|
+
if isinstance(state_data, dict) and "data" in state_data:
|
748
|
+
data_str = state_data["data"]
|
749
|
+
try:
|
750
|
+
combined_data = json.loads(data_str)
|
751
|
+
except json.JSONDecodeError:
|
752
|
+
return {
|
753
|
+
"error": "Parse Error",
|
754
|
+
"message": "Failed to parse JSON data from ContentProvider data field"
|
755
|
+
}
|
756
|
+
else:
|
757
|
+
return {
|
758
|
+
"error": "Format Error",
|
759
|
+
"message": f"Unexpected state data format: {type(state_data)}"
|
760
|
+
}
|
761
|
+
|
762
|
+
# Validate that both a11y_tree and phone_state are present
|
763
|
+
if "a11y_tree" not in combined_data:
|
764
|
+
return {
|
765
|
+
"error": "Missing Data",
|
766
|
+
"message": "a11y_tree not found in combined state data"
|
767
|
+
}
|
768
|
+
|
769
|
+
if "phone_state" not in combined_data:
|
770
|
+
return {
|
771
|
+
"error": "Missing Data",
|
772
|
+
"message": "phone_state not found in combined state data"
|
773
|
+
}
|
774
|
+
|
775
|
+
# Filter out the "type" attribute from all a11y_tree elements
|
776
|
+
elements = combined_data["a11y_tree"]
|
777
|
+
filtered_elements = []
|
778
|
+
for element in elements:
|
779
|
+
# Create a copy of the element without the "type" attribute
|
780
|
+
filtered_element = {
|
781
|
+
k: v for k, v in element.items() if k != "type"
|
782
|
+
}
|
783
|
+
|
784
|
+
# Also filter children if present
|
785
|
+
if "children" in filtered_element:
|
786
|
+
filtered_element["children"] = [
|
787
|
+
{k: v for k, v in child.items() if k != "type"}
|
788
|
+
for child in filtered_element["children"]
|
789
|
+
]
|
790
|
+
|
791
|
+
filtered_elements.append(filtered_element)
|
792
|
+
|
793
|
+
self.clickable_elements_cache = filtered_elements
|
794
|
+
|
795
|
+
return {
|
796
|
+
"a11y_tree": filtered_elements,
|
797
|
+
"phone_state": combined_data["phone_state"]
|
798
|
+
}
|
799
|
+
|
800
|
+
except Exception as e:
|
801
|
+
return {"error": str(e), "message": f"Error getting combined state: {str(e)}"}
|
802
|
+
|
803
|
+
if __name__ == "__main__":
|
804
|
+
async def main():
|
805
|
+
tools = AdbTools()
|
806
|
+
|
807
|
+
asyncio.run(main())
|