jaydroid 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jaydroid-0.1.0/LICENSE +0 -0
- jaydroid-0.1.0/PKG-INFO +188 -0
- jaydroid-0.1.0/README.md +169 -0
- jaydroid-0.1.0/pyproject.toml +30 -0
- jaydroid-0.1.0/setup.cfg +4 -0
- jaydroid-0.1.0/src/jaydroid/__init__.py +20 -0
- jaydroid-0.1.0/src/jaydroid/button.py +137 -0
- jaydroid-0.1.0/src/jaydroid/core.py +27 -0
- jaydroid-0.1.0/src/jaydroid/device.py +147 -0
- jaydroid-0.1.0/src/jaydroid/exceptions.py +8 -0
- jaydroid-0.1.0/src/jaydroid/gesture.py +139 -0
- jaydroid-0.1.0/src/jaydroid/screen.py +106 -0
- jaydroid-0.1.0/src/jaydroid/utils.py +32 -0
- jaydroid-0.1.0/src/jaydroid.egg-info/PKG-INFO +188 -0
- jaydroid-0.1.0/src/jaydroid.egg-info/SOURCES.txt +16 -0
- jaydroid-0.1.0/src/jaydroid.egg-info/dependency_links.txt +1 -0
- jaydroid-0.1.0/src/jaydroid.egg-info/top_level.txt +1 -0
- jaydroid-0.1.0/tests/test_core.py +6 -0
jaydroid-0.1.0/LICENSE
ADDED
|
File without changes
|
jaydroid-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jaydroid
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A beginner-friendly Python wrapper for simulating Android actions via ADB (tap, swipe, power on/off).
|
|
5
|
+
Author: Johnston Kweku Abubakar
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/johnston-kweku/jaydroid
|
|
8
|
+
Project-URL: Issues, https://github.com/johnston-kweku/jaydroid/issues
|
|
9
|
+
Keywords: adb,android,automation,adb-wrapper
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# jaydroid
|
|
21
|
+
|
|
22
|
+
`jaydroid` is a small Python wrapper around [Android Debug Bridge (ADB)](https://developer.android.com/tools/adb). It provides helpers for connecting to an Android device, reading display dimensions and device info, sending button input, capturing the screen, and performing swipe and tap gestures.
|
|
23
|
+
|
|
24
|
+
This is my first Python package, built as a way to learn how to design, structure, and publish one from scratch. I'm documenting the build process — bugs, refactors, and design decisions — as a video series. Expect it to keep growing.
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
- Python 3.8 or newer
|
|
29
|
+
- ADB installed and available on your `PATH`
|
|
30
|
+
- An Android device or emulator connected to ADB
|
|
31
|
+
- USB debugging enabled when using a physical device
|
|
32
|
+
|
|
33
|
+
Check the connection with:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
adb devices
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install jaydroid
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For local development, from the project directory:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python -m pip install -e .
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
`jaydroid` exposes ready-to-use instances, so there's no setup required beyond connecting your device:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from jaydroid import device, button, swipe, tap
|
|
57
|
+
|
|
58
|
+
device.connect()
|
|
59
|
+
|
|
60
|
+
button.wake(delay=1)
|
|
61
|
+
swipe.swipe_up()
|
|
62
|
+
tap.tap(360, 640)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`device.connect()` checks that a device or emulator is actually reachable over ADB and loads its display dimensions. It's the one required step before using anything that depends on screen size (like the directional swipe helpers).
|
|
66
|
+
|
|
67
|
+
## Delays
|
|
68
|
+
|
|
69
|
+
Every `Button`, `Screen`, `Swipe`, and `Tap` method accepts an optional `delay` argument, in seconds (default `0`). The delay runs *after* the action completes successfully — "do this, then wait" — so it's useful for giving the device a moment to catch up before your next command fires:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
button.wake(delay=1)
|
|
73
|
+
swipe.swipe_up(delay=0.5)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If a command fails, the delay is skipped — you'll see the error immediately rather than waiting first.
|
|
77
|
+
|
|
78
|
+
## Gestures
|
|
79
|
+
|
|
80
|
+
### Swipe
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from jaydroid import swipe
|
|
84
|
+
|
|
85
|
+
swipe.swipe_up()
|
|
86
|
+
swipe.swipe_down()
|
|
87
|
+
swipe.swipe_left()
|
|
88
|
+
swipe.swipe_right()
|
|
89
|
+
swipe.unlock() # alias for swipe_up()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Directional swipes are calculated as a percentage of the connected device's display size, so they scale across different screens rather than relying on hardcoded pixels:
|
|
93
|
+
|
|
94
|
+
- Left and right: 10% to 90% of display width, at 50% of height
|
|
95
|
+
- Up and down: 50% of display width, from 80% to 10% of height
|
|
96
|
+
|
|
97
|
+
For custom coordinates, use `swipe.swipe(x1, y1, x2, y2)`:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
swipe.swipe(600, 640, 100, 640, delay=0.5)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Tap
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from jaydroid import tap
|
|
107
|
+
|
|
108
|
+
tap.tap(360, 640)
|
|
109
|
+
tap.double_tap(360, 640)
|
|
110
|
+
tap.longpress(360, 640, duration=1000)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Buttons
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from jaydroid import button
|
|
117
|
+
|
|
118
|
+
button.power()
|
|
119
|
+
button.wake()
|
|
120
|
+
button.sleep()
|
|
121
|
+
button.home()
|
|
122
|
+
button.back()
|
|
123
|
+
button.recent_apps()
|
|
124
|
+
button.menu()
|
|
125
|
+
button.volume_up()
|
|
126
|
+
button.volume_down()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Each accepts `delay=0`.
|
|
130
|
+
|
|
131
|
+
## Screen
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from jaydroid.screen import Screen
|
|
135
|
+
|
|
136
|
+
screen = Screen()
|
|
137
|
+
screen.capture()
|
|
138
|
+
screen.screenshot(filename='screen.png', pull=True)
|
|
139
|
+
screen.screenrecord(filename='recording.mp4', pull=True, duration=10)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`screenshot()` and `screenrecord()` save to `/sdcard/` on the device by default, and can optionally pull the file to your local directory with `pull=True`.
|
|
143
|
+
|
|
144
|
+
## Device information
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from jaydroid import device
|
|
148
|
+
|
|
149
|
+
device.connect()
|
|
150
|
+
print(device.width, device.height)
|
|
151
|
+
print(device.is_connected())
|
|
152
|
+
print(device.get_android_version())
|
|
153
|
+
print(device.battery_info())
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- `width` and `height` are only available after `connect()` — accessing them beforehand raises `DeviceNotConnectedError`.
|
|
157
|
+
- `is_connected()`, `get_android_version()`, and `battery_info()` check the ADB connection live each time they're called, so they don't require `connect()` to have been run first.
|
|
158
|
+
- `battery_info()` returns a dictionary parsed from `adb shell dumpsys battery`. Core fields (`level`, `status`, `health`, `voltage`, `temperature`, `technology`, and the `*_powered` flags) are consistently present across devices, but the full set of keys can vary by manufacturer, since some OEMs include additional proprietary fields.
|
|
159
|
+
|
|
160
|
+
## Errors
|
|
161
|
+
|
|
162
|
+
`jaydroid` raises specific exceptions instead of generic ones, so you can catch exactly what went wrong:
|
|
163
|
+
|
|
164
|
+
- `DeviceNotFoundError` — raised by `connect()` when no device or emulator is reachable over ADB.
|
|
165
|
+
- `DeviceNotConnectedError` — raised when accessing `width`/`height` before `connect()` has been called.
|
|
166
|
+
- `AdbCommandError` — raised when an ADB command itself fails (e.g. the device disconnects mid-session). Includes ADB's own error output.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from jaydroid import device
|
|
170
|
+
from jaydroid.exceptions import DeviceNotFoundError
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
device.connect()
|
|
174
|
+
except DeviceNotFoundError:
|
|
175
|
+
print("No device connected — plug one in and try again.")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Roadmap
|
|
179
|
+
|
|
180
|
+
Planned additions include WiFi status, installed app listing, and more device/system information. This package is under active development.
|
|
181
|
+
|
|
182
|
+
## Author
|
|
183
|
+
|
|
184
|
+
Built by Johnston Kweku Abubakar ([@johnston-kweku](https://github.com/johnston-kweku)) — this is my first Python package, built while learning to design, structure, and publish one from scratch.
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
This project is licensed under the MIT License. See [LICENSE](LICENSE).
|
jaydroid-0.1.0/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# jaydroid
|
|
2
|
+
|
|
3
|
+
`jaydroid` is a small Python wrapper around [Android Debug Bridge (ADB)](https://developer.android.com/tools/adb). It provides helpers for connecting to an Android device, reading display dimensions and device info, sending button input, capturing the screen, and performing swipe and tap gestures.
|
|
4
|
+
|
|
5
|
+
This is my first Python package, built as a way to learn how to design, structure, and publish one from scratch. I'm documenting the build process — bugs, refactors, and design decisions — as a video series. Expect it to keep growing.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Python 3.8 or newer
|
|
10
|
+
- ADB installed and available on your `PATH`
|
|
11
|
+
- An Android device or emulator connected to ADB
|
|
12
|
+
- USB debugging enabled when using a physical device
|
|
13
|
+
|
|
14
|
+
Check the connection with:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
adb devices
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install jaydroid
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
For local development, from the project directory:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
python -m pip install -e .
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
`jaydroid` exposes ready-to-use instances, so there's no setup required beyond connecting your device:
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from jaydroid import device, button, swipe, tap
|
|
38
|
+
|
|
39
|
+
device.connect()
|
|
40
|
+
|
|
41
|
+
button.wake(delay=1)
|
|
42
|
+
swipe.swipe_up()
|
|
43
|
+
tap.tap(360, 640)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`device.connect()` checks that a device or emulator is actually reachable over ADB and loads its display dimensions. It's the one required step before using anything that depends on screen size (like the directional swipe helpers).
|
|
47
|
+
|
|
48
|
+
## Delays
|
|
49
|
+
|
|
50
|
+
Every `Button`, `Screen`, `Swipe`, and `Tap` method accepts an optional `delay` argument, in seconds (default `0`). The delay runs *after* the action completes successfully — "do this, then wait" — so it's useful for giving the device a moment to catch up before your next command fires:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
button.wake(delay=1)
|
|
54
|
+
swipe.swipe_up(delay=0.5)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If a command fails, the delay is skipped — you'll see the error immediately rather than waiting first.
|
|
58
|
+
|
|
59
|
+
## Gestures
|
|
60
|
+
|
|
61
|
+
### Swipe
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from jaydroid import swipe
|
|
65
|
+
|
|
66
|
+
swipe.swipe_up()
|
|
67
|
+
swipe.swipe_down()
|
|
68
|
+
swipe.swipe_left()
|
|
69
|
+
swipe.swipe_right()
|
|
70
|
+
swipe.unlock() # alias for swipe_up()
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Directional swipes are calculated as a percentage of the connected device's display size, so they scale across different screens rather than relying on hardcoded pixels:
|
|
74
|
+
|
|
75
|
+
- Left and right: 10% to 90% of display width, at 50% of height
|
|
76
|
+
- Up and down: 50% of display width, from 80% to 10% of height
|
|
77
|
+
|
|
78
|
+
For custom coordinates, use `swipe.swipe(x1, y1, x2, y2)`:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
swipe.swipe(600, 640, 100, 640, delay=0.5)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Tap
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from jaydroid import tap
|
|
88
|
+
|
|
89
|
+
tap.tap(360, 640)
|
|
90
|
+
tap.double_tap(360, 640)
|
|
91
|
+
tap.longpress(360, 640, duration=1000)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Buttons
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from jaydroid import button
|
|
98
|
+
|
|
99
|
+
button.power()
|
|
100
|
+
button.wake()
|
|
101
|
+
button.sleep()
|
|
102
|
+
button.home()
|
|
103
|
+
button.back()
|
|
104
|
+
button.recent_apps()
|
|
105
|
+
button.menu()
|
|
106
|
+
button.volume_up()
|
|
107
|
+
button.volume_down()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Each accepts `delay=0`.
|
|
111
|
+
|
|
112
|
+
## Screen
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from jaydroid.screen import Screen
|
|
116
|
+
|
|
117
|
+
screen = Screen()
|
|
118
|
+
screen.capture()
|
|
119
|
+
screen.screenshot(filename='screen.png', pull=True)
|
|
120
|
+
screen.screenrecord(filename='recording.mp4', pull=True, duration=10)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`screenshot()` and `screenrecord()` save to `/sdcard/` on the device by default, and can optionally pull the file to your local directory with `pull=True`.
|
|
124
|
+
|
|
125
|
+
## Device information
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from jaydroid import device
|
|
129
|
+
|
|
130
|
+
device.connect()
|
|
131
|
+
print(device.width, device.height)
|
|
132
|
+
print(device.is_connected())
|
|
133
|
+
print(device.get_android_version())
|
|
134
|
+
print(device.battery_info())
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- `width` and `height` are only available after `connect()` — accessing them beforehand raises `DeviceNotConnectedError`.
|
|
138
|
+
- `is_connected()`, `get_android_version()`, and `battery_info()` check the ADB connection live each time they're called, so they don't require `connect()` to have been run first.
|
|
139
|
+
- `battery_info()` returns a dictionary parsed from `adb shell dumpsys battery`. Core fields (`level`, `status`, `health`, `voltage`, `temperature`, `technology`, and the `*_powered` flags) are consistently present across devices, but the full set of keys can vary by manufacturer, since some OEMs include additional proprietary fields.
|
|
140
|
+
|
|
141
|
+
## Errors
|
|
142
|
+
|
|
143
|
+
`jaydroid` raises specific exceptions instead of generic ones, so you can catch exactly what went wrong:
|
|
144
|
+
|
|
145
|
+
- `DeviceNotFoundError` — raised by `connect()` when no device or emulator is reachable over ADB.
|
|
146
|
+
- `DeviceNotConnectedError` — raised when accessing `width`/`height` before `connect()` has been called.
|
|
147
|
+
- `AdbCommandError` — raised when an ADB command itself fails (e.g. the device disconnects mid-session). Includes ADB's own error output.
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
from jaydroid import device
|
|
151
|
+
from jaydroid.exceptions import DeviceNotFoundError
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
device.connect()
|
|
155
|
+
except DeviceNotFoundError:
|
|
156
|
+
print("No device connected — plug one in and try again.")
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Roadmap
|
|
160
|
+
|
|
161
|
+
Planned additions include WiFi status, installed app listing, and more device/system information. This package is under active development.
|
|
162
|
+
|
|
163
|
+
## Author
|
|
164
|
+
|
|
165
|
+
Built by Johnston Kweku Abubakar ([@johnston-kweku](https://github.com/johnston-kweku)) — this is my first Python package, built while learning to design, structure, and publish one from scratch.
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
This project is licensed under the MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "jaydroid"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A beginner-friendly Python wrapper for simulating Android actions via ADB (tap, swipe, power on/off)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Johnston Kweku Abubakar" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["adb", "android", "automation", "adb-wrapper"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/johnston-kweku/jaydroid"
|
|
27
|
+
Issues = "https://github.com/johnston-kweku/jaydroid/issues"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
jaydroid-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
jaydroid — a beginner-friendly Python wrapper for simulating Android actions via ADB.
|
|
3
|
+
|
|
4
|
+
Example:
|
|
5
|
+
import jaydroid
|
|
6
|
+
|
|
7
|
+
jaydroid.device.connect()
|
|
8
|
+
jaydroid.tap.tap(500, 800)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .core import device, button, swipe, tap, Device, Button, Screen, Gesture
|
|
12
|
+
from .exceptions import DeviceNotFoundError, DeviceNotConnectedError
|
|
13
|
+
|
|
14
|
+
__version__ = "0.1.0"
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"device", "button", "swipe", "tap",
|
|
18
|
+
"Device", "Button", "Screen", "Gesture",
|
|
19
|
+
"DeviceNotFoundError", "DeviceNotConnectedError",
|
|
20
|
+
]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from .utils import adb
|
|
2
|
+
|
|
3
|
+
class Button:
|
|
4
|
+
"""
|
|
5
|
+
Methods for simulating common Android hardware and navigation buttons.
|
|
6
|
+
|
|
7
|
+
ADB must be installed and available on ``PATH``, and a device or emulator
|
|
8
|
+
must be connected before calling these methods.
|
|
9
|
+
|
|
10
|
+
Each button method accepts ``delay`` in seconds and waits after its ADB
|
|
11
|
+
action completes. The default delay is ``0``.
|
|
12
|
+
"""
|
|
13
|
+
def power(self, delay=0):
|
|
14
|
+
"""
|
|
15
|
+
Toggle the device's screen with a power-button press.
|
|
16
|
+
|
|
17
|
+
The screen is turned on when it is off and turned off when it is on.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
subprocess.CompletedProcess: The result of the ADB command.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
delay (int | float): Seconds to wait after the key event completes.
|
|
24
|
+
Defaults to ``0``.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
return adb('shell', 'input', 'keyevent', '26', delay=delay)
|
|
28
|
+
|
|
29
|
+
def volume_up(self, delay=0):
|
|
30
|
+
"""
|
|
31
|
+
Increase the device's system volume by one default increment.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
subprocess.CompletedProcess: The result of the ADB command.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
delay (int | float): Seconds to wait after the key event completes.
|
|
38
|
+
Defaults to ``0``.
|
|
39
|
+
"""
|
|
40
|
+
return adb('shell', 'input', 'keyevent', '24', delay=delay)
|
|
41
|
+
|
|
42
|
+
def volume_down(self, delay=0):
|
|
43
|
+
"""
|
|
44
|
+
Decrease the device's system volume by one default increment.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
subprocess.CompletedProcess: The result of the ADB command.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
delay (int | float): Seconds to wait after the key event completes.
|
|
51
|
+
Defaults to ``0``.
|
|
52
|
+
"""
|
|
53
|
+
return adb('shell', 'input', 'keyevent', '25', delay=delay)
|
|
54
|
+
|
|
55
|
+
def home(self, delay=0):
|
|
56
|
+
"""
|
|
57
|
+
Navigate to the device home screen.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
subprocess.CompletedProcess: The result of the ADB command.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
delay (int | float): Seconds to wait after the key event completes.
|
|
64
|
+
Defaults to ``0``.
|
|
65
|
+
"""
|
|
66
|
+
return adb('shell', 'input', 'keyevent', '3', delay=delay)
|
|
67
|
+
|
|
68
|
+
def back(self, delay=0):
|
|
69
|
+
"""
|
|
70
|
+
Navigate back one step in the current Android interface.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
subprocess.CompletedProcess: The result of the ADB command.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
delay (int | float): Seconds to wait after the key event completes.
|
|
77
|
+
Defaults to ``0``.
|
|
78
|
+
"""
|
|
79
|
+
return adb('shell', 'input', 'keyevent', '4', delay=delay)
|
|
80
|
+
|
|
81
|
+
def recent_apps(self, delay=0):
|
|
82
|
+
"""
|
|
83
|
+
Open the Android recent-apps view.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
subprocess.CompletedProcess: The result of the ADB command.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
delay (int | float): Seconds to wait after the key event completes.
|
|
90
|
+
Defaults to ``0``.
|
|
91
|
+
"""
|
|
92
|
+
return adb('shell', 'input', 'keyevent', '187', delay=delay)
|
|
93
|
+
|
|
94
|
+
def menu(self, delay=0):
|
|
95
|
+
"""
|
|
96
|
+
Send the Android menu-button key event.
|
|
97
|
+
|
|
98
|
+
The effect depends on the active application and Android version.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
subprocess.CompletedProcess: The result of the ADB command.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
delay (int | float): Seconds to wait after the key event completes.
|
|
105
|
+
Defaults to ``0``.
|
|
106
|
+
"""
|
|
107
|
+
return adb('shell', 'input', 'keyevent', '82', delay=delay)
|
|
108
|
+
|
|
109
|
+
def wake(self, delay=0):
|
|
110
|
+
"""
|
|
111
|
+
Wake the device's screen if it is off.
|
|
112
|
+
|
|
113
|
+
This has no effect when the device is already awake.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
subprocess.CompletedProcess: The result of the ADB command.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
delay (int | float): Seconds to wait after the key event completes.
|
|
120
|
+
Defaults to ``0``.
|
|
121
|
+
"""
|
|
122
|
+
return adb('shell', 'input', 'keyevent', '224', delay=delay)
|
|
123
|
+
|
|
124
|
+
def sleep(self, delay=0):
|
|
125
|
+
"""
|
|
126
|
+
Put the device's screen to sleep.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
subprocess.CompletedProcess: The result of the ADB command.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
delay (int | float): Seconds to wait after the key event completes.
|
|
133
|
+
Defaults to ``0``.
|
|
134
|
+
"""
|
|
135
|
+
return adb('shell', 'input', 'keyevent', '223', delay=delay)
|
|
136
|
+
|
|
137
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Utilities for sending Android Debug Bridge (ADB) input commands.
|
|
3
|
+
|
|
4
|
+
The connected Android device must have developer options and USB debugging
|
|
5
|
+
enabled. Most operations return the :class:`subprocess.CompletedProcess`
|
|
6
|
+
instance produced by :func:`subprocess.run`, allowing callers to inspect the
|
|
7
|
+
command's output and return code.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from .device import Device
|
|
12
|
+
from .gesture import Gesture
|
|
13
|
+
from .button import Button
|
|
14
|
+
from .screen import Screen
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
device = Device()
|
|
22
|
+
button = Button()
|
|
23
|
+
swipe = Gesture.Swipe(device=device)
|
|
24
|
+
tap = Gesture.Tap(device=device)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from .utils import adb
|
|
2
|
+
from .exceptions import DeviceNotFoundError, DeviceNotConnectedError
|
|
3
|
+
|
|
4
|
+
class Device:
|
|
5
|
+
"""
|
|
6
|
+
Manage connection state and display dimensions for an Android device.
|
|
7
|
+
|
|
8
|
+
Create a ``Device`` instance before using gesture helpers, then call
|
|
9
|
+
:meth:`connect` to verify that an ADB device is available and load its
|
|
10
|
+
display dimensions.
|
|
11
|
+
|
|
12
|
+
Device information helpers query the connected device through ADB and
|
|
13
|
+
raise ``DeviceNotConnectedError`` when a connection is required but absent.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self._width = None
|
|
18
|
+
self._height = None
|
|
19
|
+
|
|
20
|
+
def connect(self):
|
|
21
|
+
"""Connect to the first available ADB device and load its display size.
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
DeviceNotFoundError: If no device or emulator is available through
|
|
25
|
+
ADB.
|
|
26
|
+
"""
|
|
27
|
+
result = adb('devices')
|
|
28
|
+
devices = result.stdout.splitlines()
|
|
29
|
+
header = devices.pop(0)
|
|
30
|
+
devices = [item for item in devices if item]
|
|
31
|
+
if not devices:
|
|
32
|
+
raise DeviceNotFoundError('Error: No device(s) connected')
|
|
33
|
+
|
|
34
|
+
self.setup()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def width(self):
|
|
39
|
+
"""The connected device's display width in pixels.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
DeviceNotConnectedError: If :meth:`connect` has not completed.
|
|
43
|
+
"""
|
|
44
|
+
if self._width is not None:
|
|
45
|
+
return self._width
|
|
46
|
+
raise DeviceNotConnectedError('Error: No device/emulator connected. Did you forget to run connect()? See documentation')
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def height(self):
|
|
51
|
+
"""The connected device's display height in pixels.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
DeviceNotConnectedError: If :meth:`connect` has not completed.
|
|
55
|
+
"""
|
|
56
|
+
if self._height is not None:
|
|
57
|
+
return self._height
|
|
58
|
+
raise DeviceNotConnectedError('Error: No device/emulator connected. Did you forget to run connect()? See documentation')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def setup(self):
|
|
63
|
+
"""Refresh and store the device display width and height.
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
RuntimeError: If the device does not return a recognized display
|
|
67
|
+
resolution.
|
|
68
|
+
"""
|
|
69
|
+
self._width, self._height = self.get_screen_size()
|
|
70
|
+
|
|
71
|
+
def get_screen_size(self):
|
|
72
|
+
"""Return the physical display size as ``(width, height)``.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
RuntimeError: If ADB does not return a ``Physical size: WIDTHxHEIGHT``
|
|
76
|
+
line.
|
|
77
|
+
"""
|
|
78
|
+
result = adb('shell', 'wm', 'size')
|
|
79
|
+
screen_info = result.stdout.splitlines()
|
|
80
|
+
for info in screen_info:
|
|
81
|
+
if info.startswith('Physical'):
|
|
82
|
+
screen = info
|
|
83
|
+
break
|
|
84
|
+
else:
|
|
85
|
+
raise RuntimeError('Connected device did not return a recognized resolution format.\
|
|
86
|
+
You can use swipe() to enter custom coordinates')
|
|
87
|
+
text, resolution = screen.split(':')
|
|
88
|
+
resolution = resolution.strip()
|
|
89
|
+
width, height = resolution.split('x')
|
|
90
|
+
return int(width), int(height)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def is_connected(self):
|
|
94
|
+
"""Return whether an Android device or emulator is connected through ADB.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
bool: ``True`` when at least one device is connected; otherwise,
|
|
98
|
+
``False``.
|
|
99
|
+
"""
|
|
100
|
+
result = adb('devices')
|
|
101
|
+
devices = result.stdout.splitlines()
|
|
102
|
+
header = devices.pop(0)
|
|
103
|
+
devices = [item for item in devices if item]
|
|
104
|
+
return bool(devices)
|
|
105
|
+
|
|
106
|
+
def get_android_version(self):
|
|
107
|
+
"""Return the Android release version of the connected device.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
str: The Android release version reported by ADB.
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
DeviceNotConnectedError: If no device or emulator is connected.
|
|
114
|
+
"""
|
|
115
|
+
if self.is_connected():
|
|
116
|
+
result = adb('shell', 'getprop', 'ro.build.version.release')
|
|
117
|
+
android_version = result.stdout.strip()
|
|
118
|
+
return android_version
|
|
119
|
+
error = 'No device was detected. Connect an android device and try again.'
|
|
120
|
+
raise DeviceNotConnectedError(error)
|
|
121
|
+
|
|
122
|
+
def battery_info(self):
|
|
123
|
+
"""Return battery information reported by the connected device.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
dict: Battery properties parsed from ``adb shell dumpsys battery``.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
DeviceNotConnectedError: If no device or emulator is connected.
|
|
130
|
+
"""
|
|
131
|
+
if self.is_connected():
|
|
132
|
+
result = adb('shell', 'dumpsys', 'battery')
|
|
133
|
+
info = result.stdout
|
|
134
|
+
info = info.splitlines()
|
|
135
|
+
info.pop(0)
|
|
136
|
+
info = [line.strip() for line in info if line.count(':') == 1]
|
|
137
|
+
|
|
138
|
+
battery_information = {}
|
|
139
|
+
for line in info:
|
|
140
|
+
key, value = line.split(':')
|
|
141
|
+
key, value = key.strip(), value.strip()
|
|
142
|
+
|
|
143
|
+
battery_information[key] = value
|
|
144
|
+
|
|
145
|
+
return battery_information
|
|
146
|
+
error = 'No device was detected. Connect an android device and try again.'
|
|
147
|
+
raise DeviceNotConnectedError(error)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from .utils import adb
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Gesture:
|
|
6
|
+
"""Gesture helpers for a connected Android device.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
device (Device): Device whose display dimensions are used for the
|
|
10
|
+
built-in directional gestures.
|
|
11
|
+
|
|
12
|
+
Gesture methods accept ``delay`` in seconds and wait after the ADB action
|
|
13
|
+
completes. The default delay is ``0``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, device):
|
|
17
|
+
"""Create gesture helpers associated with ``device``."""
|
|
18
|
+
self.device = device
|
|
19
|
+
self.swipe = Gesture.Swipe(device)
|
|
20
|
+
self.tap = Gesture.Tap(device)
|
|
21
|
+
|
|
22
|
+
class Swipe():
|
|
23
|
+
"""Helpers for sending coordinate-based swipe gestures.
|
|
24
|
+
|
|
25
|
+
Built-in directional gestures calculate their coordinates as a
|
|
26
|
+
percentage of the associated device's display dimensions.
|
|
27
|
+
"""
|
|
28
|
+
def __init__(self, device):
|
|
29
|
+
"""Create a swipe helper associated with ``device``."""
|
|
30
|
+
self.device = device
|
|
31
|
+
|
|
32
|
+
def swipe(self, x1, y1, x2, y2, delay=0):
|
|
33
|
+
"""Swipe from ``(x1, y1)`` to ``(x2, y2)`` in screen pixels.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
delay (int | float): Seconds to wait after the swipe completes.
|
|
37
|
+
Defaults to ``0``.
|
|
38
|
+
"""
|
|
39
|
+
return adb('shell', 'input', 'swipe', str(x1), str(y1), str(x2), str(y2), delay=delay)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def swipe_up(self, delay=0):
|
|
43
|
+
"""Swipe upward using the built-in screen coordinates.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
delay (int | float): Seconds to wait after the swipe completes.
|
|
47
|
+
Defaults to ``0``.
|
|
48
|
+
"""
|
|
49
|
+
x1 = int(self.device.width * 0.5)
|
|
50
|
+
y1 = int(self.device.height * 0.60)
|
|
51
|
+
y2 = int(self.device.height * 0.10)
|
|
52
|
+
return adb('shell', 'input', 'swipe', str(x1), str(y1), str(x1), str(y2), delay=delay)
|
|
53
|
+
|
|
54
|
+
def unlock(self, delay=0):
|
|
55
|
+
"""Attempt to unlock the device with an upward swipe.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
delay (int | float): Seconds to wait after the swipe completes.
|
|
59
|
+
Defaults to ``0``.
|
|
60
|
+
"""
|
|
61
|
+
return self.swipe_up(delay=delay)
|
|
62
|
+
|
|
63
|
+
def swipe_down(self, delay=0):
|
|
64
|
+
"""Swipe downward using the built-in screen coordinates.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
delay (int | float): Seconds to wait after the swipe completes.
|
|
68
|
+
Defaults to ``0``.
|
|
69
|
+
"""
|
|
70
|
+
x1 = int(self.device.width * 0.5)
|
|
71
|
+
y1 = int(self.device.height * 0.10)
|
|
72
|
+
y2 = int(self.device.height * 0.80)
|
|
73
|
+
|
|
74
|
+
return adb('shell', 'input', 'swipe', str(x1), str(y1), str(x1), str(y2), delay=delay)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def swipe_right(self, delay=0):
|
|
78
|
+
"""Swipe right using the built-in screen coordinates.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
delay (int | float): Seconds to wait after the swipe completes.
|
|
82
|
+
Defaults to ``0``.
|
|
83
|
+
"""
|
|
84
|
+
x1 = int(self.device.width * 0.10)
|
|
85
|
+
y1 = int(self.device.height * 0.50)
|
|
86
|
+
x2 = int(self.device.width * 0.70)
|
|
87
|
+
return adb('shell', 'input', 'swipe', str(x1), str(y1), str(x2), str(y1), delay=delay)
|
|
88
|
+
|
|
89
|
+
def swipe_left(self, delay=0):
|
|
90
|
+
"""Swipe left using the built-in screen coordinates.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
delay (int | float): Seconds to wait after the swipe completes.
|
|
94
|
+
Defaults to ``0``.
|
|
95
|
+
"""
|
|
96
|
+
x1 = int(self.device.width * 0.70)
|
|
97
|
+
y1 = int(self.device.height * 0.50)
|
|
98
|
+
x2 = int(self.device.width * 0.10)
|
|
99
|
+
return adb('shell', 'input', 'swipe', str(x1), str(y1), str(x2), str(y1), delay=delay)
|
|
100
|
+
|
|
101
|
+
class Tap:
|
|
102
|
+
"""Helpers for sending coordinate-based tap gestures.
|
|
103
|
+
|
|
104
|
+
Tap coordinates are expressed in screen pixels.
|
|
105
|
+
"""
|
|
106
|
+
def __init__(self, device):
|
|
107
|
+
"""Create a tap helper associated with ``device``."""
|
|
108
|
+
self.device = device
|
|
109
|
+
|
|
110
|
+
def tap(self, x, y, delay=0):
|
|
111
|
+
"""Tap at ``(x, y)`` in screen pixels.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
delay (int | float): Seconds to wait after the tap completes.
|
|
115
|
+
Defaults to ``0``.
|
|
116
|
+
"""
|
|
117
|
+
return adb('shell', 'input', 'tap', str(x), str(y), delay=delay)
|
|
118
|
+
|
|
119
|
+
def double_tap(self, x, y, delay=0):
|
|
120
|
+
"""Double-tap near ``(x, y)`` in screen pixels.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
delay (int | float): Seconds to wait after each tap completes.
|
|
124
|
+
Defaults to ``0``.
|
|
125
|
+
"""
|
|
126
|
+
self.tap(x, y, delay=delay)
|
|
127
|
+
time.sleep(0.05)
|
|
128
|
+
return self.tap(x, y, delay=delay)
|
|
129
|
+
|
|
130
|
+
def longpress(self, x, y, duration=3000, delay=0):
|
|
131
|
+
"""Press at ``(x, y)`` for ``duration`` milliseconds.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
delay (int | float): Seconds to wait after the long press
|
|
135
|
+
completes. Defaults to ``0``.
|
|
136
|
+
"""
|
|
137
|
+
return adb('shell', 'input', 'swipe', str(x), str(y), str(x), str(y), str(duration), delay=delay)
|
|
138
|
+
|
|
139
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from .utils import adb
|
|
2
|
+
|
|
3
|
+
class Screen:
|
|
4
|
+
"""Methods for capturing screenshots and recording the device screen.
|
|
5
|
+
|
|
6
|
+
Each method accepts ``delay`` in seconds and waits after its ADB action
|
|
7
|
+
completes. For methods that run multiple commands, the delay is applied
|
|
8
|
+
after each command. The default delay is ``0``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def capture(self, delay=0):
|
|
12
|
+
"""
|
|
13
|
+
Send the Android screenshot key event.
|
|
14
|
+
|
|
15
|
+
This method does not save or pull an image file. Use :meth:`screenshot`
|
|
16
|
+
to capture the screen to a file.
|
|
17
|
+
|
|
18
|
+
Note:
|
|
19
|
+
This key event is less universally reliable across OEM skins than
|
|
20
|
+
calling :meth:`screenshot` directly.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
subprocess.CompletedProcess: The result of the ADB command.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
delay (int | float): Seconds to wait after the capture completes.
|
|
27
|
+
Defaults to ``0``.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
return adb('shell', 'input', 'keyevent', '120', delay=delay)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def screenshot(self, filename=None, pull=False, delay=0):
|
|
34
|
+
"""
|
|
35
|
+
Save a screenshot on the connected device.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
filename (str | None): Name of the image file on the device.
|
|
39
|
+
Defaults to ``'screenshot.png'``. The file is saved under
|
|
40
|
+
``/sdcard/``.
|
|
41
|
+
pull (bool): If ``True``, copy the screenshot to the current local
|
|
42
|
+
directory after capturing it. Defaults to ``False``.
|
|
43
|
+
delay (int | float): Seconds to wait after each ADB command.
|
|
44
|
+
Defaults to ``0``.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
subprocess.CompletedProcess: The result of ``adb pull`` when
|
|
48
|
+
``pull`` is ``True``; otherwise, the result of the capture
|
|
49
|
+
command.
|
|
50
|
+
"""
|
|
51
|
+
if not filename:
|
|
52
|
+
filename = 'screenshot.png'
|
|
53
|
+
remote_path = f'/sdcard/{filename}'
|
|
54
|
+
|
|
55
|
+
res_capture = adb('shell', 'screencap', '-p', remote_path, delay=delay)
|
|
56
|
+
|
|
57
|
+
if pull:
|
|
58
|
+
res_pull = adb('pull', remote_path, check=True, delay=delay)
|
|
59
|
+
return res_pull
|
|
60
|
+
|
|
61
|
+
return res_capture
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def screenrecord(self, filename=None, pull=False, duration=10, delay=0):
|
|
65
|
+
"""
|
|
66
|
+
Record the device screen to an MP4 file.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
filename (str | None): Name of the video file on the device.
|
|
70
|
+
Defaults to ``'recording.mp4'``. The file is saved under
|
|
71
|
+
``/sdcard/``.
|
|
72
|
+
pull (bool): If ``True``, copy the recording to the current local
|
|
73
|
+
directory after recording it. Defaults to ``False``.
|
|
74
|
+
duration (int | float): Maximum recording duration in seconds.
|
|
75
|
+
Defaults to ``10``.
|
|
76
|
+
delay (int | float): Seconds to wait after each ADB command.
|
|
77
|
+
Defaults to ``0``.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
subprocess.CompletedProcess: The result of ``adb pull`` when
|
|
81
|
+
``pull`` is ``True``; otherwise, the result of the recording
|
|
82
|
+
command.
|
|
83
|
+
str: ``'File format unsupported.'`` when ``filename`` does not
|
|
84
|
+
use the ``.mp4`` extension.
|
|
85
|
+
"""
|
|
86
|
+
if not filename:
|
|
87
|
+
filename = 'recording.mp4'
|
|
88
|
+
|
|
89
|
+
name, file_format = filename.rsplit('.', 1)
|
|
90
|
+
if file_format != 'mp4':
|
|
91
|
+
return "File format unsupported."
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
remote_path = f'/sdcard/{filename}'
|
|
95
|
+
|
|
96
|
+
res_record = adb(
|
|
97
|
+
'shell', 'screenrecord', '--time-limit', str(duration), remote_path,
|
|
98
|
+
delay=delay
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if pull:
|
|
102
|
+
res_pull = adb('pull', remote_path, check=True, delay=delay)
|
|
103
|
+
return res_pull
|
|
104
|
+
|
|
105
|
+
return res_record
|
|
106
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import time, subprocess
|
|
2
|
+
from .exceptions import AdbCommandError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def adb(*args, **kwargs):
|
|
6
|
+
"""Run an ADB command and return its completed-process result.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
*args (str): Arguments passed to the ``adb`` executable.
|
|
10
|
+
**kwargs: Additional keyword arguments passed to
|
|
11
|
+
:func:`subprocess.run`. Output is captured and decoded as text by
|
|
12
|
+
default.
|
|
13
|
+
delay (int | float): Seconds to wait after the command completes.
|
|
14
|
+
Defaults to ``0``.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
subprocess.CompletedProcess: The result of the ADB command.
|
|
18
|
+
"""
|
|
19
|
+
kwargs.setdefault('capture_output', True)
|
|
20
|
+
kwargs.setdefault('text', True)
|
|
21
|
+
delay_value = kwargs.pop('delay', 0)
|
|
22
|
+
|
|
23
|
+
result = subprocess.run(
|
|
24
|
+
['adb', *args],
|
|
25
|
+
**kwargs
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
if result.returncode != 0:
|
|
29
|
+
raise AdbCommandError(result.stderr)
|
|
30
|
+
time.sleep(delay_value)
|
|
31
|
+
return result
|
|
32
|
+
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jaydroid
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A beginner-friendly Python wrapper for simulating Android actions via ADB (tap, swipe, power on/off).
|
|
5
|
+
Author: Johnston Kweku Abubakar
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/johnston-kweku/jaydroid
|
|
8
|
+
Project-URL: Issues, https://github.com/johnston-kweku/jaydroid/issues
|
|
9
|
+
Keywords: adb,android,automation,adb-wrapper
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# jaydroid
|
|
21
|
+
|
|
22
|
+
`jaydroid` is a small Python wrapper around [Android Debug Bridge (ADB)](https://developer.android.com/tools/adb). It provides helpers for connecting to an Android device, reading display dimensions and device info, sending button input, capturing the screen, and performing swipe and tap gestures.
|
|
23
|
+
|
|
24
|
+
This is my first Python package, built as a way to learn how to design, structure, and publish one from scratch. I'm documenting the build process — bugs, refactors, and design decisions — as a video series. Expect it to keep growing.
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
- Python 3.8 or newer
|
|
29
|
+
- ADB installed and available on your `PATH`
|
|
30
|
+
- An Android device or emulator connected to ADB
|
|
31
|
+
- USB debugging enabled when using a physical device
|
|
32
|
+
|
|
33
|
+
Check the connection with:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
adb devices
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install jaydroid
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For local development, from the project directory:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python -m pip install -e .
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
`jaydroid` exposes ready-to-use instances, so there's no setup required beyond connecting your device:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from jaydroid import device, button, swipe, tap
|
|
57
|
+
|
|
58
|
+
device.connect()
|
|
59
|
+
|
|
60
|
+
button.wake(delay=1)
|
|
61
|
+
swipe.swipe_up()
|
|
62
|
+
tap.tap(360, 640)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`device.connect()` checks that a device or emulator is actually reachable over ADB and loads its display dimensions. It's the one required step before using anything that depends on screen size (like the directional swipe helpers).
|
|
66
|
+
|
|
67
|
+
## Delays
|
|
68
|
+
|
|
69
|
+
Every `Button`, `Screen`, `Swipe`, and `Tap` method accepts an optional `delay` argument, in seconds (default `0`). The delay runs *after* the action completes successfully — "do this, then wait" — so it's useful for giving the device a moment to catch up before your next command fires:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
button.wake(delay=1)
|
|
73
|
+
swipe.swipe_up(delay=0.5)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
If a command fails, the delay is skipped — you'll see the error immediately rather than waiting first.
|
|
77
|
+
|
|
78
|
+
## Gestures
|
|
79
|
+
|
|
80
|
+
### Swipe
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from jaydroid import swipe
|
|
84
|
+
|
|
85
|
+
swipe.swipe_up()
|
|
86
|
+
swipe.swipe_down()
|
|
87
|
+
swipe.swipe_left()
|
|
88
|
+
swipe.swipe_right()
|
|
89
|
+
swipe.unlock() # alias for swipe_up()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Directional swipes are calculated as a percentage of the connected device's display size, so they scale across different screens rather than relying on hardcoded pixels:
|
|
93
|
+
|
|
94
|
+
- Left and right: 10% to 90% of display width, at 50% of height
|
|
95
|
+
- Up and down: 50% of display width, from 80% to 10% of height
|
|
96
|
+
|
|
97
|
+
For custom coordinates, use `swipe.swipe(x1, y1, x2, y2)`:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
swipe.swipe(600, 640, 100, 640, delay=0.5)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Tap
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from jaydroid import tap
|
|
107
|
+
|
|
108
|
+
tap.tap(360, 640)
|
|
109
|
+
tap.double_tap(360, 640)
|
|
110
|
+
tap.longpress(360, 640, duration=1000)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Buttons
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from jaydroid import button
|
|
117
|
+
|
|
118
|
+
button.power()
|
|
119
|
+
button.wake()
|
|
120
|
+
button.sleep()
|
|
121
|
+
button.home()
|
|
122
|
+
button.back()
|
|
123
|
+
button.recent_apps()
|
|
124
|
+
button.menu()
|
|
125
|
+
button.volume_up()
|
|
126
|
+
button.volume_down()
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Each accepts `delay=0`.
|
|
130
|
+
|
|
131
|
+
## Screen
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from jaydroid.screen import Screen
|
|
135
|
+
|
|
136
|
+
screen = Screen()
|
|
137
|
+
screen.capture()
|
|
138
|
+
screen.screenshot(filename='screen.png', pull=True)
|
|
139
|
+
screen.screenrecord(filename='recording.mp4', pull=True, duration=10)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`screenshot()` and `screenrecord()` save to `/sdcard/` on the device by default, and can optionally pull the file to your local directory with `pull=True`.
|
|
143
|
+
|
|
144
|
+
## Device information
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from jaydroid import device
|
|
148
|
+
|
|
149
|
+
device.connect()
|
|
150
|
+
print(device.width, device.height)
|
|
151
|
+
print(device.is_connected())
|
|
152
|
+
print(device.get_android_version())
|
|
153
|
+
print(device.battery_info())
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- `width` and `height` are only available after `connect()` — accessing them beforehand raises `DeviceNotConnectedError`.
|
|
157
|
+
- `is_connected()`, `get_android_version()`, and `battery_info()` check the ADB connection live each time they're called, so they don't require `connect()` to have been run first.
|
|
158
|
+
- `battery_info()` returns a dictionary parsed from `adb shell dumpsys battery`. Core fields (`level`, `status`, `health`, `voltage`, `temperature`, `technology`, and the `*_powered` flags) are consistently present across devices, but the full set of keys can vary by manufacturer, since some OEMs include additional proprietary fields.
|
|
159
|
+
|
|
160
|
+
## Errors
|
|
161
|
+
|
|
162
|
+
`jaydroid` raises specific exceptions instead of generic ones, so you can catch exactly what went wrong:
|
|
163
|
+
|
|
164
|
+
- `DeviceNotFoundError` — raised by `connect()` when no device or emulator is reachable over ADB.
|
|
165
|
+
- `DeviceNotConnectedError` — raised when accessing `width`/`height` before `connect()` has been called.
|
|
166
|
+
- `AdbCommandError` — raised when an ADB command itself fails (e.g. the device disconnects mid-session). Includes ADB's own error output.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from jaydroid import device
|
|
170
|
+
from jaydroid.exceptions import DeviceNotFoundError
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
device.connect()
|
|
174
|
+
except DeviceNotFoundError:
|
|
175
|
+
print("No device connected — plug one in and try again.")
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Roadmap
|
|
179
|
+
|
|
180
|
+
Planned additions include WiFi status, installed app listing, and more device/system information. This package is under active development.
|
|
181
|
+
|
|
182
|
+
## Author
|
|
183
|
+
|
|
184
|
+
Built by Johnston Kweku Abubakar ([@johnston-kweku](https://github.com/johnston-kweku)) — this is my first Python package, built while learning to design, structure, and publish one from scratch.
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
This project is licensed under the MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/jaydroid/__init__.py
|
|
5
|
+
src/jaydroid/button.py
|
|
6
|
+
src/jaydroid/core.py
|
|
7
|
+
src/jaydroid/device.py
|
|
8
|
+
src/jaydroid/exceptions.py
|
|
9
|
+
src/jaydroid/gesture.py
|
|
10
|
+
src/jaydroid/screen.py
|
|
11
|
+
src/jaydroid/utils.py
|
|
12
|
+
src/jaydroid.egg-info/PKG-INFO
|
|
13
|
+
src/jaydroid.egg-info/SOURCES.txt
|
|
14
|
+
src/jaydroid.egg-info/dependency_links.txt
|
|
15
|
+
src/jaydroid.egg-info/top_level.txt
|
|
16
|
+
tests/test_core.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
jaydroid
|