droidrun 0.3.2__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 +6 -2
- droidrun/agent/codeact/codeact_agent.py +7 -6
- droidrun/agent/common/events.py +44 -1
- droidrun/agent/context/personas/__init__.py +2 -0
- droidrun/agent/context/personas/big_agent.py +96 -0
- droidrun/agent/context/personas/ui_expert.py +1 -0
- droidrun/agent/droid/droid_agent.py +15 -6
- droidrun/agent/planner/planner_agent.py +2 -2
- droidrun/agent/utils/executer.py +10 -2
- droidrun/agent/utils/trajectory.py +258 -11
- droidrun/cli/main.py +73 -36
- 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 +16 -17
- droidrun/telemetry/tracker.py +3 -2
- droidrun/tools/adb.py +668 -200
- droidrun/tools/ios.py +163 -163
- droidrun/tools/tools.py +32 -14
- {droidrun-0.3.2.dist-info → droidrun-0.3.3.dist-info}/METADATA +21 -8
- {droidrun-0.3.2.dist-info → droidrun-0.3.3.dist-info}/RECORD +25 -24
- droidrun/adb/__init__.py +0 -13
- droidrun/adb/device.py +0 -345
- droidrun/adb/manager.py +0 -93
- droidrun/adb/wrapper.py +0 -226
- {droidrun-0.3.2.dist-info → droidrun-0.3.3.dist-info}/WHEEL +0 -0
- {droidrun-0.3.2.dist-info → droidrun-0.3.3.dist-info}/entry_points.txt +0 -0
- {droidrun-0.3.2.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
|
@@ -44,6 +46,7 @@ Requires-Dist: posthog==6.0.2
|
|
44
46
|
Requires-Dist: pydantic>=2.0.0
|
45
47
|
Requires-Dist: python-dotenv>=1.0.0
|
46
48
|
Requires-Dist: rich>=13.0.0
|
49
|
+
Requires-Dist: typing-extensions
|
47
50
|
Provides-Extra: dev
|
48
51
|
Requires-Dist: bandit>=1.7.0; extra == 'dev'
|
49
52
|
Requires-Dist: black>=23.0.0; extra == 'dev'
|
@@ -64,6 +67,12 @@ Description-Content-Type: text/markdown
|
|
64
67
|
[](https://droidrun.ai/benchmark)
|
65
68
|
[](https://x.com/droid_run)
|
66
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
|
+
|
67
76
|
|
68
77
|
|
69
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)
|
@@ -91,17 +100,21 @@ Read on how to get droidrun up and running within seconds in [our docs](https://
|
|
91
100
|
|
92
101
|
## 🎬 Demo Videos
|
93
102
|
|
94
|
-
1. **
|
103
|
+
1. **Accommodation booking**: Let Droidrun search for an apartment for you
|
104
|
+
|
105
|
+
[](https://youtu.be/VUpCyq1PSXw)
|
106
|
+
|
107
|
+
<br>
|
95
108
|
|
96
|
-
|
109
|
+
2. **Trend Hunter**: Let Droidrun hunt down trending posts
|
97
110
|
|
98
|
-
|
111
|
+
[](https://youtu.be/7V8S2f8PnkQ)
|
99
112
|
|
100
|
-
|
113
|
+
<br>
|
101
114
|
|
102
|
-
3. **
|
115
|
+
3. **Streak Saver**: Let Droidrun save your streak on your favorite language learning app
|
103
116
|
|
104
|
-
|
117
|
+
[](https://youtu.be/B5q2B467HKw)
|
105
118
|
|
106
119
|
|
107
120
|
## 💡 Example Use Cases
|
@@ -1,53 +1,54 @@
|
|
1
|
-
droidrun/__init__.py,sha256=
|
1
|
+
droidrun/__init__.py,sha256=Cqt4NXZ-753220dlRZXglMkFT2ygcXSErMmqupGsi9A,622
|
2
2
|
droidrun/__main__.py,sha256=78o1Wr_Z-NrZy9yLWmEfNfRRhAiJGBr4Xi3lmbgkx3w,105
|
3
|
-
droidrun/portal.py,sha256=
|
4
|
-
droidrun/adb/__init__.py,sha256=kh-iT9Sv6RZ2dFSDu1beK_GgtAq8wlMvZQ7puR8JWsI,257
|
5
|
-
droidrun/adb/device.py,sha256=8Qc4pU2-dy8v7skBKsn74Lnke1d7x339cQCrba28vlU,11694
|
6
|
-
droidrun/adb/manager.py,sha256=mGdQSlHmZ00t-4jmg_I_qRrBigWnWr_gQXB1TcFXqoQ,2767
|
7
|
-
droidrun/adb/wrapper.py,sha256=Yz3_JSIidq-5bf-t0UfawTutMaLrINS_1Y15m_Uss4g,7093
|
3
|
+
droidrun/portal.py,sha256=C0gZK9GHigsM6Vq2fQP_DKaDXqtnQSA2NUrxClVe6hw,3931
|
8
4
|
droidrun/agent/__init__.py,sha256=4SqTJeGDvr_wT8rtN9J8hnN6P-pae663mkYr-JmzH4w,208
|
9
5
|
droidrun/agent/codeact/__init__.py,sha256=ZLDGT_lTTzyNm7pcBzdyRIGHJ2ZgbInJdhXZRbJLhSQ,278
|
10
|
-
droidrun/agent/codeact/codeact_agent.py,sha256=
|
6
|
+
droidrun/agent/codeact/codeact_agent.py,sha256=XYyRrDAj2MoNb4WTtCzY8bqlI1WDy9TUIRku4NMUezk,16087
|
11
7
|
droidrun/agent/codeact/events.py,sha256=skCfZ-5SR0YhhzZVxx8_VkUjfILk8rCv47k9pHNYhdc,634
|
12
8
|
droidrun/agent/codeact/prompts.py,sha256=28HflWMNkC1ky0hGCzAxhJftjU2IIU1ZRUfya3S7M6I,1006
|
13
9
|
droidrun/agent/common/default.py,sha256=P07el-PrHsoqQMYsYxSSln6mFl-QY75vzwp1Dll_XmY,259
|
14
|
-
droidrun/agent/common/events.py,sha256=
|
10
|
+
droidrun/agent/common/events.py,sha256=CUYdglSchK7W39VTbDdw24WJCwF4HfU0jKMbnjnFBdc,1085
|
15
11
|
droidrun/agent/context/__init__.py,sha256=upSJilVQUwQYWJuGr_8QrmJaReV3SNAc_Z61wQZzbK4,697
|
16
12
|
droidrun/agent/context/agent_persona.py,sha256=Mxd4HTyirWD-aqNlka1hQBdS-0I-lXJr2AjPMwDMUo8,390
|
17
13
|
droidrun/agent/context/context_injection_manager.py,sha256=sA33q2KPtX_4Yap8wM11T6ewlZC_0FIbKPEc400SHrE,2188
|
18
14
|
droidrun/agent/context/episodic_memory.py,sha256=1ImeR3jAWOpKwkQt3bMlXVOBiQbIli5fBIlBq2waREQ,394
|
19
15
|
droidrun/agent/context/reflection.py,sha256=0hJluOz0hTlHHhReKpIJ9HU5aJbaJsvrjMfraQ84D-M,652
|
20
16
|
droidrun/agent/context/task_manager.py,sha256=ESLs4kR6VNYiYQsc4V7WAeoSLwbaZPSWBXpveOfOv8c,4343
|
21
|
-
droidrun/agent/context/personas/__init__.py,sha256=
|
17
|
+
droidrun/agent/context/personas/__init__.py,sha256=oSRa8g_xngX7JPIRPu7fLO33m3r7fdEQzIuORuqcw5M,232
|
22
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
|
23
20
|
droidrun/agent/context/personas/default.py,sha256=Xm07YCWoKjvlHAbQRtzE3vn7BVcz6wYcSVeg4FiojJQ,5060
|
24
|
-
droidrun/agent/context/personas/ui_expert.py,sha256=
|
21
|
+
droidrun/agent/context/personas/ui_expert.py,sha256=j0OKfN1jQSrREHcVeomMTDPCWLsZVX4aeuWN4Y-x3z0,4739
|
25
22
|
droidrun/agent/droid/__init__.py,sha256=3BfUVZiUQ8ATAJ_JmqQZQx53WoERRpQ4AyHW5WOgbRI,297
|
26
|
-
droidrun/agent/droid/droid_agent.py,sha256=
|
23
|
+
droidrun/agent/droid/droid_agent.py,sha256=_XtGkkQM27qfxDO8V2MwehWz931Z10CBhtOIoGNsWBk,15252
|
27
24
|
droidrun/agent/droid/events.py,sha256=Ks2D6lX5P1rpZ4nIAPXSC83z4AT5OzKt3isP5yk25F4,689
|
28
25
|
droidrun/agent/oneflows/reflector.py,sha256=I_tE0PBjvwWbS6SA8Qd41etxJglFgn8oScuKUxc9LEE,11621
|
29
26
|
droidrun/agent/planner/__init__.py,sha256=Fu0Ewtd-dIRLgHIL1DB_9EEKvQS_f1vjB8jgO5TbJXg,364
|
30
27
|
droidrun/agent/planner/events.py,sha256=oyt2FNrA2uVyUeVT65-N0AC6sWBFxSnwNEqWtnRYoFM,390
|
31
|
-
droidrun/agent/planner/planner_agent.py,sha256=
|
28
|
+
droidrun/agent/planner/planner_agent.py,sha256=fxLeLeN_2kmaWHXLaDLvXOR_uvP_ult_I8SPZbgL_AU,10482
|
32
29
|
droidrun/agent/planner/prompts.py,sha256=Ci7Oeu3J4TAhx-tKGPZ9l6Wb3a81FSqC8cWW4jW73HI,6046
|
33
30
|
droidrun/agent/utils/__init__.py,sha256=JK6ygRjw7gzcQSG0HBEYLoVGH54QQAxJJ7HpIS5mgyc,44
|
34
31
|
droidrun/agent/utils/async_utils.py,sha256=IQBcWPwevm89B7R_UdMXk0unWeNCBA232b5kQGqoxNI,336
|
35
32
|
droidrun/agent/utils/chat_utils.py,sha256=5oqP2nmKs8sHWP1H_TK82yaxrxWf7FdEbFKASKpR60g,13000
|
36
|
-
droidrun/agent/utils/executer.py,sha256=
|
33
|
+
droidrun/agent/utils/executer.py,sha256=UKVd7asr3RvO-YgRflGdG12FFZxDvU6hFxrdV1sWLcU,4892
|
37
34
|
droidrun/agent/utils/llm_picker.py,sha256=16tNkNhEM9gD_uivzxLvuaa6s0Tz7Igu-3fxMP2lAtY,5968
|
38
|
-
droidrun/agent/utils/trajectory.py,sha256=
|
35
|
+
droidrun/agent/utils/trajectory.py,sha256=PU3nI3Zru580_bK0TvqUaf-5kiWvj6hFoedXkDgTqdc,17047
|
39
36
|
droidrun/cli/__init__.py,sha256=DuwSRtZ8WILPd-nf-fZ7BaBsRgtofoInOF3JtJ9wag0,167
|
40
37
|
droidrun/cli/logs.py,sha256=PsT_VbnOa_sOLXK4KkEJk4AsYCpscqrVoryMmLVwPG0,9714
|
41
|
-
droidrun/cli/main.py,sha256=
|
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
|
42
43
|
droidrun/telemetry/__init__.py,sha256=D4Mp02iGJH2Tjpv42Bzyo6_WC3NWj9Qy9hQPWFaCkhA,234
|
43
44
|
droidrun/telemetry/events.py,sha256=S6r6_c2bGTZt6F88m_vREDD_MDhw3Pz4I53lLmsI764,518
|
44
|
-
droidrun/telemetry/tracker.py,sha256=
|
45
|
+
droidrun/telemetry/tracker.py,sha256=Ljue6zivX8KnadXI9DivrayuWxAnUwbJKvCvNtY1Y4Y,2717
|
45
46
|
droidrun/tools/__init__.py,sha256=9ReauavtSKDQG9ya9_Fr9O0TQnDFixgOPaP5n82_iEk,271
|
46
|
-
droidrun/tools/adb.py,sha256=
|
47
|
-
droidrun/tools/ios.py,sha256=
|
48
|
-
droidrun/tools/tools.py,sha256=
|
49
|
-
droidrun-0.3.
|
50
|
-
droidrun-0.3.
|
51
|
-
droidrun-0.3.
|
52
|
-
droidrun-0.3.
|
53
|
-
droidrun-0.3.
|
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,345 +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
|
-
|
14
|
-
class Device:
|
15
|
-
"""High-level representation of an Android device."""
|
16
|
-
|
17
|
-
def __init__(self, serial: str, adb: ADBWrapper):
|
18
|
-
"""Initialize device.
|
19
|
-
|
20
|
-
Args:
|
21
|
-
serial: Device serial number
|
22
|
-
adb: ADB wrapper instance
|
23
|
-
"""
|
24
|
-
self._serial = serial
|
25
|
-
self._adb = adb
|
26
|
-
self._properties_cache: Dict[str, str] = {}
|
27
|
-
|
28
|
-
@property
|
29
|
-
def serial(self) -> str:
|
30
|
-
"""Get device serial number."""
|
31
|
-
return self._serial
|
32
|
-
|
33
|
-
async def get_properties(self) -> Dict[str, str]:
|
34
|
-
"""Get all device properties."""
|
35
|
-
if not self._properties_cache:
|
36
|
-
self._properties_cache = await self._adb.get_properties(self._serial)
|
37
|
-
return self._properties_cache
|
38
|
-
|
39
|
-
async def get_property(self, name: str) -> str:
|
40
|
-
"""Get a specific device property."""
|
41
|
-
props = await self.get_properties()
|
42
|
-
return props.get(name, "")
|
43
|
-
|
44
|
-
@property
|
45
|
-
async def model(self) -> str:
|
46
|
-
"""Get device model."""
|
47
|
-
return await self.get_property("ro.product.model")
|
48
|
-
|
49
|
-
@property
|
50
|
-
async def brand(self) -> str:
|
51
|
-
"""Get device brand."""
|
52
|
-
return await self.get_property("ro.product.brand")
|
53
|
-
|
54
|
-
@property
|
55
|
-
async def android_version(self) -> str:
|
56
|
-
"""Get Android version."""
|
57
|
-
return await self.get_property("ro.build.version.release")
|
58
|
-
|
59
|
-
@property
|
60
|
-
async def sdk_level(self) -> str:
|
61
|
-
"""Get SDK level."""
|
62
|
-
return await self.get_property("ro.build.version.sdk")
|
63
|
-
|
64
|
-
async def shell(self, command: str, timeout: float | None = None) -> str:
|
65
|
-
"""Execute a shell command on the device."""
|
66
|
-
return await self._adb.shell(self._serial, command, timeout)
|
67
|
-
|
68
|
-
async def tap(self, x: int, y: int) -> None:
|
69
|
-
"""Tap at coordinates.
|
70
|
-
|
71
|
-
Args:
|
72
|
-
x: X coordinate
|
73
|
-
y: Y coordinate
|
74
|
-
"""
|
75
|
-
await self._adb.shell(self._serial, f"input tap {x} {y}")
|
76
|
-
|
77
|
-
async def swipe(
|
78
|
-
self, start_x: int, start_y: int, end_x: int, end_y: int, 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 (
|
144
|
-
not activity.startswith(".")
|
145
|
-
and "." in activity
|
146
|
-
and not activity.startswith(package)
|
147
|
-
):
|
148
|
-
# Fully qualified activity name
|
149
|
-
component = activity.split("/", 1)
|
150
|
-
return await self.start_activity(
|
151
|
-
component[0], component[1] if len(component) > 1 else activity
|
152
|
-
)
|
153
|
-
|
154
|
-
# Relative activity name
|
155
|
-
return await self.start_activity(package, activity)
|
156
|
-
|
157
|
-
# Start main activity using monkey
|
158
|
-
cmd = f"monkey -p {package} -c android.intent.category.LAUNCHER 1"
|
159
|
-
result = await self._adb.shell(self._serial, cmd)
|
160
|
-
return f"Started {package}"
|
161
|
-
|
162
|
-
async def install_app(
|
163
|
-
self, apk_path: str, reinstall: bool = False, grant_permissions: bool = True
|
164
|
-
) -> str:
|
165
|
-
"""Install an APK on the device.
|
166
|
-
|
167
|
-
Args:
|
168
|
-
apk_path: Path to the APK file
|
169
|
-
reinstall: Whether to reinstall if app exists
|
170
|
-
grant_permissions: Whether to grant all requested permissions
|
171
|
-
|
172
|
-
Returns:
|
173
|
-
Installation result
|
174
|
-
"""
|
175
|
-
if not os.path.exists(apk_path):
|
176
|
-
return f"Error: APK file not found: {apk_path}"
|
177
|
-
|
178
|
-
# Build install command args
|
179
|
-
install_args = ["install"]
|
180
|
-
if reinstall:
|
181
|
-
install_args.append("-r")
|
182
|
-
if grant_permissions:
|
183
|
-
install_args.append("-g")
|
184
|
-
install_args.append(apk_path)
|
185
|
-
|
186
|
-
try:
|
187
|
-
stdout, stderr = await self._adb._run_device_command(
|
188
|
-
self._serial,
|
189
|
-
install_args,
|
190
|
-
timeout=120, # Longer timeout for installation
|
191
|
-
)
|
192
|
-
|
193
|
-
if "success" in stdout.lower():
|
194
|
-
return f"Successfully installed {os.path.basename(apk_path)}"
|
195
|
-
return f"Installation failed: {stdout or stderr}"
|
196
|
-
|
197
|
-
except Exception as e:
|
198
|
-
return f"Installation failed: {str(e)}"
|
199
|
-
|
200
|
-
async def uninstall_app(self, package: str, keep_data: bool = False) -> str:
|
201
|
-
"""Uninstall an app from the device.
|
202
|
-
|
203
|
-
Args:
|
204
|
-
package: Package name to uninstall
|
205
|
-
keep_data: Whether to keep app data and cache directories
|
206
|
-
|
207
|
-
Returns:
|
208
|
-
Uninstallation result
|
209
|
-
"""
|
210
|
-
cmd = ["pm", "uninstall"]
|
211
|
-
if keep_data:
|
212
|
-
cmd.append("-k")
|
213
|
-
cmd.append(package)
|
214
|
-
|
215
|
-
result = await self._adb.shell(self._serial, " ".join(cmd))
|
216
|
-
return result.strip()
|
217
|
-
|
218
|
-
async def take_screenshot(self, quality: int = 75) -> Tuple[str, bytes]:
|
219
|
-
"""Take a screenshot of the device and compress it.
|
220
|
-
|
221
|
-
Args:
|
222
|
-
quality: JPEG quality (1-100, lower means smaller file size)
|
223
|
-
|
224
|
-
Returns:
|
225
|
-
Tuple of (local file path, screenshot data as bytes)
|
226
|
-
"""
|
227
|
-
# Create a temporary file for the screenshot
|
228
|
-
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp:
|
229
|
-
screenshot_path = temp.name
|
230
|
-
|
231
|
-
try:
|
232
|
-
# Generate a random filename for the device
|
233
|
-
timestamp = int(time.time())
|
234
|
-
random_suffix = "".join(
|
235
|
-
random.choices(string.ascii_lowercase + string.digits, k=8)
|
236
|
-
)
|
237
|
-
device_path = f"/sdcard/screenshot_{timestamp}_{random_suffix}.png"
|
238
|
-
|
239
|
-
# Take screenshot using screencap command
|
240
|
-
await self._adb.shell(self._serial, f"screencap -p {device_path}")
|
241
|
-
|
242
|
-
# Pull screenshot to local machine
|
243
|
-
await self._adb._run_device_command(
|
244
|
-
self._serial, ["pull", device_path, screenshot_path]
|
245
|
-
)
|
246
|
-
|
247
|
-
# Clean up on device
|
248
|
-
await self._adb.shell(self._serial, f"rm {device_path}")
|
249
|
-
|
250
|
-
# Read the screenshot file
|
251
|
-
with open(screenshot_path, "rb") as f:
|
252
|
-
screenshot_data = f.read()
|
253
|
-
|
254
|
-
# Convert and compress the image
|
255
|
-
try:
|
256
|
-
from PIL import Image
|
257
|
-
import io
|
258
|
-
|
259
|
-
# Create buffer for the compressed image
|
260
|
-
buffer = io.BytesIO()
|
261
|
-
|
262
|
-
# Load the PNG data into a PIL Image
|
263
|
-
with Image.open(io.BytesIO(screenshot_data)) as img:
|
264
|
-
# Convert to RGB (removing alpha channel if present) and save as JPEG
|
265
|
-
converted_img = img.convert("RGB") if img.mode == "RGBA" else img
|
266
|
-
converted_img.save(
|
267
|
-
buffer, format="JPEG", quality=quality, optimize=True
|
268
|
-
)
|
269
|
-
compressed_data = buffer.getvalue()
|
270
|
-
|
271
|
-
# Get size reduction info for logging
|
272
|
-
png_size = len(screenshot_data) / 1024
|
273
|
-
jpg_size = len(compressed_data) / 1024
|
274
|
-
reduction = 100 - (jpg_size / png_size * 100) if png_size > 0 else 0
|
275
|
-
|
276
|
-
import logging
|
277
|
-
|
278
|
-
logger = logging.getLogger("droidrun")
|
279
|
-
logger.debug(
|
280
|
-
f"Screenshot compressed successfully: {png_size:.1f}KB → {jpg_size:.1f}KB ({reduction:.1f}% reduction)"
|
281
|
-
)
|
282
|
-
|
283
|
-
return screenshot_path, compressed_data
|
284
|
-
except ImportError:
|
285
|
-
# If PIL is not available, return the original PNG data
|
286
|
-
logger.warning("PIL not available, returning uncompressed screenshot")
|
287
|
-
return screenshot_path, screenshot_data
|
288
|
-
except Exception as e:
|
289
|
-
# If compression fails, return the original PNG data
|
290
|
-
logger.warning(
|
291
|
-
f"Screenshot compression failed: {e}, returning uncompressed"
|
292
|
-
)
|
293
|
-
return screenshot_path, screenshot_data
|
294
|
-
|
295
|
-
except Exception as e:
|
296
|
-
# Clean up in case of error
|
297
|
-
try:
|
298
|
-
os.unlink(screenshot_path)
|
299
|
-
except OSError:
|
300
|
-
pass
|
301
|
-
raise RuntimeError(f"Screenshot capture failed: {str(e)}")
|
302
|
-
|
303
|
-
def _parse_package_list(self, output: str) -> List[Dict[str, str]]:
|
304
|
-
"""Parse the output of 'pm list packages -f' command.
|
305
|
-
|
306
|
-
Args:
|
307
|
-
output: Raw command output from 'pm list packages -f'
|
308
|
-
|
309
|
-
Returns:
|
310
|
-
List of dictionaries containing package info with 'package' and 'path' keys
|
311
|
-
"""
|
312
|
-
apps = []
|
313
|
-
for line in output.splitlines():
|
314
|
-
if line.startswith("package:"):
|
315
|
-
# Format is: "package:/path/to/base.apk=com.package.name"
|
316
|
-
path_and_pkg = line[8:] # Strip "package:"
|
317
|
-
if "=" in path_and_pkg:
|
318
|
-
path, package = path_and_pkg.rsplit("=", 1)
|
319
|
-
apps.append({"package": package.strip(), "path": path.strip()})
|
320
|
-
return apps
|
321
|
-
|
322
|
-
async def list_packages(self, include_system_apps: bool = False) -> List[str]:
|
323
|
-
"""
|
324
|
-
List installed packages on the device.
|
325
|
-
|
326
|
-
Args:
|
327
|
-
include_system_apps: Whether to include system apps (default: False)
|
328
|
-
|
329
|
-
Returns:
|
330
|
-
List of package names
|
331
|
-
"""
|
332
|
-
# Use the direct ADB command to get packages with paths
|
333
|
-
cmd = ["pm", "list", "packages", "-f"]
|
334
|
-
if not include_system_apps:
|
335
|
-
cmd.append("-3")
|
336
|
-
|
337
|
-
output = await self.shell(" ".join(cmd))
|
338
|
-
|
339
|
-
# Parse the package list using the function
|
340
|
-
packages = self._parse_package_list(output)
|
341
|
-
# Format package list for better readability
|
342
|
-
package_list = [pack["package"] for pack in packages]
|
343
|
-
#for package in package_list:
|
344
|
-
# print(package)
|
345
|
-
return package_list
|
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 | None = None) -> 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 and 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 or not 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
|