droidrun 0.3.10.dev7__py3-none-any.whl → 0.3.10.dev9__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/common/__init__.py +0 -0
- droidrun/agent/oneflows/__init__.py +0 -0
- droidrun/app_cards/__init__.py +0 -0
- droidrun/cli/main.py +7 -10
- droidrun/tools/adb.py +23 -365
- droidrun/tools/portal_client.py +434 -0
- {droidrun-0.3.10.dev7.dist-info → droidrun-0.3.10.dev9.dist-info}/METADATA +1 -1
- {droidrun-0.3.10.dev7.dist-info → droidrun-0.3.10.dev9.dist-info}/RECORD +11 -7
- {droidrun-0.3.10.dev7.dist-info → droidrun-0.3.10.dev9.dist-info}/WHEEL +0 -0
- {droidrun-0.3.10.dev7.dist-info → droidrun-0.3.10.dev9.dist-info}/entry_points.txt +0 -0
- {droidrun-0.3.10.dev7.dist-info → droidrun-0.3.10.dev9.dist-info}/licenses/LICENSE +0 -0
File without changes
|
File without changes
|
File without changes
|
droidrun/cli/main.py
CHANGED
@@ -555,18 +555,15 @@ def run(
|
|
555
555
|
)
|
556
556
|
finally:
|
557
557
|
# Disable DroidRun keyboard after execution
|
558
|
+
# Note: Port forwards are managed automatically and persist until device disconnect
|
558
559
|
try:
|
559
560
|
if not (ios if ios is not None else False):
|
560
|
-
|
561
|
-
if
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
# Cleanup tools
|
567
|
-
del tools
|
568
|
-
except Exception as disable_e:
|
569
|
-
click.echo(f"Warning: Failed to disable DroidRun keyboard: {disable_e}")
|
561
|
+
device_obj = adb.device(device)
|
562
|
+
if device_obj:
|
563
|
+
device_obj.shell("ime disable com.droidrun.portal/.DroidrunKeyboardIME")
|
564
|
+
except Exception:
|
565
|
+
click.echo("Failed to disable DroidRun keyboard")
|
566
|
+
pass
|
570
567
|
|
571
568
|
|
572
569
|
@cli.command()
|
droidrun/tools/adb.py
CHANGED
@@ -2,15 +2,11 @@
|
|
2
2
|
UI Actions - Core UI interaction tools for Android device control.
|
3
3
|
"""
|
4
4
|
|
5
|
-
import base64
|
6
|
-
import io
|
7
|
-
import json
|
8
5
|
import logging
|
9
6
|
import os
|
10
7
|
import time
|
11
8
|
from typing import Any, Dict, List, Optional, Tuple
|
12
9
|
|
13
|
-
import requests
|
14
10
|
from adbutils import adb
|
15
11
|
from llama_index.core.workflow import Context
|
16
12
|
|
@@ -24,6 +20,7 @@ from droidrun.agent.common.events import (
|
|
24
20
|
)
|
25
21
|
from droidrun.tools.tools import Tools
|
26
22
|
|
23
|
+
from droidrun.tools.portal_client import PortalClient
|
27
24
|
logger = logging.getLogger("droidrun-tools")
|
28
25
|
PORTAL_DEFAULT_TCP_PORT = 8080
|
29
26
|
|
@@ -49,9 +46,8 @@ class AdbTools(Tools):
|
|
49
46
|
text_manipulator_llm: LLM instance for text manipulation (optional)
|
50
47
|
"""
|
51
48
|
self.device = adb.device(serial=serial)
|
52
|
-
|
53
|
-
self.
|
54
|
-
self.tcp_forwarded = False
|
49
|
+
|
50
|
+
self.portal = PortalClient(self.device, prefer_tcp=use_tcp)
|
55
51
|
|
56
52
|
self._ctx = None
|
57
53
|
# Instance‐level cache for clickable elements (index-based tapping)
|
@@ -75,10 +71,6 @@ class AdbTools(Tools):
|
|
75
71
|
from droidrun.portal import setup_keyboard
|
76
72
|
setup_keyboard(self.device)
|
77
73
|
|
78
|
-
# Set up TCP forwarding if requested
|
79
|
-
if self.use_tcp:
|
80
|
-
self.setup_tcp_forward()
|
81
|
-
|
82
74
|
|
83
75
|
def get_date(self) -> str:
|
84
76
|
"""
|
@@ -86,127 +78,9 @@ class AdbTools(Tools):
|
|
86
78
|
"""
|
87
79
|
return self.device.shell("date").strip()
|
88
80
|
|
89
|
-
|
90
|
-
def setup_tcp_forward(self) -> bool:
|
91
|
-
"""
|
92
|
-
Set up ADB TCP port forwarding for communication with the portal app.
|
93
|
-
|
94
|
-
Returns:
|
95
|
-
bool: True if forwarding was set up successfully, False otherwise
|
96
|
-
"""
|
97
|
-
try:
|
98
|
-
logger.debug(
|
99
|
-
f"Setting up TCP port forwarding for port tcp:{self.remote_tcp_port} on device {self.device.serial}"
|
100
|
-
)
|
101
|
-
# Use adb forward command to set up port forwarding
|
102
|
-
self.local_tcp_port = self.device.forward_port(self.remote_tcp_port)
|
103
|
-
self.tcp_base_url = f"http://localhost:{self.local_tcp_port}"
|
104
|
-
logger.debug(
|
105
|
-
f"TCP port forwarding set up successfully to {self.tcp_base_url}"
|
106
|
-
)
|
107
|
-
|
108
|
-
# Test the connection with a ping
|
109
|
-
try:
|
110
|
-
response = requests.get(f"{self.tcp_base_url}/ping", timeout=5)
|
111
|
-
if response.status_code == 200:
|
112
|
-
logger.debug("TCP connection test successful")
|
113
|
-
self.tcp_forwarded = True
|
114
|
-
return True
|
115
|
-
else:
|
116
|
-
logger.warning(
|
117
|
-
f"TCP connection test failed with status: {response.status_code}"
|
118
|
-
)
|
119
|
-
return False
|
120
|
-
except requests.exceptions.RequestException as e:
|
121
|
-
logger.warning(f"TCP connection test failed: {e}")
|
122
|
-
return False
|
123
|
-
|
124
|
-
except Exception as e:
|
125
|
-
logger.error(f"Failed to set up TCP port forwarding: {e}")
|
126
|
-
self.tcp_forwarded = False
|
127
|
-
return False
|
128
|
-
|
129
|
-
def teardown_tcp_forward(self) -> bool:
|
130
|
-
"""
|
131
|
-
Remove ADB TCP port forwarding.
|
132
|
-
|
133
|
-
Returns:
|
134
|
-
bool: True if forwarding was removed successfully, False otherwise
|
135
|
-
"""
|
136
|
-
try:
|
137
|
-
if self.tcp_forwarded:
|
138
|
-
logger.debug(
|
139
|
-
f"Removing TCP port forwarding for port {self.local_tcp_port}"
|
140
|
-
)
|
141
|
-
# remove forwarding
|
142
|
-
cmd = f"killforward:tcp:{self.local_tcp_port}"
|
143
|
-
logger.debug(f"Removing TCP port forwarding: {cmd}")
|
144
|
-
c = self.device.open_transport(cmd)
|
145
|
-
c.close()
|
146
|
-
|
147
|
-
self.tcp_forwarded = False
|
148
|
-
logger.debug("TCP port forwarding removed")
|
149
|
-
return True
|
150
|
-
return True
|
151
|
-
except Exception as e:
|
152
|
-
logger.error(f"Failed to remove TCP port forwarding: {e}")
|
153
|
-
return False
|
154
|
-
|
155
|
-
def __del__(self):
|
156
|
-
"""Cleanup when the object is destroyed."""
|
157
|
-
if hasattr(self, "tcp_forwarded") and self.tcp_forwarded:
|
158
|
-
self.teardown_tcp_forward()
|
159
|
-
|
160
81
|
def _set_context(self, ctx: Context):
|
161
82
|
self._ctx = ctx
|
162
83
|
|
163
|
-
def _parse_content_provider_output(
|
164
|
-
self, raw_output: str
|
165
|
-
) -> Optional[Dict[str, Any]]:
|
166
|
-
"""
|
167
|
-
Parse the raw ADB content provider output and extract JSON data.
|
168
|
-
|
169
|
-
Args:
|
170
|
-
raw_output (str): Raw output from ADB content query command
|
171
|
-
|
172
|
-
Returns:
|
173
|
-
dict: Parsed JSON data or None if parsing failed
|
174
|
-
"""
|
175
|
-
# The ADB content query output format is: "Row: 0 result={json_data}"
|
176
|
-
# We need to extract the JSON part after "result="
|
177
|
-
lines = raw_output.strip().split("\n")
|
178
|
-
|
179
|
-
for line in lines:
|
180
|
-
line = line.strip()
|
181
|
-
|
182
|
-
# Look for lines that contain "result=" pattern
|
183
|
-
if "result=" in line:
|
184
|
-
# Extract everything after "result="
|
185
|
-
result_start = line.find("result=") + 7
|
186
|
-
json_str = line[result_start:]
|
187
|
-
|
188
|
-
try:
|
189
|
-
# Parse the JSON string
|
190
|
-
json_data = json.loads(json_str)
|
191
|
-
return json_data
|
192
|
-
except json.JSONDecodeError:
|
193
|
-
continue
|
194
|
-
|
195
|
-
# Fallback: try to parse lines that start with { or [
|
196
|
-
elif line.startswith("{") or line.startswith("["):
|
197
|
-
try:
|
198
|
-
json_data = json.loads(line)
|
199
|
-
return json_data
|
200
|
-
except json.JSONDecodeError:
|
201
|
-
continue
|
202
|
-
|
203
|
-
# If no valid JSON found in individual lines, try the entire output
|
204
|
-
try:
|
205
|
-
json_data = json.loads(raw_output.strip())
|
206
|
-
return json_data
|
207
|
-
except json.JSONDecodeError:
|
208
|
-
return None
|
209
|
-
|
210
84
|
@Tools.ui_action
|
211
85
|
def _extract_element_coordinates_by_index(self, index: int) -> Tuple[int, int]:
|
212
86
|
"""
|
@@ -510,52 +384,9 @@ class AdbTools(Tools):
|
|
510
384
|
try:
|
511
385
|
if index != -1:
|
512
386
|
self.tap_by_index(index)
|
513
|
-
# Encode the text to Base64 (needed for both TCP and content provider)
|
514
|
-
encoded_text = base64.b64encode(text.encode()).decode()
|
515
|
-
|
516
|
-
if self.use_tcp and self.tcp_forwarded:
|
517
|
-
# Use TCP communication
|
518
|
-
payload = {
|
519
|
-
"base64_text": encoded_text,
|
520
|
-
"clear": clear # Include clear parameter for TCP
|
521
|
-
}
|
522
|
-
response = requests.post(
|
523
|
-
f"{self.tcp_base_url}/keyboard/input",
|
524
|
-
json=payload,
|
525
|
-
headers={"Content-Type": "application/json"},
|
526
|
-
timeout=10,
|
527
|
-
)
|
528
|
-
|
529
|
-
print(
|
530
|
-
f"Keyboard input TCP response: {response.status_code}, {response.text}"
|
531
|
-
)
|
532
387
|
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
# For TCP, you might want to parse the response for success/error details
|
537
|
-
try:
|
538
|
-
result_data = response.json()
|
539
|
-
if result_data.get("status") == "success":
|
540
|
-
return f"Text input completed (clear={clear}): {text[:50]}{'...' if len(text) > 50 else ''}"
|
541
|
-
else:
|
542
|
-
return f"Error: {result_data.get('error', 'Unknown error')}"
|
543
|
-
except: # noqa: E722
|
544
|
-
return f"Text input completed (clear={clear}): {text[:50]}{'...' if len(text) > 50 else ''}"
|
545
|
-
|
546
|
-
else:
|
547
|
-
# Fallback to content provider method
|
548
|
-
# Build the content insert command with clear parameter
|
549
|
-
clear_str = "true" if clear else "false"
|
550
|
-
cmd = (
|
551
|
-
f'content insert --uri "content://com.droidrun.portal/keyboard/input" '
|
552
|
-
f'--bind base64_text:s:"{encoded_text}" '
|
553
|
-
f'--bind clear:b:{clear_str}'
|
554
|
-
)
|
555
|
-
|
556
|
-
# Execute the command and capture output for better error handling
|
557
|
-
result = self.device.shell(cmd)
|
558
|
-
print(f"Content provider result: {result}")
|
388
|
+
# Use PortalClient for text input (automatic TCP/content provider selection)
|
389
|
+
success = self.portal.input_text(text, clear)
|
559
390
|
|
560
391
|
if self._ctx:
|
561
392
|
input_event = InputTextActionEvent(
|
@@ -565,15 +396,12 @@ class AdbTools(Tools):
|
|
565
396
|
)
|
566
397
|
self._ctx.write_event_to_stream(input_event)
|
567
398
|
|
568
|
-
|
569
|
-
f"Text input completed (clear={clear}): {text[:50]}{'...' if len(text) > 50 else ''}"
|
570
|
-
|
571
|
-
|
399
|
+
if success:
|
400
|
+
print(f"Text input completed (clear={clear}): {text[:50]}{'...' if len(text) > 50 else ''}")
|
401
|
+
return f"Text input completed (clear={clear}): {text[:50]}{'...' if len(text) > 50 else ''}"
|
402
|
+
else:
|
403
|
+
return "Error: Text input failed"
|
572
404
|
|
573
|
-
except requests.exceptions.RequestException as e:
|
574
|
-
return f"Error: TCP request failed: {str(e)}"
|
575
|
-
except ValueError as e:
|
576
|
-
return f"Error: {str(e)}"
|
577
405
|
except Exception as e:
|
578
406
|
return f"Error sending text input: {str(e)}"
|
579
407
|
|
@@ -714,56 +542,21 @@ class AdbTools(Tools):
|
|
714
542
|
"""
|
715
543
|
try:
|
716
544
|
logger.debug("Taking screenshot")
|
717
|
-
img_format = "PNG"
|
718
|
-
image_bytes = None
|
719
|
-
|
720
|
-
if self.use_tcp and self.tcp_forwarded:
|
721
|
-
# Add hideOverlay parameter to URL
|
722
|
-
url = f"{self.tcp_base_url}/screenshot"
|
723
|
-
if not hide_overlay:
|
724
|
-
url += "?hideOverlay=false"
|
725
|
-
|
726
|
-
response = requests.get(url, timeout=10)
|
727
|
-
if response.status_code == 200:
|
728
|
-
tcp_response = response.json()
|
729
|
-
|
730
|
-
# Check if response has the expected format with data field
|
731
|
-
if tcp_response.get("status") == "success" and "data" in tcp_response:
|
732
|
-
# Decode base64 string to bytes
|
733
|
-
base64_data = tcp_response["data"]
|
734
|
-
image_bytes = base64.b64decode(base64_data)
|
735
|
-
logger.debug("Screenshot taken via TCP")
|
736
|
-
else:
|
737
|
-
# Handle error response from server
|
738
|
-
error_msg = tcp_response.get("error", "Unknown error")
|
739
|
-
raise ValueError(f"Error taking screenshot via TCP: {error_msg}")
|
740
|
-
else:
|
741
|
-
raise ValueError(f"Error taking screenshot via TCP: {response.status_code}")
|
742
545
|
|
743
|
-
|
744
|
-
# Fallback to ADB screenshot method
|
745
|
-
img = self.device.screenshot()
|
746
|
-
img_buf = io.BytesIO()
|
747
|
-
img.save(img_buf, format=img_format)
|
748
|
-
image_bytes = img_buf.getvalue()
|
749
|
-
logger.debug("Screenshot taken via ADB")
|
546
|
+
image_bytes = self.portal.take_screenshot(hide_overlay)
|
750
547
|
|
751
548
|
# Store screenshot with timestamp
|
752
549
|
self.screenshots.append(
|
753
550
|
{
|
754
551
|
"timestamp": time.time(),
|
755
552
|
"image_data": image_bytes,
|
756
|
-
"format":
|
553
|
+
"format": "PNG",
|
757
554
|
}
|
758
555
|
)
|
759
|
-
return
|
556
|
+
return "PNG", image_bytes
|
760
557
|
|
761
|
-
except requests.exceptions.RequestException as e:
|
762
|
-
raise ValueError(f"Error taking screenshot via TCP: {str(e)}") from e
|
763
|
-
except ValueError as e:
|
764
|
-
raise ValueError(f"Error taking screenshot: {str(e)}") from e
|
765
558
|
except Exception as e:
|
766
|
-
raise ValueError(f"
|
559
|
+
raise ValueError(f"Error taking screenshot: {str(e)}") from e
|
767
560
|
|
768
561
|
|
769
562
|
def list_packages(self, include_system_apps: bool = False) -> List[str]:
|
@@ -792,38 +585,7 @@ class AdbTools(Tools):
|
|
792
585
|
Returns:
|
793
586
|
List of dictionaries containing 'package' and 'label' keys
|
794
587
|
"""
|
795
|
-
|
796
|
-
logger.debug("Getting apps via content provider")
|
797
|
-
|
798
|
-
# Query the content provider for packages
|
799
|
-
adb_output = self.device.shell(
|
800
|
-
"content query --uri content://com.droidrun.portal/packages"
|
801
|
-
)
|
802
|
-
|
803
|
-
# Parse the content provider output
|
804
|
-
packages_data = self._parse_content_provider_output(adb_output)
|
805
|
-
|
806
|
-
if not packages_data or "packages" not in packages_data:
|
807
|
-
logger.warning("No packages data found in content provider response")
|
808
|
-
return []
|
809
|
-
|
810
|
-
apps = []
|
811
|
-
for package_info in packages_data["packages"]:
|
812
|
-
# Filter system apps if requested
|
813
|
-
if not include_system and package_info.get("isSystemApp", False):
|
814
|
-
continue
|
815
|
-
|
816
|
-
apps.append({
|
817
|
-
"package": package_info.get("packageName", ""),
|
818
|
-
"label": package_info.get("label", "")
|
819
|
-
})
|
820
|
-
|
821
|
-
logger.debug(f"Found {len(apps)} apps")
|
822
|
-
return apps
|
823
|
-
|
824
|
-
except Exception as e:
|
825
|
-
logger.error(f"Error getting apps: {str(e)}")
|
826
|
-
raise ValueError(f"Error getting apps: {str(e)}") from e
|
588
|
+
return self.portal.get_apps(include_system)
|
827
589
|
|
828
590
|
@Tools.ui_action
|
829
591
|
def complete(self, success: bool, reason: str = ""):
|
@@ -881,82 +643,22 @@ class AdbTools(Tools):
|
|
881
643
|
"""
|
882
644
|
return self.memory.copy()
|
883
645
|
|
884
|
-
def get_state(self
|
646
|
+
def get_state(self) -> Dict[str, Any]:
|
885
647
|
"""
|
886
648
|
Get both the a11y tree and phone state in a single call using the combined /state endpoint.
|
887
649
|
|
888
|
-
Args:
|
889
|
-
serial: Optional device serial number
|
890
|
-
|
891
650
|
Returns:
|
892
651
|
Dictionary containing both 'a11y_tree' and 'phone_state' data
|
893
652
|
"""
|
894
|
-
|
895
653
|
try:
|
896
654
|
logger.debug("Getting state")
|
897
655
|
|
898
|
-
|
899
|
-
|
900
|
-
response = requests.get(f"{self.tcp_base_url}/state", timeout=10)
|
901
|
-
|
902
|
-
if response.status_code == 200:
|
903
|
-
tcp_response = response.json()
|
904
|
-
|
905
|
-
# Check if response has the expected format
|
906
|
-
if isinstance(tcp_response, dict) and "data" in tcp_response:
|
907
|
-
data_str = tcp_response["data"]
|
908
|
-
try:
|
909
|
-
combined_data = json.loads(data_str)
|
910
|
-
except json.JSONDecodeError:
|
911
|
-
return {
|
912
|
-
"error": "Parse Error",
|
913
|
-
"message": "Failed to parse JSON data from TCP response data field",
|
914
|
-
}
|
915
|
-
else:
|
916
|
-
# Fallback: assume direct JSON format
|
917
|
-
combined_data = tcp_response
|
918
|
-
else:
|
919
|
-
return {
|
920
|
-
"error": "HTTP Error",
|
921
|
-
"message": f"HTTP request failed with status {response.status_code}",
|
922
|
-
}
|
923
|
-
else:
|
924
|
-
# Fallback to content provider method
|
925
|
-
adb_output = self.device.shell(
|
926
|
-
"content query --uri content://com.droidrun.portal/state",
|
927
|
-
)
|
656
|
+
# Use PortalClient for state (automatic TCP/content provider selection)
|
657
|
+
combined_data = self.portal.get_state()
|
928
658
|
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
return {
|
933
|
-
"error": "Parse Error",
|
934
|
-
"message": "Failed to parse state data from ContentProvider response",
|
935
|
-
}
|
936
|
-
|
937
|
-
if isinstance(state_data, dict):
|
938
|
-
data_str = None
|
939
|
-
if "data" in state_data:
|
940
|
-
data_str = state_data["data"]
|
941
|
-
|
942
|
-
if data_str:
|
943
|
-
try:
|
944
|
-
combined_data = json.loads(data_str)
|
945
|
-
except json.JSONDecodeError:
|
946
|
-
return {
|
947
|
-
"error": "Parse Error",
|
948
|
-
"message": "Failed to parse JSON data from ContentProvider response",
|
949
|
-
}
|
950
|
-
else:
|
951
|
-
return {
|
952
|
-
"error": "Format Error",
|
953
|
-
"message": "Neither 'data' nor 'message' field found in ContentProvider response",
|
954
|
-
}
|
955
|
-
else:
|
956
|
-
return {
|
957
|
-
"error": "Format Error",
|
958
|
-
"message": f"Unexpected state data format: {type(state_data)}",
|
959
|
-
}
|
659
|
+
# Handle error responses
|
660
|
+
if "error" in combined_data:
|
661
|
+
return combined_data
|
960
662
|
|
961
663
|
# Validate that both a11y_tree and phone_state are present
|
962
664
|
if "a11y_tree" not in combined_data:
|
@@ -994,11 +696,6 @@ class AdbTools(Tools):
|
|
994
696
|
"phone_state": combined_data["phone_state"],
|
995
697
|
}
|
996
698
|
|
997
|
-
except requests.exceptions.RequestException as e:
|
998
|
-
return {
|
999
|
-
"error": "TCP Error",
|
1000
|
-
"message": f"TCP request failed: {str(e)}",
|
1001
|
-
}
|
1002
699
|
except Exception as e:
|
1003
700
|
return {
|
1004
701
|
"error": str(e),
|
@@ -1007,51 +704,12 @@ class AdbTools(Tools):
|
|
1007
704
|
|
1008
705
|
def ping(self) -> Dict[str, Any]:
|
1009
706
|
"""
|
1010
|
-
Test the
|
707
|
+
Test the Portal connection.
|
1011
708
|
|
1012
709
|
Returns:
|
1013
710
|
Dictionary with ping result
|
1014
711
|
"""
|
1015
|
-
|
1016
|
-
if self.use_tcp and self.tcp_forwarded:
|
1017
|
-
response = requests.get(f"{self.tcp_base_url}/ping", timeout=5)
|
1018
|
-
|
1019
|
-
if response.status_code == 200:
|
1020
|
-
try:
|
1021
|
-
tcp_response = response.json() if response.content else {}
|
1022
|
-
logger.debug(f"Ping TCP response: {tcp_response}")
|
1023
|
-
return {
|
1024
|
-
"status": "success",
|
1025
|
-
"message": "Ping successful",
|
1026
|
-
"response": tcp_response,
|
1027
|
-
}
|
1028
|
-
except json.JSONDecodeError:
|
1029
|
-
return {
|
1030
|
-
"status": "success",
|
1031
|
-
"message": "Ping successful (non-JSON response)",
|
1032
|
-
"response": response.text,
|
1033
|
-
}
|
1034
|
-
else:
|
1035
|
-
return {
|
1036
|
-
"status": "error",
|
1037
|
-
"message": f"Ping failed with status {response.status_code}: {response.text}",
|
1038
|
-
}
|
1039
|
-
else:
|
1040
|
-
return {
|
1041
|
-
"status": "error",
|
1042
|
-
"message": "TCP communication is not enabled",
|
1043
|
-
}
|
1044
|
-
|
1045
|
-
except requests.exceptions.RequestException as e:
|
1046
|
-
return {
|
1047
|
-
"status": "error",
|
1048
|
-
"message": f"Ping failed: {str(e)}",
|
1049
|
-
}
|
1050
|
-
except Exception as e:
|
1051
|
-
return {
|
1052
|
-
"status": "error",
|
1053
|
-
"message": f"Error during ping: {str(e)}",
|
1054
|
-
}
|
712
|
+
return self.portal.ping()
|
1055
713
|
|
1056
714
|
|
1057
715
|
def _shell_test_cli(serial: str, command: str) -> tuple[str, float]:
|
@@ -0,0 +1,434 @@
|
|
1
|
+
"""
|
2
|
+
Portal Client - Unified communication layer for DroidRun Portal app.
|
3
|
+
|
4
|
+
This module provides automatic TCP/Content Provider fallback for Portal communication.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import base64
|
8
|
+
import io
|
9
|
+
import json
|
10
|
+
import logging
|
11
|
+
import re
|
12
|
+
from typing import Any, Dict, List, Optional
|
13
|
+
|
14
|
+
import requests
|
15
|
+
from adbutils import AdbDevice
|
16
|
+
|
17
|
+
logger = logging.getLogger("droidrun-tools")
|
18
|
+
|
19
|
+
PORTAL_REMOTE_PORT = 8080 # Port on device where Portal HTTP server runs
|
20
|
+
|
21
|
+
|
22
|
+
class PortalClient:
|
23
|
+
"""
|
24
|
+
Unified client for DroidRun Portal communication.
|
25
|
+
|
26
|
+
Automatically handles TCP vs Content Provider fallback with the following strategy:
|
27
|
+
- On init, checks for existing port forward and reuses it
|
28
|
+
- If no forward exists, creates new one
|
29
|
+
- Tests connection and sets tcp_available flag
|
30
|
+
- All methods auto-select TCP or content provider based on availability
|
31
|
+
- No cleanup needed - forwards persist until device disconnect
|
32
|
+
|
33
|
+
Key features:
|
34
|
+
- Reuses existing port forwards (no cleanup needed)
|
35
|
+
- Automatic fallback to content provider if TCP fails
|
36
|
+
- Zero explicit resource management
|
37
|
+
- Graceful degradation
|
38
|
+
"""
|
39
|
+
|
40
|
+
def __init__(self, device: AdbDevice, prefer_tcp: bool = False):
|
41
|
+
"""
|
42
|
+
Initialize Portal client.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
device: ADB device instance
|
46
|
+
prefer_tcp: Whether to prefer TCP communication (will fallback to content provider if unavailable)
|
47
|
+
"""
|
48
|
+
self.device = device
|
49
|
+
self.tcp_available = False
|
50
|
+
self.tcp_base_url = None
|
51
|
+
self.local_tcp_port = None
|
52
|
+
|
53
|
+
if prefer_tcp:
|
54
|
+
self._try_enable_tcp()
|
55
|
+
|
56
|
+
def _try_enable_tcp(self) -> None:
|
57
|
+
"""
|
58
|
+
Try to enable TCP communication. Fails silently and falls back to content provider.
|
59
|
+
|
60
|
+
Strategy:
|
61
|
+
1. Check if forward already exists → reuse
|
62
|
+
2. If not, create new forward
|
63
|
+
3. Test connection with ping
|
64
|
+
4. Set tcp_available flag
|
65
|
+
"""
|
66
|
+
try:
|
67
|
+
# Step 1: Check for existing forward
|
68
|
+
local_port = self._find_existing_forward()
|
69
|
+
|
70
|
+
# Step 2: If no forward exists, create one
|
71
|
+
if local_port is None:
|
72
|
+
logger.debug(f"No existing forward found, creating new forward for port {PORTAL_REMOTE_PORT}")
|
73
|
+
local_port = self.device.forward_port(PORTAL_REMOTE_PORT)
|
74
|
+
logger.debug(f"Created forward: localhost:{local_port} -> device:{PORTAL_REMOTE_PORT}")
|
75
|
+
else:
|
76
|
+
logger.debug(f"Reusing existing forward: localhost:{local_port} -> device:{PORTAL_REMOTE_PORT}")
|
77
|
+
|
78
|
+
# Store local port
|
79
|
+
self.local_tcp_port = local_port
|
80
|
+
|
81
|
+
# Step 3: Test connection
|
82
|
+
self.tcp_base_url = f"http://localhost:{local_port}"
|
83
|
+
if self._test_connection():
|
84
|
+
self.tcp_available = True
|
85
|
+
logger.info(f"✓ TCP mode enabled: {self.tcp_base_url}")
|
86
|
+
else:
|
87
|
+
logger.warning("TCP connection test failed, falling back to content provider")
|
88
|
+
self.tcp_available = False
|
89
|
+
|
90
|
+
except Exception as e:
|
91
|
+
logger.warning(f"Failed to setup TCP forwarding: {e}. Using content provider fallback.")
|
92
|
+
self.tcp_available = False
|
93
|
+
|
94
|
+
def _find_existing_forward(self) -> Optional[int]:
|
95
|
+
"""
|
96
|
+
Check if a forward already exists for the Portal remote port.
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
Local port number if forward exists, None otherwise
|
100
|
+
"""
|
101
|
+
try:
|
102
|
+
forwards = self.device.forward_list()
|
103
|
+
# Format: ['serial tcp:local_port tcp:remote_port', ...]
|
104
|
+
for forward in forwards:
|
105
|
+
if self.device.serial in forward and f"tcp:{PORTAL_REMOTE_PORT}" in forward:
|
106
|
+
# Extract local port: "serial tcp:12345 tcp:8080"
|
107
|
+
match = re.search(r'tcp:(\d+)\s+tcp:' + str(PORTAL_REMOTE_PORT), forward)
|
108
|
+
if match:
|
109
|
+
local_port = int(match.group(1))
|
110
|
+
logger.debug(f"Found existing forward: localhost:{local_port} -> {PORTAL_REMOTE_PORT}")
|
111
|
+
return local_port
|
112
|
+
except Exception as e:
|
113
|
+
logger.debug(f"Failed to check existing forwards: {e}")
|
114
|
+
|
115
|
+
return None
|
116
|
+
|
117
|
+
def _test_connection(self) -> bool:
|
118
|
+
"""Test if TCP connection to Portal is working."""
|
119
|
+
try:
|
120
|
+
response = requests.get(f"{self.tcp_base_url}/ping", timeout=3)
|
121
|
+
return response.status_code == 200
|
122
|
+
except Exception as e:
|
123
|
+
logger.debug(f"TCP connection test failed: {e}")
|
124
|
+
return False
|
125
|
+
|
126
|
+
def _parse_content_provider_output(self, raw_output: str) -> Optional[Dict[str, Any]]:
|
127
|
+
"""
|
128
|
+
Parse the raw ADB content provider output and extract JSON data.
|
129
|
+
|
130
|
+
Args:
|
131
|
+
raw_output: Raw output from ADB content query command
|
132
|
+
|
133
|
+
Returns:
|
134
|
+
Parsed JSON data or None if parsing failed
|
135
|
+
"""
|
136
|
+
lines = raw_output.strip().split("\n")
|
137
|
+
|
138
|
+
# Try line-by-line parsing
|
139
|
+
for line in lines:
|
140
|
+
line = line.strip()
|
141
|
+
|
142
|
+
# Look for "result=" pattern (common content provider format)
|
143
|
+
if "result=" in line:
|
144
|
+
result_start = line.find("result=") + 7
|
145
|
+
json_str = line[result_start:]
|
146
|
+
try:
|
147
|
+
json_data = json.loads(json_str)
|
148
|
+
# Handle nested "data" field with JSON string
|
149
|
+
if isinstance(json_data, dict) and "data" in json_data:
|
150
|
+
if isinstance(json_data["data"], str):
|
151
|
+
return json.loads(json_data["data"])
|
152
|
+
return json_data
|
153
|
+
except json.JSONDecodeError:
|
154
|
+
continue
|
155
|
+
|
156
|
+
# Fallback: try lines starting with JSON
|
157
|
+
elif line.startswith("{") or line.startswith("["):
|
158
|
+
try:
|
159
|
+
return json.loads(line)
|
160
|
+
except json.JSONDecodeError:
|
161
|
+
continue
|
162
|
+
|
163
|
+
# Last resort: try parsing entire output
|
164
|
+
try:
|
165
|
+
return json.loads(raw_output.strip())
|
166
|
+
except json.JSONDecodeError:
|
167
|
+
return None
|
168
|
+
|
169
|
+
|
170
|
+
def get_state(self) -> Dict[str, Any]:
|
171
|
+
"""
|
172
|
+
Get device state (accessibility tree + phone state).
|
173
|
+
Auto-selects TCP or content provider.
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
Dictionary containing 'a11y_tree' and 'phone_state' keys
|
177
|
+
"""
|
178
|
+
if self.tcp_available:
|
179
|
+
return self._get_state_tcp()
|
180
|
+
return self._get_state_content_provider()
|
181
|
+
|
182
|
+
def _get_state_tcp(self) -> Dict[str, Any]:
|
183
|
+
"""Get state via TCP."""
|
184
|
+
try:
|
185
|
+
response = requests.get(f"{self.tcp_base_url}/state", timeout=10)
|
186
|
+
if response.status_code == 200:
|
187
|
+
data = response.json()
|
188
|
+
|
189
|
+
# Handle nested "data" field
|
190
|
+
if isinstance(data, dict) and "data" in data:
|
191
|
+
if isinstance(data["data"], str):
|
192
|
+
return json.loads(data["data"])
|
193
|
+
return data
|
194
|
+
else:
|
195
|
+
logger.warning(f"TCP get_state failed ({response.status_code}), falling back")
|
196
|
+
return self._get_state_content_provider()
|
197
|
+
except Exception as e:
|
198
|
+
logger.warning(f"TCP get_state error: {e}, falling back")
|
199
|
+
return self._get_state_content_provider()
|
200
|
+
|
201
|
+
def _get_state_content_provider(self) -> Dict[str, Any]:
|
202
|
+
"""Get state via content provider (fallback)."""
|
203
|
+
try:
|
204
|
+
output = self.device.shell("content query --uri content://com.droidrun.portal/state")
|
205
|
+
state_data = self._parse_content_provider_output(output)
|
206
|
+
|
207
|
+
if state_data is None:
|
208
|
+
return {
|
209
|
+
"error": "Parse Error",
|
210
|
+
"message": "Failed to parse state data from ContentProvider"
|
211
|
+
}
|
212
|
+
|
213
|
+
# Handle nested "data" field if present
|
214
|
+
if isinstance(state_data, dict) and "data" in state_data:
|
215
|
+
if isinstance(state_data["data"], str):
|
216
|
+
try:
|
217
|
+
return json.loads(state_data["data"])
|
218
|
+
except json.JSONDecodeError:
|
219
|
+
return {
|
220
|
+
"error": "Parse Error",
|
221
|
+
"message": "Failed to parse nested JSON data"
|
222
|
+
}
|
223
|
+
|
224
|
+
return state_data
|
225
|
+
|
226
|
+
except Exception as e:
|
227
|
+
return {
|
228
|
+
"error": "ContentProvider Error",
|
229
|
+
"message": str(e)
|
230
|
+
}
|
231
|
+
|
232
|
+
|
233
|
+
def input_text(self, text: str, clear: bool = False) -> bool:
|
234
|
+
"""
|
235
|
+
Input text via keyboard.
|
236
|
+
Auto-selects TCP or content provider.
|
237
|
+
|
238
|
+
Args:
|
239
|
+
text: Text to input
|
240
|
+
clear: Whether to clear existing text first
|
241
|
+
|
242
|
+
Returns:
|
243
|
+
True if successful, False otherwise
|
244
|
+
"""
|
245
|
+
if self.tcp_available:
|
246
|
+
return self._input_text_tcp(text, clear)
|
247
|
+
return self._input_text_content_provider(text, clear)
|
248
|
+
|
249
|
+
def _input_text_tcp(self, text: str, clear: bool) -> bool:
|
250
|
+
"""Input text via TCP."""
|
251
|
+
try:
|
252
|
+
encoded = base64.b64encode(text.encode()).decode()
|
253
|
+
payload = {"base64_text": encoded, "clear": clear}
|
254
|
+
response = requests.post(
|
255
|
+
f"{self.tcp_base_url}/keyboard/input",
|
256
|
+
json=payload,
|
257
|
+
headers={"Content-Type": "application/json"},
|
258
|
+
timeout=10
|
259
|
+
)
|
260
|
+
if response.status_code == 200:
|
261
|
+
logger.debug(f"TCP input_text successful")
|
262
|
+
return True
|
263
|
+
else:
|
264
|
+
logger.warning(f"TCP input_text failed ({response.status_code}), falling back")
|
265
|
+
return self._input_text_content_provider(text, clear)
|
266
|
+
except Exception as e:
|
267
|
+
logger.warning(f"TCP input_text error: {e}, falling back")
|
268
|
+
return self._input_text_content_provider(text, clear)
|
269
|
+
|
270
|
+
def _input_text_content_provider(self, text: str, clear: bool) -> bool:
|
271
|
+
"""Input text via content provider (fallback)."""
|
272
|
+
try:
|
273
|
+
encoded = base64.b64encode(text.encode()).decode()
|
274
|
+
clear_str = "true" if clear else "false"
|
275
|
+
cmd = (
|
276
|
+
f'content insert --uri "content://com.droidrun.portal/keyboard/input" '
|
277
|
+
f'--bind base64_text:s:"{encoded}" '
|
278
|
+
f'--bind clear:b:{clear_str}'
|
279
|
+
)
|
280
|
+
self.device.shell(cmd)
|
281
|
+
logger.debug("Content provider input_text successful")
|
282
|
+
return True
|
283
|
+
except Exception as e:
|
284
|
+
logger.error(f"Content provider input_text error: {e}")
|
285
|
+
return False
|
286
|
+
|
287
|
+
|
288
|
+
def take_screenshot(self, hide_overlay: bool = True) -> bytes:
|
289
|
+
"""
|
290
|
+
Take screenshot of device.
|
291
|
+
Auto-selects TCP or ADB screencap.
|
292
|
+
|
293
|
+
Args:
|
294
|
+
hide_overlay: Whether to hide Portal overlay during screenshot
|
295
|
+
|
296
|
+
Returns:
|
297
|
+
Screenshot image bytes (PNG format)
|
298
|
+
"""
|
299
|
+
if self.tcp_available:
|
300
|
+
return self._take_screenshot_tcp(hide_overlay)
|
301
|
+
return self._take_screenshot_adb()
|
302
|
+
|
303
|
+
def _take_screenshot_tcp(self, hide_overlay: bool) -> bytes:
|
304
|
+
"""Take screenshot via TCP."""
|
305
|
+
try:
|
306
|
+
url = f"{self.tcp_base_url}/screenshot"
|
307
|
+
if not hide_overlay:
|
308
|
+
url += "?hideOverlay=false"
|
309
|
+
|
310
|
+
response = requests.get(url, timeout=10)
|
311
|
+
if response.status_code == 200:
|
312
|
+
data = response.json()
|
313
|
+
if data.get("status") == "success" and "data" in data:
|
314
|
+
logger.debug("Screenshot taken via TCP")
|
315
|
+
return base64.b64decode(data["data"])
|
316
|
+
else:
|
317
|
+
logger.warning("TCP screenshot failed (invalid response), falling back")
|
318
|
+
return self._take_screenshot_adb()
|
319
|
+
else:
|
320
|
+
logger.warning(f"TCP screenshot failed ({response.status_code}), falling back")
|
321
|
+
return self._take_screenshot_adb()
|
322
|
+
except Exception as e:
|
323
|
+
logger.warning(f"TCP screenshot error: {e}, falling back")
|
324
|
+
return self._take_screenshot_adb()
|
325
|
+
|
326
|
+
def _take_screenshot_adb(self) -> bytes:
|
327
|
+
"""Take screenshot via ADB screencap (fallback)."""
|
328
|
+
img = self.device.screenshot()
|
329
|
+
buf = io.BytesIO()
|
330
|
+
img.save(buf, format="PNG")
|
331
|
+
logger.debug("Screenshot taken via ADB")
|
332
|
+
return buf.getvalue()
|
333
|
+
|
334
|
+
def get_apps(self, include_system: bool = True) -> List[Dict[str, str]]:
|
335
|
+
"""
|
336
|
+
Get installed apps with package name and label.
|
337
|
+
|
338
|
+
Note: Currently only supports content provider (no TCP endpoint exists yet)
|
339
|
+
|
340
|
+
Args:
|
341
|
+
include_system: Whether to include system apps
|
342
|
+
|
343
|
+
Returns:
|
344
|
+
List of dicts with 'package' and 'label' keys
|
345
|
+
"""
|
346
|
+
try:
|
347
|
+
logger.debug("Getting apps via content provider")
|
348
|
+
|
349
|
+
# Query content provider
|
350
|
+
output = self.device.shell("content query --uri content://com.droidrun.portal/packages")
|
351
|
+
packages_data = self._parse_content_provider_output(output)
|
352
|
+
|
353
|
+
if not packages_data or "packages" not in packages_data:
|
354
|
+
logger.warning("No packages data found in content provider response")
|
355
|
+
return []
|
356
|
+
|
357
|
+
# Filter and format apps
|
358
|
+
apps = []
|
359
|
+
for package_info in packages_data["packages"]:
|
360
|
+
if not include_system and package_info.get("isSystemApp", False):
|
361
|
+
continue
|
362
|
+
|
363
|
+
apps.append({
|
364
|
+
"package": package_info.get("packageName", ""),
|
365
|
+
"label": package_info.get("label", "")
|
366
|
+
})
|
367
|
+
|
368
|
+
logger.debug(f"Found {len(apps)} apps")
|
369
|
+
return apps
|
370
|
+
|
371
|
+
except Exception as e:
|
372
|
+
logger.error(f"Error getting apps: {e}")
|
373
|
+
raise ValueError(f"Error getting apps: {e}") from e
|
374
|
+
|
375
|
+
|
376
|
+
def ping(self) -> Dict[str, Any]:
|
377
|
+
"""
|
378
|
+
Test Portal connection.
|
379
|
+
|
380
|
+
Returns:
|
381
|
+
Dictionary with status and connection details
|
382
|
+
"""
|
383
|
+
if self.tcp_available:
|
384
|
+
try:
|
385
|
+
response = requests.get(f"{self.tcp_base_url}/ping", timeout=5)
|
386
|
+
if response.status_code == 200:
|
387
|
+
try:
|
388
|
+
tcp_response = response.json() if response.content else {}
|
389
|
+
return {
|
390
|
+
"status": "success",
|
391
|
+
"method": "tcp",
|
392
|
+
"url": self.tcp_base_url,
|
393
|
+
"response": tcp_response
|
394
|
+
}
|
395
|
+
except json.JSONDecodeError:
|
396
|
+
return {
|
397
|
+
"status": "success",
|
398
|
+
"method": "tcp",
|
399
|
+
"url": self.tcp_base_url,
|
400
|
+
"response": response.text
|
401
|
+
}
|
402
|
+
else:
|
403
|
+
return {
|
404
|
+
"status": "error",
|
405
|
+
"method": "tcp",
|
406
|
+
"message": f"HTTP {response.status_code}: {response.text}"
|
407
|
+
}
|
408
|
+
except Exception as e:
|
409
|
+
return {
|
410
|
+
"status": "error",
|
411
|
+
"method": "tcp",
|
412
|
+
"message": str(e)
|
413
|
+
}
|
414
|
+
else:
|
415
|
+
# Test content provider
|
416
|
+
try:
|
417
|
+
output = self.device.shell("content query --uri content://com.droidrun.portal/state")
|
418
|
+
if "Row: 0 result=" in output:
|
419
|
+
return {
|
420
|
+
"status": "success",
|
421
|
+
"method": "content_provider"
|
422
|
+
}
|
423
|
+
else:
|
424
|
+
return {
|
425
|
+
"status": "error",
|
426
|
+
"method": "content_provider",
|
427
|
+
"message": "Invalid response"
|
428
|
+
}
|
429
|
+
except Exception as e:
|
430
|
+
return {
|
431
|
+
"status": "error",
|
432
|
+
"method": "content_provider",
|
433
|
+
"message": str(e)
|
434
|
+
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: droidrun
|
3
|
-
Version: 0.3.10.
|
3
|
+
Version: 0.3.10.dev9
|
4
4
|
Summary: A framework for controlling Android devices through LLM agents
|
5
5
|
Project-URL: Homepage, https://github.com/droidrun/droidrun
|
6
6
|
Project-URL: Bug Tracker, https://github.com/droidrun/droidrun/issues
|
@@ -6,6 +6,7 @@ droidrun/agent/usage.py,sha256=6PVeHctNa0EmHmNPTdOUv5e3-EK6AMu6D2Pz5OMqs5c,7145
|
|
6
6
|
droidrun/agent/codeact/__init__.py,sha256=lagBdrury33kbHN1XEZ-xzJ-RywmpkUUoUidOno9ym8,96
|
7
7
|
droidrun/agent/codeact/codeact_agent.py,sha256=7EkuazNIpTOX-W1oSG0XmtOwcmNOyaiPddgNxnK10No,20292
|
8
8
|
droidrun/agent/codeact/events.py,sha256=kRKTQPzogPiQwmOCc_fGcg1g1zDXXVeBpDl45GTdpYU,734
|
9
|
+
droidrun/agent/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
10
|
droidrun/agent/common/constants.py,sha256=q7ywmOXCsJZg8m9ctpzQ-nxvuj5GMn28Pr8z3dMj1Rg,94
|
10
11
|
droidrun/agent/common/events.py,sha256=rbPWdlqNNMdnVjYhJOL2mJcNNORHhjXOkY8XiLPzp7c,1182
|
11
12
|
droidrun/agent/context/__init__.py,sha256=-CiAv66qym_WgFy5vCRfNLxmiprmEbssu6S_2jj0LZw,452
|
@@ -22,6 +23,7 @@ droidrun/agent/manager/__init__.py,sha256=A8esHVpxzHd3Epzkl0j5seNkRQqwNEn1a97eeL
|
|
22
23
|
droidrun/agent/manager/events.py,sha256=X0tUwCX2mU8I4bGR4JW2NmUqiOrX-Hrb017vGVPVyHw,855
|
23
24
|
droidrun/agent/manager/manager_agent.py,sha256=nXftmLlSLDs9LLB3rHE3EzpaCnUKa6v1dNfjFTMV9ys,22256
|
24
25
|
droidrun/agent/manager/prompts.py,sha256=qfDYcSbpWpnUaavAuPE6qY6Df6w25LmtY1mEiBUMti0,2060
|
26
|
+
droidrun/agent/oneflows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
25
27
|
droidrun/agent/oneflows/app_starter_workflow.py,sha256=MSJ6_jfbiCfSIjnw-qfSDFDuqsUS6rUGLsdKVj43wvY,3525
|
26
28
|
droidrun/agent/oneflows/text_manipulator.py,sha256=mO59DF1uif9poUWy90UehrBmHbNxL9ph4Evtgt1ODbQ,8751
|
27
29
|
droidrun/agent/utils/__init__.py,sha256=Oro0oyiz1xzRpchWLDA1TZJELJNSwBOb2WdGgknthKo,244
|
@@ -34,6 +36,7 @@ droidrun/agent/utils/llm_picker.py,sha256=KQzrRcHE38NwujDbNth5F9v5so9HVvHjfkQznM
|
|
34
36
|
droidrun/agent/utils/message_utils.py,sha256=_wngf082gg232y_3pC_yn4fnPhHiyYAxhU4ewT78roo,2309
|
35
37
|
droidrun/agent/utils/tools.py,sha256=anc10NAKmZx91JslHFpo6wfnUOZ2pnPXJS-5nMVHC_A,9930
|
36
38
|
droidrun/agent/utils/trajectory.py,sha256=Z6C19Y9hsRxjLZWywqYWTApKU7PelvWM-5Tsl3h7KEw,19718
|
39
|
+
droidrun/app_cards/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
37
40
|
droidrun/app_cards/app_card_provider.py,sha256=wy7CGFnBd_EPU58xNdv4ZWUA9F4Plon71N4-5RT5vNg,827
|
38
41
|
droidrun/app_cards/providers/__init__.py,sha256=vN4TvBtsvfdvzgqbIJegIfHhct0aTFZjvJazWFDvdhg,372
|
39
42
|
droidrun/app_cards/providers/composite_provider.py,sha256=oi7dlkv_Hv2rEZMxQlO1jP9fQcTBydr40zCyunCNxQA,3156
|
@@ -41,7 +44,7 @@ droidrun/app_cards/providers/local_provider.py,sha256=RRGQ7VR7qHT9uKSOlSvqCTRq_p
|
|
41
44
|
droidrun/app_cards/providers/server_provider.py,sha256=rOJyiCE_zTCCK9SAJeee3vLWISytoZrBUiXB6LaJEv8,4148
|
42
45
|
droidrun/cli/__init__.py,sha256=5cO-QBcUl5w35zO18OENj4OShdglQjn8Ne9aqgSh-PM,167
|
43
46
|
droidrun/cli/logs.py,sha256=V8rn6oXgYObExX4dG8MUnQXxUdKOk1QlTkOQtI5e6wo,12686
|
44
|
-
droidrun/cli/main.py,sha256=
|
47
|
+
droidrun/cli/main.py,sha256=lzGwWk8SbbxmVeyz2mqkPBT6Xs5SMxnQSxecuK-7L5s,35119
|
45
48
|
droidrun/config_manager/__init__.py,sha256=SeLoEYVU5jMEtXLjx76VE_3rxzZXjCMlVPW7hodU128,460
|
46
49
|
droidrun/config_manager/config_manager.py,sha256=hPETII_5wYvfb11e7sJlfCVk9p3WbA7nHPAV3bQQdmE,19930
|
47
50
|
droidrun/config_manager/path_resolver.py,sha256=vQKT5XmnENtSK3B1D-iItL8CpOQTKzfKZ1wTO4khlTs,3421
|
@@ -55,11 +58,12 @@ droidrun/telemetry/events.py,sha256=y-i2d5KiPkikVXrzMQu87osy1LAZTBIx8DlPIWGAXG0,
|
|
55
58
|
droidrun/telemetry/phoenix.py,sha256=JHdFdRHXu7cleAb4X4_Y5yn5zPSIApwyKCOxoaj_gf4,7117
|
56
59
|
droidrun/telemetry/tracker.py,sha256=YWOkyLE8XiHainVSB77JE37y-rloOYVYs6j53Aw1J8A,2735
|
57
60
|
droidrun/tools/__init__.py,sha256=BbQFKuPn-5MwGzr-3urMDK8S1ZsP96D96y7WTJYB3AA,271
|
58
|
-
droidrun/tools/adb.py,sha256=
|
61
|
+
droidrun/tools/adb.py,sha256=PRbQS1qhy_HFUVx78LYPwTa4GT4TgMrRTwc7NM4Tf4A,28950
|
59
62
|
droidrun/tools/ios.py,sha256=GMYbiNNBeHLwVQAo4_fEZ7snr4JCHE6sG11rcuPvSpk,21831
|
63
|
+
droidrun/tools/portal_client.py,sha256=BthC-ryHtCxh3czmkTge5aaurinPZfFkV4DgbkD_wbw,16307
|
60
64
|
droidrun/tools/tools.py,sha256=0eAZFTaY10eiiUcJM4AkURmTGX-O1RRXjpQ5MHj2Ydo,5241
|
61
|
-
droidrun-0.3.10.
|
62
|
-
droidrun-0.3.10.
|
63
|
-
droidrun-0.3.10.
|
64
|
-
droidrun-0.3.10.
|
65
|
-
droidrun-0.3.10.
|
65
|
+
droidrun-0.3.10.dev9.dist-info/METADATA,sha256=YJPn9618Zy-2d_jo_46H45eSOi517Br1bxZ-7513_ms,7044
|
66
|
+
droidrun-0.3.10.dev9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
67
|
+
droidrun-0.3.10.dev9.dist-info/entry_points.txt,sha256=o259U66js8TIybQ7zs814Oe_LQ_GpZsp6a9Cr-xm5zE,51
|
68
|
+
droidrun-0.3.10.dev9.dist-info/licenses/LICENSE,sha256=s-uxn9qChu-kFdRXUp6v_0HhsaJ_5OANmfNOFVm2zdk,1069
|
69
|
+
droidrun-0.3.10.dev9.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|