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/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
- async def get_clickables(self, serial: Optional[str] = None) -> str:
79
+ def _parse_content_provider_output(self, raw_output: str) -> Optional[Dict[str, Any]]:
78
80
  """
79
- Get all clickable UI elements from the device using the custom TopViewService.
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
- serial: Optional device serial number
88
-
84
+ raw_output (str): Raw output from ADB content query command
85
+
89
86
  Returns:
90
- JSON string containing UI elements extracted from the device screen
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
- # Get the device
94
- if serial:
95
- device_manager = DeviceManager()
96
- device = await device_manager.get_device(serial)
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 CLICKABLE_ELEMENTS_CACHE:
254
- return "Error: No UI elements cached. Call get_clickables first."
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(CLICKABLE_ELEMENTS_CACHE, 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(CLICKABLE_ELEMENTS_CACHE))
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
- device_manager = DeviceManager()
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
- device_manager = DeviceManager()
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
- device_manager = DeviceManager()
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
- device_manager = DeviceManager()
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(0.2)
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'am broadcast -a com.droidrun.portal.DROIDRUN_INPUT_B64 --es msg "{encoded_text}" -p com.droidrun.portal'
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
- device_manager = DeviceManager()
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
- device_manager = DeviceManager()
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
- device_manager = DeviceManager()
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
- device_manager = DeviceManager()
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
- device_manager = DeviceManager()
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
- device_manager = DeviceManager()
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
- print(f"Returning {len(package_list)} packages")
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
- device_manager = DeviceManager()
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", delete=False) as temp:
580
+ with tempfile.NamedTemporaryFile(suffix=".json") as temp:
679
581
  local_path = temp.name
680
582
 
681
- try:
682
- # Clear logcat to make it easier to find our output
683
- await device._adb.shell(device._serial, "logcat -c")
684
-
685
- # Trigger the custom service via broadcast to get ALL elements
686
- await device._adb.shell(
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
- 'logcat -d | grep "DROIDRUN_FILE" | grep "JSON data written to" | tail -1',
590
+ "am broadcast -a com.droidrun.portal.GET_ALL_ELEMENTS",
702
591
  )
703
592
 
704
- # Parse the file path if present
705
- match = re.search(r"JSON data written to: (.*)", logcat_output)
706
- if match:
707
- device_path = match.group(1).strip()
708
- break
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
- # Wait before polling again
711
- await asyncio.sleep(poll_interval)
612
+ # Wait before polling again
613
+ await asyncio.sleep(poll_interval)
712
614
 
713
- # Check if we found the file path
714
- if not device_path:
715
- raise ValueError(
716
- f"Failed to find the JSON file path in logcat after {max_wait_time} seconds"
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
- # Pull the JSON file from the device
720
- await device._adb.pull_file(device._serial, device_path, local_path)
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
- # Read the JSON file
723
- async with aiofiles.open(local_path, "r", encoding="utf-8") as f:
724
- json_content = await f.read()
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
- # Clean up the temporary file
727
- with contextlib.suppress(OSError):
728
- os.unlink(local_path)
629
+ # Clean up the temporary file
630
+ with contextlib.suppress(OSError):
631
+ os.unlink(local_path)
729
632
 
730
- # Try to parse the JSON
731
- import json
633
+ # Try to parse the JSON
634
+ import json
732
635
 
733
- try:
734
- ui_data = json.loads(json_content)
636
+ try:
637
+ ui_data = json.loads(json_content)
735
638
 
736
- return {
737
- "all_elements": ui_data,
738
- "count": (
739
- len(ui_data)
740
- if isinstance(ui_data, list)
741
- else sum(1 for _ in ui_data.get("elements", []))
742
- ),
743
- "message": "Retrieved all UI elements from the device screen",
744
- }
745
- except json.JSONDecodeError:
746
- raise ValueError("Failed to parse UI elements JSON data")
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
- except Exception as e:
749
- # Clean up in case of error
750
- with contextlib.suppress(OSError):
751
- os.unlink(local_path)
752
- raise ValueError(f"Error retrieving all UI elements: {e}")
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())