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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: droidrun
3
- Version: 0.3.2
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
@@ -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
  [![Benchmark](https://img.shields.io/badge/Benchmark-🏅-teal)](https://droidrun.ai/benchmark)
65
68
  [![Twitter Follow](https://img.shields.io/twitter/follow/droid_run?style=social)](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&#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
+
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. **Group Chat Summarization**: Let DroidRun summarize an escalated group chat for you.
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>
95
108
 
96
- [![Group Chat Summarizer](https://img.youtube.com/vi/ofEnSUHHxX8/0.jpg)](https://www.youtube.com/watch?v=ofEnSUHHxX8)
109
+ 2. **Trend Hunter**: Let Droidrun hunt down trending posts
97
110
 
98
- 2. **Travel Search Assistant**: Wittness DroidRun looking for the cheapest stay and share it with a colleague on telegram.
111
+ [![Droidrun Trend Hunter Demo](https://img.youtube.com/vi/7V8S2f8PnkQ/0.jpg)](https://youtu.be/7V8S2f8PnkQ)
99
112
 
100
- [![Travel Search Assistant](https://img.youtube.com/vi/QgtRaLS3NBM/0.jpg)](https://www.youtube.com/watch?v=QgtRaLS3NBM)
113
+ <br>
101
114
 
102
- 3. **Automate TikTok Shopping**: See how DroidRun looks for a stanley cup on TikTok Shop and send the product details via email.
115
+ 3. **Streak Saver**: Let Droidrun save your streak on your favorite language learning app
103
116
 
104
- [![TikTok Shopping Assistant](https://img.youtube.com/vi/ol3bivBAmn4/0.jpg)](https://www.youtube.com/watch?v=ol3bivBAmn4)
117
+ [![Droidrun Streak Saver Demo](https://img.youtube.com/vi/B5q2B467HKw/0.jpg)](https://youtu.be/B5q2B467HKw)
105
118
 
106
119
 
107
120
  ## 💡 Example Use Cases
@@ -1,53 +1,54 @@
1
- droidrun/__init__.py,sha256=OqgWoczsC4gpt40pV-wuw7x5DQmmbKtNSxQ8QqPEY1E,510
1
+ droidrun/__init__.py,sha256=Cqt4NXZ-753220dlRZXglMkFT2ygcXSErMmqupGsi9A,622
2
2
  droidrun/__main__.py,sha256=78o1Wr_Z-NrZy9yLWmEfNfRRhAiJGBr4Xi3lmbgkx3w,105
3
- droidrun/portal.py,sha256=4UEoTdAg6TWgxCXEhyAXMARr8Hsq5wLDx_4zim9-eaA,4061
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=SoJPFeJUfNjs875rsbRSVrSjMIwIpWq_vYcct83PQhU,16075
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=axauKdrXFEeB1hpd0geazwBtJySnHsvLZV0u9F9OWZI,96
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=Kjze5UDSSjV2EZShXsIM75O1IPprSYop0y0MI4bbPUY,182
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=P2dAnkKiR3O4bN4ZUuWHmuu4Qo8WRiH1mU6EEOr3yJA,4710
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=SEJbFoLDwFqE_nXNke-D4e0lx9DwCY-I-ryywuW6-Y8,14955
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=vcZx8tawMss0F2nt-4QEtdF7kSaikUq3rdWPdTNicjk,10494
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=lQbk2TbPyl_F0k2FqEVimq8cQRcWM_KCO_I7fnkkxqA,4587
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=OmO8TvNO0LVtPXg2qTCv8o9ePaMeDyf-MRWN_YObXho,6845
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=kGYub0eJt-Ly-fczFPwHs5vq7OC_iP9ZjLZRo1umKcc,16730
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=lFVQfOVLuiGulILqVo-nBxJ_u7iC2TDVXwOK4TtHEZA,2611
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=8ElV7pNfm-c5FPDoZ7qjzkqtHSjnHzQ_iyEg2e9lhZU,23819
47
- droidrun/tools/ios.py,sha256=DrmL_4xtQ5IgJn0vdPKTCJBFxxkz3eNNoo74Ha8ZZVs,23053
48
- droidrun/tools/tools.py,sha256=fgl4B62UA5KmxPnAXI1aZY5sncmKl0N97zKrjTdL510,3215
49
- droidrun-0.3.2.dist-info/METADATA,sha256=frA--iht9pvq_bTlAnEjWJ_gSbSd4yi42xW6gpF03gs,5838
50
- droidrun-0.3.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
51
- droidrun-0.3.2.dist-info/entry_points.txt,sha256=o259U66js8TIybQ7zs814Oe_LQ_GpZsp6a9Cr-xm5zE,51
52
- droidrun-0.3.2.dist-info/licenses/LICENSE,sha256=s-uxn9qChu-kFdRXUp6v_0HhsaJ_5OANmfNOFVm2zdk,1069
53
- droidrun-0.3.2.dist-info/RECORD,,
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