droidrun 0.3.1__py3-none-any.whl → 0.3.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: droidrun
3
- Version: 0.3.1
3
+ Version: 0.3.3
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
@@ -25,9 +25,11 @@ Classifier: Topic :: Software Development :: Testing
25
25
  Classifier: Topic :: Software Development :: Testing :: Acceptance
26
26
  Classifier: Topic :: System :: Emulators
27
27
  Classifier: Topic :: Utilities
28
- Requires-Python: >=3.10
28
+ Requires-Python: >=3.11
29
+ Requires-Dist: adbutils==2.10.0
29
30
  Requires-Dist: aiofiles>=23.0.0
30
31
  Requires-Dist: anthropic>=0.7.0
32
+ Requires-Dist: apkutils==2.0.0
31
33
  Requires-Dist: arize-phoenix
32
34
  Requires-Dist: click>=8.1.0
33
35
  Requires-Dist: llama-index
@@ -40,9 +42,11 @@ Requires-Dist: llama-index-llms-openai
40
42
  Requires-Dist: llama-index-llms-openai-like
41
43
  Requires-Dist: openai>=1.0.0
42
44
  Requires-Dist: pillow>=10.0.0
45
+ Requires-Dist: posthog==6.0.2
43
46
  Requires-Dist: pydantic>=2.0.0
44
47
  Requires-Dist: python-dotenv>=1.0.0
45
48
  Requires-Dist: rich>=13.0.0
49
+ Requires-Dist: typing-extensions
46
50
  Provides-Extra: dev
47
51
  Requires-Dist: bandit>=1.7.0; extra == 'dev'
48
52
  Requires-Dist: black>=23.0.0; extra == 'dev'
@@ -63,10 +67,18 @@ Description-Content-Type: text/markdown
63
67
  [![Benchmark](https://img.shields.io/badge/Benchmark-🏅-teal)](https://droidrun.ai/benchmark)
64
68
  [![Twitter Follow](https://img.shields.io/twitter/follow/droid_run?style=social)](https://x.com/droid_run)
65
69
 
70
+ <picture>
71
+ <source media="(prefers-color-scheme: dark)" srcset="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=983810&theme=dark&period=daily&t=1753948032207">
72
+ <source media="(prefers-color-scheme: light)" srcset="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=983810&theme=neutral&period=daily&t=1753948125523">
73
+ <a href="https://www.producthunt.com/products/droidrun-framework-for-mobile-agent?embed=true&utm_source=badge-top-post-badge&utm_medium=badge&utm_source=badge-droidrun" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=983810&theme=neutral&period=daily&t=1753948125523" alt="Droidrun - Give&#0032;AI&#0032;native&#0032;control&#0032;of&#0032;physical&#0032;&#0038;&#0032;virtual&#0032;phones&#0046; | Product Hunt" style="width: 200px; height: 54px;" width="200" height="54" /></a>
74
+ </picture>
75
+
66
76
 
67
77
 
68
78
  DroidRun is a powerful framework for controlling Android and iOS devices through LLM agents. It allows you to automate device interactions using natural language commands. [Checkout our benchmark results](https://droidrun.ai/benchmark)
69
79
 
80
+ ## Why Droidrun?
81
+
70
82
  - 🤖 Control Android and iOS devices with natural language commands
71
83
  - 🔀 Supports multiple LLM providers (OpenAI, Anthropic, Gemini, Ollama, DeepSeek)
72
84
  - 🧠 Planning capabilities for complex multi-step tasks
@@ -82,22 +94,28 @@ pip install droidrun
82
94
  ```
83
95
 
84
96
  ## 🚀 Quickstart
85
- Read on how to get droidrun up and running within seconds in [our docs](https://docs.droidrun.ai/v3/quickstart)!
97
+ Read on how to get droidrun up and running within seconds in [our docs](https://docs.droidrun.ai/v3/quickstart)!
86
98
 
99
+ [![Quickstart Video](https://img.youtube.com/vi/4WT7FXJah2I/0.jpg)](https://www.youtube.com/watch?v=4WT7FXJah2I)
87
100
 
88
101
  ## 🎬 Demo Videos
89
102
 
90
- 1. **Shopping Assistant**: Watch how DroidRun searches Amazon for headphones and sends the top 3 products to a colleague on WhatsApp.
91
-
92
- Prompt: "Go to Amazon, search for headphones and write the top 3 products to my colleague on WhatsApp."
93
-
94
- [![Shopping Assistant Demo](https://img.youtube.com/vi/VQK3JcifgwU/0.jpg)](https://www.youtube.com/watch?v=VQK3JcifgwU)
103
+ 1. **Accommodation booking**: Let Droidrun search for an apartment for you
104
+
105
+ [![Droidrun Accommodation Booking Demo](https://img.youtube.com/vi/VUpCyq1PSXw/0.jpg)](https://youtu.be/VUpCyq1PSXw)
106
+
107
+ <br>
108
+
109
+ 2. **Trend Hunter**: Let Droidrun hunt down trending posts
110
+
111
+ [![Droidrun Trend Hunter Demo](https://img.youtube.com/vi/7V8S2f8PnkQ/0.jpg)](https://youtu.be/7V8S2f8PnkQ)
112
+
113
+ <br>
114
+
115
+ 3. **Streak Saver**: Let Droidrun save your streak on your favorite language learning app
116
+
117
+ [![Droidrun Streak Saver Demo](https://img.youtube.com/vi/B5q2B467HKw/0.jpg)](https://youtu.be/B5q2B467HKw)
95
118
 
96
- 2. **Social Media Automation**: See DroidRun open X (Twitter) and post "Hello World".
97
-
98
- Prompt: "Open up X and post Hello World."
99
-
100
- [![Social Media Automation Demo](https://img.youtube.com/vi/i4-sDQhzt_M/0.jpg)](https://www.youtube.com/watch?v=i4-sDQhzt_M)
101
119
 
102
120
  ## 💡 Example Use Cases
103
121
 
@@ -107,22 +125,6 @@ Read on how to get droidrun up and running within seconds in [our docs](https://
107
125
  - Remote assistance for less technical users
108
126
  - Exploring mobile UI with natural language commands
109
127
 
110
- ## 🗺️ Roadmap
111
-
112
- ### 🤖 Agent:
113
- - **Improve memory**: Enhance context retention for complex multi-step tasks
114
- - **Expand planning capabilities**: Add support for more complex reasoning strategies
115
- - **Add Integrations**: Support more LLM providers and agent frameworks (LangChain, Agno etc.)
116
-
117
- ### ⚙️ Automations:
118
- - **Create Automation Scripts**: Generate reusable scripts from agent actions that can be scheduled or shared
119
-
120
- ### ☁️ Cloud:
121
- - **Hosted version**: Remote device control via web interface without local setup
122
- - **Add-Ons**: Marketplace for extensions serving specific use cases
123
- - **Proxy Hours**: Cloud compute time with tiered pricing for running automations
124
- - **Droidrun AppStore**: Simple installation of Apps on your hosted devices
125
-
126
128
  ## 👥 Contributing
127
129
 
128
130
  Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,54 @@
1
+ droidrun/__init__.py,sha256=Cqt4NXZ-753220dlRZXglMkFT2ygcXSErMmqupGsi9A,622
2
+ droidrun/__main__.py,sha256=78o1Wr_Z-NrZy9yLWmEfNfRRhAiJGBr4Xi3lmbgkx3w,105
3
+ droidrun/portal.py,sha256=C0gZK9GHigsM6Vq2fQP_DKaDXqtnQSA2NUrxClVe6hw,3931
4
+ droidrun/agent/__init__.py,sha256=4SqTJeGDvr_wT8rtN9J8hnN6P-pae663mkYr-JmzH4w,208
5
+ droidrun/agent/codeact/__init__.py,sha256=ZLDGT_lTTzyNm7pcBzdyRIGHJ2ZgbInJdhXZRbJLhSQ,278
6
+ droidrun/agent/codeact/codeact_agent.py,sha256=XYyRrDAj2MoNb4WTtCzY8bqlI1WDy9TUIRku4NMUezk,16087
7
+ droidrun/agent/codeact/events.py,sha256=skCfZ-5SR0YhhzZVxx8_VkUjfILk8rCv47k9pHNYhdc,634
8
+ droidrun/agent/codeact/prompts.py,sha256=28HflWMNkC1ky0hGCzAxhJftjU2IIU1ZRUfya3S7M6I,1006
9
+ droidrun/agent/common/default.py,sha256=P07el-PrHsoqQMYsYxSSln6mFl-QY75vzwp1Dll_XmY,259
10
+ droidrun/agent/common/events.py,sha256=CUYdglSchK7W39VTbDdw24WJCwF4HfU0jKMbnjnFBdc,1085
11
+ droidrun/agent/context/__init__.py,sha256=upSJilVQUwQYWJuGr_8QrmJaReV3SNAc_Z61wQZzbK4,697
12
+ droidrun/agent/context/agent_persona.py,sha256=Mxd4HTyirWD-aqNlka1hQBdS-0I-lXJr2AjPMwDMUo8,390
13
+ droidrun/agent/context/context_injection_manager.py,sha256=sA33q2KPtX_4Yap8wM11T6ewlZC_0FIbKPEc400SHrE,2188
14
+ droidrun/agent/context/episodic_memory.py,sha256=1ImeR3jAWOpKwkQt3bMlXVOBiQbIli5fBIlBq2waREQ,394
15
+ droidrun/agent/context/reflection.py,sha256=0hJluOz0hTlHHhReKpIJ9HU5aJbaJsvrjMfraQ84D-M,652
16
+ droidrun/agent/context/task_manager.py,sha256=ESLs4kR6VNYiYQsc4V7WAeoSLwbaZPSWBXpveOfOv8c,4343
17
+ droidrun/agent/context/personas/__init__.py,sha256=oSRa8g_xngX7JPIRPu7fLO33m3r7fdEQzIuORuqcw5M,232
18
+ droidrun/agent/context/personas/app_starter.py,sha256=dHeknznxGEPJ7S6VPyEG_MB-HvAvQwUOnRWaShaV8Xo,1585
19
+ droidrun/agent/context/personas/big_agent.py,sha256=Gl_y4ykz3apGc203-KG2UbSOwf7gDUiWh7GOVyiLn-Y,5091
20
+ droidrun/agent/context/personas/default.py,sha256=Xm07YCWoKjvlHAbQRtzE3vn7BVcz6wYcSVeg4FiojJQ,5060
21
+ droidrun/agent/context/personas/ui_expert.py,sha256=j0OKfN1jQSrREHcVeomMTDPCWLsZVX4aeuWN4Y-x3z0,4739
22
+ droidrun/agent/droid/__init__.py,sha256=3BfUVZiUQ8ATAJ_JmqQZQx53WoERRpQ4AyHW5WOgbRI,297
23
+ droidrun/agent/droid/droid_agent.py,sha256=_XtGkkQM27qfxDO8V2MwehWz931Z10CBhtOIoGNsWBk,15252
24
+ droidrun/agent/droid/events.py,sha256=Ks2D6lX5P1rpZ4nIAPXSC83z4AT5OzKt3isP5yk25F4,689
25
+ droidrun/agent/oneflows/reflector.py,sha256=I_tE0PBjvwWbS6SA8Qd41etxJglFgn8oScuKUxc9LEE,11621
26
+ droidrun/agent/planner/__init__.py,sha256=Fu0Ewtd-dIRLgHIL1DB_9EEKvQS_f1vjB8jgO5TbJXg,364
27
+ droidrun/agent/planner/events.py,sha256=oyt2FNrA2uVyUeVT65-N0AC6sWBFxSnwNEqWtnRYoFM,390
28
+ droidrun/agent/planner/planner_agent.py,sha256=fxLeLeN_2kmaWHXLaDLvXOR_uvP_ult_I8SPZbgL_AU,10482
29
+ droidrun/agent/planner/prompts.py,sha256=Ci7Oeu3J4TAhx-tKGPZ9l6Wb3a81FSqC8cWW4jW73HI,6046
30
+ droidrun/agent/utils/__init__.py,sha256=JK6ygRjw7gzcQSG0HBEYLoVGH54QQAxJJ7HpIS5mgyc,44
31
+ droidrun/agent/utils/async_utils.py,sha256=IQBcWPwevm89B7R_UdMXk0unWeNCBA232b5kQGqoxNI,336
32
+ droidrun/agent/utils/chat_utils.py,sha256=5oqP2nmKs8sHWP1H_TK82yaxrxWf7FdEbFKASKpR60g,13000
33
+ droidrun/agent/utils/executer.py,sha256=UKVd7asr3RvO-YgRflGdG12FFZxDvU6hFxrdV1sWLcU,4892
34
+ droidrun/agent/utils/llm_picker.py,sha256=16tNkNhEM9gD_uivzxLvuaa6s0Tz7Igu-3fxMP2lAtY,5968
35
+ droidrun/agent/utils/trajectory.py,sha256=PU3nI3Zru580_bK0TvqUaf-5kiWvj6hFoedXkDgTqdc,17047
36
+ droidrun/cli/__init__.py,sha256=DuwSRtZ8WILPd-nf-fZ7BaBsRgtofoInOF3JtJ9wag0,167
37
+ droidrun/cli/logs.py,sha256=PsT_VbnOa_sOLXK4KkEJk4AsYCpscqrVoryMmLVwPG0,9714
38
+ droidrun/cli/main.py,sha256=UnTocHNd8za9Fj-ppnvJSD8DT3RQ1aqVPPTvAsWRX5g,17734
39
+ droidrun/macro/__init__.py,sha256=333sMt19mA8sfQa4qPKbbCmr8Ej3nvpdxGXUZtVTEqM,344
40
+ droidrun/macro/__main__.py,sha256=-zj42Bj7309oLPZbNsxZeNwIDaEe7Th1I4zF8yAHasw,193
41
+ droidrun/macro/cli.py,sha256=fecato896z9OxZsAzCX7FWgF2TXwM5EnR85EcSGzc4U,9057
42
+ droidrun/macro/replay.py,sha256=q_3ZcHVjvsdDfS2xyt_vuuwXGt9_1t38JD1cPsjzIfU,10764
43
+ droidrun/telemetry/__init__.py,sha256=D4Mp02iGJH2Tjpv42Bzyo6_WC3NWj9Qy9hQPWFaCkhA,234
44
+ droidrun/telemetry/events.py,sha256=S6r6_c2bGTZt6F88m_vREDD_MDhw3Pz4I53lLmsI764,518
45
+ droidrun/telemetry/tracker.py,sha256=Ljue6zivX8KnadXI9DivrayuWxAnUwbJKvCvNtY1Y4Y,2717
46
+ droidrun/tools/__init__.py,sha256=9ReauavtSKDQG9ya9_Fr9O0TQnDFixgOPaP5n82_iEk,271
47
+ droidrun/tools/adb.py,sha256=XRa9TnuS_0-vqMJvn6RDjCRzu-MvSbdtBVAuhVHdSzo,42083
48
+ droidrun/tools/ios.py,sha256=imzojiS6gqz4IKexUEz1ga7-flSOaC5QRpHIJTwcgSQ,21807
49
+ droidrun/tools/tools.py,sha256=eXCaFjb_FNJFPrIfKi9W1WghxfrfnsbfutW3f0XlTzw,3720
50
+ droidrun-0.3.3.dist-info/METADATA,sha256=9aqH7cPVJ24Snsta0Fkyv8tG9b7dMLZ1H3ZhjcvEScM,6718
51
+ droidrun-0.3.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
52
+ droidrun-0.3.3.dist-info/entry_points.txt,sha256=o259U66js8TIybQ7zs814Oe_LQ_GpZsp6a9Cr-xm5zE,51
53
+ droidrun-0.3.3.dist-info/licenses/LICENSE,sha256=s-uxn9qChu-kFdRXUp6v_0HhsaJ_5OANmfNOFVm2zdk,1069
54
+ droidrun-0.3.3.dist-info/RECORD,,
droidrun/adb/__init__.py DELETED
@@ -1,13 +0,0 @@
1
- """
2
- ADB Package - Android Debug Bridge functionality.
3
- """
4
-
5
- from droidrun.adb.device import Device
6
- from droidrun.adb.manager import DeviceManager
7
- from droidrun.adb.wrapper import ADBWrapper
8
-
9
- __all__ = [
10
- 'Device',
11
- 'DeviceManager',
12
- 'ADBWrapper',
13
- ]
droidrun/adb/device.py DELETED
@@ -1,315 +0,0 @@
1
- """
2
- Device - High-level representation of an Android device.
3
- """
4
-
5
- import os
6
- import tempfile
7
- import time
8
- import random
9
- import string
10
- from typing import Dict, Optional, Tuple, List
11
- from droidrun.adb.wrapper import ADBWrapper
12
-
13
- class Device:
14
- """High-level representation of an Android device."""
15
-
16
- def __init__(self, serial: str, adb: ADBWrapper):
17
- """Initialize device.
18
-
19
- Args:
20
- serial: Device serial number
21
- adb: ADB wrapper instance
22
- """
23
- self._serial = serial
24
- self._adb = adb
25
- self._properties_cache: Dict[str, str] = {}
26
-
27
- @property
28
- def serial(self) -> str:
29
- """Get device serial number."""
30
- return self._serial
31
-
32
- async def get_properties(self) -> Dict[str, str]:
33
- """Get all device properties."""
34
- if not self._properties_cache:
35
- self._properties_cache = await self._adb.get_properties(self._serial)
36
- return self._properties_cache
37
-
38
- async def get_property(self, name: str) -> str:
39
- """Get a specific device property."""
40
- props = await self.get_properties()
41
- return props.get(name, "")
42
-
43
- @property
44
- async def model(self) -> str:
45
- """Get device model."""
46
- return await self.get_property("ro.product.model")
47
-
48
- @property
49
- async def brand(self) -> str:
50
- """Get device brand."""
51
- return await self.get_property("ro.product.brand")
52
-
53
- @property
54
- async def android_version(self) -> str:
55
- """Get Android version."""
56
- return await self.get_property("ro.build.version.release")
57
-
58
- @property
59
- async def sdk_level(self) -> str:
60
- """Get SDK level."""
61
- return await self.get_property("ro.build.version.sdk")
62
-
63
- async def tap(self, x: int, y: int) -> None:
64
- """Tap at coordinates.
65
-
66
- Args:
67
- x: X coordinate
68
- y: Y coordinate
69
- """
70
- await self._adb.shell(self._serial, f"input tap {x} {y}")
71
-
72
- async def swipe(
73
- self,
74
- start_x: int,
75
- start_y: int,
76
- end_x: int,
77
- end_y: int,
78
- duration_ms: int = 300
79
- ) -> None:
80
- """Perform swipe gesture.
81
-
82
- Args:
83
- start_x: Starting X coordinate
84
- start_y: Starting Y coordinate
85
- end_x: Ending X coordinate
86
- end_y: Ending Y coordinate
87
- duration_ms: Swipe duration in milliseconds
88
- """
89
- await self._adb.shell(
90
- self._serial,
91
- f"input swipe {start_x} {start_y} {end_x} {end_y} {duration_ms}"
92
- )
93
-
94
- async def input_text(self, text: str) -> None:
95
- """Input text.
96
-
97
- Args:
98
- text: Text to input
99
- """
100
- await self._adb.shell(self._serial, f"input text {text}")
101
-
102
- async def press_key(self, keycode: int) -> None:
103
- """Press a key.
104
-
105
- Args:
106
- keycode: Android keycode to press
107
- """
108
- await self._adb.shell(self._serial, f"input keyevent {keycode}")
109
-
110
- async def start_activity(
111
- self,
112
- package: str,
113
- activity: str = ".MainActivity",
114
- extras: Optional[Dict[str, str]] = None
115
- ) -> None:
116
- """Start an app activity.
117
-
118
- Args:
119
- package: Package name
120
- activity: Activity name
121
- extras: Intent extras
122
- """
123
- cmd = f"am start -n {package}/{activity}"
124
- if extras:
125
- for key, value in extras.items():
126
- cmd += f" -e {key} {value}"
127
- await self._adb.shell(self._serial, cmd)
128
-
129
- async def start_app(self, package: str, activity: str = "") -> str:
130
- """Start an app on the device.
131
-
132
- Args:
133
- package: Package name
134
- activity: Optional activity name (if empty, launches default activity)
135
-
136
- Returns:
137
- Result message
138
- """
139
- if activity:
140
- if not activity.startswith(".") and "." not in activity:
141
- activity = f".{activity}"
142
-
143
- if not activity.startswith(".") and "." in activity and not activity.startswith(package):
144
- # Fully qualified activity name
145
- component = activity.split("/", 1)
146
- return await self.start_activity(component[0], component[1] if len(component) > 1 else activity)
147
-
148
- # Relative activity name
149
- return await self.start_activity(package, activity)
150
-
151
- # Start main activity using monkey
152
- cmd = f"monkey -p {package} -c android.intent.category.LAUNCHER 1"
153
- result = await self._adb.shell(self._serial, cmd)
154
- return f"Started {package}"
155
-
156
- async def install_app(self, apk_path: str, reinstall: bool = False, grant_permissions: bool = True) -> str:
157
- """Install an APK on the device.
158
-
159
- Args:
160
- apk_path: Path to the APK file
161
- reinstall: Whether to reinstall if app exists
162
- grant_permissions: Whether to grant all requested permissions
163
-
164
- Returns:
165
- Installation result
166
- """
167
- if not os.path.exists(apk_path):
168
- return f"Error: APK file not found: {apk_path}"
169
-
170
- # Build install command args
171
- install_args = ["install"]
172
- if reinstall:
173
- install_args.append("-r")
174
- if grant_permissions:
175
- install_args.append("-g")
176
- install_args.append(apk_path)
177
-
178
- try:
179
- stdout, stderr = await self._adb._run_device_command(
180
- self._serial,
181
- install_args,
182
- timeout=120 # Longer timeout for installation
183
- )
184
-
185
- if "success" in stdout.lower():
186
- return f"Successfully installed {os.path.basename(apk_path)}"
187
- return f"Installation failed: {stdout or stderr}"
188
-
189
- except Exception as e:
190
- return f"Installation failed: {str(e)}"
191
-
192
- async def uninstall_app(self, package: str, keep_data: bool = False) -> str:
193
- """Uninstall an app from the device.
194
-
195
- Args:
196
- package: Package name to uninstall
197
- keep_data: Whether to keep app data and cache directories
198
-
199
- Returns:
200
- Uninstallation result
201
- """
202
- cmd = ["pm", "uninstall"]
203
- if keep_data:
204
- cmd.append("-k")
205
- cmd.append(package)
206
-
207
- result = await self._adb.shell(self._serial, " ".join(cmd))
208
- return result.strip()
209
-
210
- async def take_screenshot(self, quality: int = 75) -> Tuple[str, bytes]:
211
- """Take a screenshot of the device and compress it.
212
-
213
- Args:
214
- quality: JPEG quality (1-100, lower means smaller file size)
215
-
216
- Returns:
217
- Tuple of (local file path, screenshot data as bytes)
218
- """
219
- # Create a temporary file for the screenshot
220
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp:
221
- screenshot_path = temp.name
222
-
223
- try:
224
- # Generate a random filename for the device
225
- timestamp = int(time.time())
226
- random_suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=8))
227
- device_path = f"/sdcard/screenshot_{timestamp}_{random_suffix}.png"
228
-
229
- # Take screenshot using screencap command
230
- await self._adb.shell(self._serial, f"screencap -p {device_path}")
231
-
232
- # Pull screenshot to local machine
233
- await self._adb._run_device_command(
234
- self._serial,
235
- ["pull", device_path, screenshot_path]
236
- )
237
-
238
- # Clean up on device
239
- await self._adb.shell(self._serial, f"rm {device_path}")
240
-
241
- # Read the screenshot file
242
- with open(screenshot_path, "rb") as f:
243
- screenshot_data = f.read()
244
-
245
- # Convert and compress the image
246
- try:
247
- from PIL import Image
248
- import io
249
-
250
- # Create buffer for the compressed image
251
- buffer = io.BytesIO()
252
-
253
- # Load the PNG data into a PIL Image
254
- with Image.open(io.BytesIO(screenshot_data)) as img:
255
- # Convert to RGB (removing alpha channel if present) and save as JPEG
256
- converted_img = img.convert("RGB") if img.mode == "RGBA" else img
257
- converted_img.save(buffer, format="JPEG", quality=quality, optimize=True)
258
- compressed_data = buffer.getvalue()
259
-
260
- # Get size reduction info for logging
261
- png_size = len(screenshot_data) / 1024
262
- jpg_size = len(compressed_data) / 1024
263
- reduction = 100 - (jpg_size / png_size * 100) if png_size > 0 else 0
264
-
265
- import logging
266
- logger = logging.getLogger("droidrun")
267
- logger.debug(
268
- f"Screenshot compressed successfully: {png_size:.1f}KB → {jpg_size:.1f}KB ({reduction:.1f}% reduction)"
269
- )
270
-
271
- return screenshot_path, compressed_data
272
- except ImportError:
273
- # If PIL is not available, return the original PNG data
274
- logger.warning("PIL not available, returning uncompressed screenshot")
275
- return screenshot_path, screenshot_data
276
- except Exception as e:
277
- # If compression fails, return the original PNG data
278
- logger.warning(f"Screenshot compression failed: {e}, returning uncompressed")
279
- return screenshot_path, screenshot_data
280
-
281
- except Exception as e:
282
- # Clean up in case of error
283
- try:
284
- os.unlink(screenshot_path)
285
- except OSError:
286
- pass
287
- raise RuntimeError(f"Screenshot capture failed: {str(e)}")
288
-
289
- async def list_packages(self, include_system_apps: bool = False) -> List[Dict[str, str]]:
290
- """List installed packages on the device.
291
-
292
- Args:
293
- include_system_apps: Whether to include system apps
294
-
295
- Returns:
296
- List of package dictionaries with 'package' and 'path' keys
297
- """
298
- cmd = ["pm", "list", "packages", "-f"]
299
- if not include_system_apps:
300
- cmd.append("-3")
301
-
302
- output = await self._adb.shell(self._serial, " ".join(cmd))
303
-
304
- packages = []
305
- for line in output.splitlines():
306
- if line.startswith("package:"):
307
- parts = line[8:].split("=")
308
- if len(parts) == 2:
309
- path, package = parts
310
- packages.append({
311
- "package": package,
312
- "path": path
313
- })
314
-
315
- return packages
droidrun/adb/manager.py DELETED
@@ -1,93 +0,0 @@
1
- """
2
- Device Manager - Manages Android device connections.
3
- """
4
-
5
- from typing import Dict, List, Optional
6
- from droidrun.adb.wrapper import ADBWrapper
7
- from droidrun.adb.device import Device
8
-
9
- class DeviceManager:
10
- """Manages Android device connections."""
11
-
12
- def __init__(self, adb_path: Optional[str] = None):
13
- """Initialize device manager.
14
-
15
- Args:
16
- adb_path: Path to ADB binary
17
- """
18
- self._adb = ADBWrapper(adb_path)
19
- self._devices: Dict[str, Device] = {}
20
-
21
- async def list_devices(self) -> List[Device]:
22
- """List connected devices.
23
-
24
- Returns:
25
- List of connected devices
26
- """
27
- devices_info = await self._adb.get_devices()
28
-
29
- # Update device cache
30
- current_serials = set()
31
- for device_info in devices_info:
32
- serial = device_info["serial"]
33
- current_serials.add(serial)
34
-
35
- if serial not in self._devices:
36
- self._devices[serial] = Device(serial, self._adb)
37
-
38
- # Remove disconnected devices
39
- for serial in list(self._devices.keys()):
40
- if serial not in current_serials:
41
- del self._devices[serial]
42
-
43
- return list(self._devices.values())
44
-
45
- async def get_device(self, serial: str) -> Optional[Device]:
46
- """Get a specific device.
47
-
48
- Args:
49
- serial: Device serial number
50
-
51
- Returns:
52
- Device instance if found, None otherwise
53
- """
54
- if serial in self._devices:
55
- return self._devices[serial]
56
-
57
- # Try to find the device
58
- devices = await self.list_devices()
59
- for device in devices:
60
- if device.serial == serial:
61
- return device
62
-
63
- return None
64
-
65
- async def connect(self, host: str, port: int = 5555) -> Optional[Device]:
66
- """Connect to a device over TCP/IP.
67
-
68
- Args:
69
- host: Device IP address
70
- port: Device port
71
-
72
- Returns:
73
- Connected device instance
74
- """
75
- try:
76
- serial = await self._adb.connect(host, port)
77
- return await self.get_device(serial)
78
- except Exception:
79
- return None
80
-
81
- async def disconnect(self, serial: str) -> bool:
82
- """Disconnect from a device.
83
-
84
- Args:
85
- serial: Device serial number
86
-
87
- Returns:
88
- True if disconnected successfully
89
- """
90
- success = await self._adb.disconnect(serial)
91
- if success and serial in self._devices:
92
- del self._devices[serial]
93
- return success