droidrun 0.3.10.dev7__tar.gz → 0.3.10.dev8__tar.gz

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.
Files changed (121) hide show
  1. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/PKG-INFO +1 -1
  2. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/cli/main.py +7 -10
  3. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/tools/adb.py +23 -365
  4. droidrun-0.3.10.dev8/droidrun/tools/portal_client.py +434 -0
  5. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/pyproject.toml +1 -1
  6. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/uv.lock +1 -1
  7. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/.github/workflows/bounty.yml +0 -0
  8. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/.github/workflows/publish.yml +0 -0
  9. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/.gitignore +0 -0
  10. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/.python-version +0 -0
  11. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/CHANGELOG.md +0 -0
  12. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/CONTRIBUTING.md +0 -0
  13. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/LICENSE +0 -0
  14. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/MANIFEST.in +0 -0
  15. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/README.md +0 -0
  16. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/config/app_cards/README.md +0 -0
  17. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/config/app_cards/app_cards.json +0 -0
  18. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/config/app_cards/gmail.md +0 -0
  19. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/config/prompts/codeact/system.jinja2 +0 -0
  20. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/config/prompts/codeact/user.jinja2 +0 -0
  21. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/config/prompts/executor/rev1.jinja2 +0 -0
  22. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/config/prompts/executor/system.jinja2 +0 -0
  23. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/config/prompts/manager/rev1.jinja2 +0 -0
  24. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/config/prompts/manager/system.jinja2 +0 -0
  25. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/config_example.yaml +0 -0
  26. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/.generated-files.txt +0 -0
  27. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/docs.json +0 -0
  28. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/favicon.png +0 -0
  29. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/logo/dark.svg +0 -0
  30. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/logo/light.svg +0 -0
  31. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v1/concepts/agent.mdx +0 -0
  32. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v1/concepts/android-control.mdx +0 -0
  33. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v1/concepts/portal-app.mdx +0 -0
  34. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v1/overview.mdx +0 -0
  35. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v1/quickstart.mdx +0 -0
  36. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v2/concepts/agent.mdx +0 -0
  37. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v2/concepts/android-control.mdx +0 -0
  38. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v2/concepts/planning.mdx +0 -0
  39. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v2/concepts/portal-app.mdx +0 -0
  40. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v2/concepts/tracing.mdx +0 -0
  41. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v2/overview.mdx +0 -0
  42. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v2/quickstart.mdx +0 -0
  43. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v3/concepts/agent.mdx +0 -0
  44. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v3/concepts/android-tools.mdx +0 -0
  45. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v3/concepts/models.mdx +0 -0
  46. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v3/concepts/portal-app.mdx +0 -0
  47. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v3/guides/cli.mdx +0 -0
  48. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v3/guides/gemini.mdx +0 -0
  49. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v3/guides/ollama.mdx +0 -0
  50. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v3/guides/openailike.mdx +0 -0
  51. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v3/guides/overview.mdx +0 -0
  52. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v3/guides/telemetry.mdx +0 -0
  53. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v3/images/portal_apk.png +0 -0
  54. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v3/overview.mdx +0 -0
  55. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v3/quickstart.mdx +0 -0
  56. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v3/sdk/adb-tools.mdx +0 -0
  57. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v3/sdk/base-tools.mdx +0 -0
  58. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v3/sdk/droid-agent.mdx +0 -0
  59. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/docs/v3/sdk/ios-tools.mdx +0 -0
  60. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/__init__.py +0 -0
  61. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/__main__.py +0 -0
  62. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/__init__.py +0 -0
  63. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/codeact/__init__.py +0 -0
  64. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/codeact/codeact_agent.py +0 -0
  65. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/codeact/events.py +0 -0
  66. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/common/constants.py +0 -0
  67. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/common/events.py +0 -0
  68. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/context/__init__.py +0 -0
  69. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/context/episodic_memory.py +0 -0
  70. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/context/task_manager.py +0 -0
  71. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/droid/__init__.py +0 -0
  72. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/droid/droid_agent.py +0 -0
  73. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/droid/events.py +0 -0
  74. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/executor/__init__.py +0 -0
  75. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/executor/events.py +0 -0
  76. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/executor/executor_agent.py +0 -0
  77. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/executor/prompts.py +0 -0
  78. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/manager/__init__.py +0 -0
  79. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/manager/events.py +0 -0
  80. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/manager/manager_agent.py +0 -0
  81. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/manager/prompts.py +0 -0
  82. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/oneflows/app_starter_workflow.py +0 -0
  83. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/oneflows/text_manipulator.py +0 -0
  84. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/usage.py +0 -0
  85. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/utils/__init__.py +0 -0
  86. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/utils/async_utils.py +0 -0
  87. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/utils/chat_utils.py +0 -0
  88. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/utils/device_state_formatter.py +0 -0
  89. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/utils/executer.py +0 -0
  90. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/utils/inference.py +0 -0
  91. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/utils/llm_picker.py +0 -0
  92. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/utils/message_utils.py +0 -0
  93. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/utils/tools.py +0 -0
  94. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/agent/utils/trajectory.py +0 -0
  95. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/app_cards/app_card_provider.py +0 -0
  96. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/app_cards/providers/__init__.py +0 -0
  97. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/app_cards/providers/composite_provider.py +0 -0
  98. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/app_cards/providers/local_provider.py +0 -0
  99. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/app_cards/providers/server_provider.py +0 -0
  100. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/cli/__init__.py +0 -0
  101. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/cli/logs.py +0 -0
  102. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/config_manager/__init__.py +0 -0
  103. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/config_manager/config_manager.py +0 -0
  104. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/config_manager/path_resolver.py +0 -0
  105. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/config_manager/prompt_loader.py +0 -0
  106. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/macro/__init__.py +0 -0
  107. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/macro/__main__.py +0 -0
  108. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/macro/cli.py +0 -0
  109. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/macro/replay.py +0 -0
  110. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/portal.py +0 -0
  111. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/telemetry/__init__.py +0 -0
  112. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/telemetry/events.py +0 -0
  113. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/telemetry/phoenix.py +0 -0
  114. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/telemetry/tracker.py +0 -0
  115. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/tools/__init__.py +0 -0
  116. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/tools/ios.py +0 -0
  117. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/droidrun/tools/tools.py +0 -0
  118. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/gen-docs-sdk-ref.sh +0 -0
  119. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/setup.py +0 -0
  120. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/static/droidrun-dark.png +0 -0
  121. {droidrun-0.3.10.dev7 → droidrun-0.3.10.dev8}/static/droidrun.png +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: droidrun
3
- Version: 0.3.10.dev7
3
+ Version: 0.3.10.dev8
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
@@ -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
- device_serial = adb.device().serial
561
- if device_serial:
562
- tools = AdbTools(serial=device, use_tcp=use_tcp if use_tcp is not None else False)
563
- if hasattr(tools, 'device') and tools.device:
564
- tools.device.shell("ime disable com.droidrun.portal/.DroidrunKeyboardIME")
565
- click.echo("DroidRun keyboard disabled successfully")
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()
@@ -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
- self.use_tcp = use_tcp
53
- self.remote_tcp_port = remote_tcp_port
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
- if response.status_code != 200:
534
- return f"Error: HTTP request failed with status {response.status_code}: {response.text}"
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
- print(
569
- f"Text input completed (clear={clear}): {text[:50]}{'...' if len(text) > 50 else ''}"
570
- )
571
- return f"Text input completed (clear={clear}): {text[:50]}{'...' if len(text) > 50 else ''}"
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
- else:
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": img_format,
553
+ "format": "PNG",
757
554
  }
758
555
  )
759
- return img_format, image_bytes
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"Unexpected error taking screenshot: {str(e)}") from e
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
- try:
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, serial: Optional[str] = None) -> Dict[str, Any]:
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
- if self.use_tcp and self.tcp_forwarded:
899
- # Use TCP communication
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
- state_data = self._parse_content_provider_output(adb_output)
930
-
931
- if state_data is None:
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 TCP connection using the /ping endpoint.
707
+ Test the Portal connection.
1011
708
 
1012
709
  Returns:
1013
710
  Dictionary with ping result
1014
711
  """
1015
- try:
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]: