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 ADDED
File without changes
@@ -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,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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,8 @@
1
+ class DeviceNotFoundError(Exception):
2
+ pass
3
+
4
+ class DeviceNotConnectedError(Exception):
5
+ pass
6
+
7
+ class AdbCommandError(Exception):
8
+ pass
@@ -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
+ jaydroid
@@ -0,0 +1,6 @@
1
+ from jaydroid.core import device, swipe, button, tap
2
+
3
+ device.connect()
4
+
5
+
6
+ print(device.battery_info()['temperature'])