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.
- droidrun/__init__.py +7 -12
- droidrun/agent/codeact/codeact_agent.py +9 -7
- droidrun/agent/common/events.py +44 -1
- droidrun/agent/context/personas/__init__.py +2 -2
- droidrun/agent/context/personas/big_agent.py +96 -0
- droidrun/agent/context/personas/ui_expert.py +1 -0
- droidrun/agent/droid/droid_agent.py +63 -11
- droidrun/agent/droid/events.py +4 -0
- droidrun/agent/planner/planner_agent.py +2 -2
- droidrun/agent/utils/executer.py +10 -2
- droidrun/agent/utils/llm_picker.py +1 -0
- droidrun/agent/utils/trajectory.py +258 -11
- droidrun/cli/main.py +179 -86
- droidrun/macro/__init__.py +14 -0
- droidrun/macro/__main__.py +10 -0
- droidrun/macro/cli.py +228 -0
- droidrun/macro/replay.py +309 -0
- droidrun/portal.py +138 -0
- droidrun/telemetry/__init__.py +4 -0
- droidrun/telemetry/events.py +27 -0
- droidrun/telemetry/tracker.py +84 -0
- droidrun/tools/adb.py +704 -372
- droidrun/tools/ios.py +169 -166
- droidrun/tools/tools.py +70 -17
- {droidrun-0.3.1.dist-info → droidrun-0.3.3.dist-info}/METADATA +31 -29
- droidrun-0.3.3.dist-info/RECORD +54 -0
- droidrun/adb/__init__.py +0 -13
- droidrun/adb/device.py +0 -315
- droidrun/adb/manager.py +0 -93
- droidrun/adb/wrapper.py +0 -226
- droidrun/agent/context/personas/extractor.py +0 -52
- droidrun-0.3.1.dist-info/RECORD +0 -50
- {droidrun-0.3.1.dist-info → droidrun-0.3.3.dist-info}/WHEEL +0 -0
- {droidrun-0.3.1.dist-info → droidrun-0.3.3.dist-info}/entry_points.txt +0 -0
- {droidrun-0.3.1.dist-info → droidrun-0.3.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: droidrun
|
3
|
-
Version: 0.3.
|
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.
|
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
|
[](https://droidrun.ai/benchmark)
|
64
68
|
[](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 AI native control of physical & virtual phones. | 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
|
+
[](https://www.youtube.com/watch?v=4WT7FXJah2I)
|
87
100
|
|
88
101
|
## 🎬 Demo Videos
|
89
102
|
|
90
|
-
1. **
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
103
|
+
1. **Accommodation booking**: Let Droidrun search for an apartment for you
|
104
|
+
|
105
|
+
[](https://youtu.be/VUpCyq1PSXw)
|
106
|
+
|
107
|
+
<br>
|
108
|
+
|
109
|
+
2. **Trend Hunter**: Let Droidrun hunt down trending posts
|
110
|
+
|
111
|
+
[](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
|
+
[](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
|
-
[](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
|