VoidRemote
A Python interface to Android Debug Bridge (ADB).
VoidRemote wraps adb so you write Python instead of shell commands. It
discovers devices, manages wireless pairing and connections, and gives you
a typed Device object for input, file transfer, shell access, package
management, screen capture, and monitoring.
from voidremote import VoidRemote
client = VoidRemote()
client.start()
device = client.devices().first()
device.tap(500, 800)
device.text("hello world")
device.screenshot("screen.png")
No adb shell input tap ... strings. No manually parsing adb devices -l
output. No subprocess plumbing in your code.
Who this is for
- Python developers automating Android devices
- Test engineers writing device-driven test suites
- CI/CD pipelines that need to install, launch, and verify apps
- Scripts and tools that need device telemetry (battery, storage, CPU)
- Applications built on top of ADB — VoidRemote's own CLI and desktop GUI are themselves built on this SDK (see Also included)
Installation
pip install voidremote
Requires Python 3.12+ and the adb binary on your PATH (install via
Android Platform Tools).
VoidRemote shells out to your existing adb — it does not bundle or
replace it.
The base install is SDK-only, with no CLI or GUI dependencies pulled in:
pip install voidremote # SDK only
pip install voidremote[cli] # + command-line tool
pip install voidremote[gui] # + desktop app
pip install voidremote[full] # SDK + CLI + GUI
Quick start
from voidremote import VoidRemote
client = VoidRemote()
client.start() # verify adb, start the adb server
devices = client.devices() # discover devices already visible to adb
device = devices.first()
device.tap(500, 800)
device.swipe(500, 1600, 500, 400)
device.text("hello world")
device.key_event(4) # KEYCODE_BACK
device.push("app-release.apk", "/sdcard/app.apk")
device.install("app-release.apk")
output = device.shell("pm list packages -3")
device.screenshot("screen.png")
Connecting a device that isn't already known to adb:
from voidremote import VoidRemote
client = VoidRemote()
client.start()
# Wireless debugging, paired by the 6-digit code shown on-device
device = client.pair_and_connect(host="192.168.1.42", port=37831, code="482913")
# Already paired, just needs a TCP connection
device = client.connect("192.168.1.42")
Every call above runs one real adb command under the hood — VoidRemote
just gives it a name, a type, and a place to raise a real exception.
Philosophy
VoidRemote exists to remove one layer of indirection: the one between what
you mean and the adb command line syntax for it.
| Instead of shelling out to... | You write |
|---|---|
adb shell input tap 500 800 |
device.tap(500, 800) |
adb shell input text "hello" |
device.text("hello") |
adb push app.apk /sdcard/app.apk |
device.push("app.apk", "/sdcard/app.apk") |
adb install -r app.apk |
device.install("app.apk") |
adb shell pm list packages -3 |
device.list_packages() |
adb pair 192.168.1.42:37831 482913 |
client.pair(host, port, code) |
parsing adb devices -l output by hand |
client.devices() |
parsing dumpsys battery output by hand |
device.battery_level |
Nothing here is magic — every method is a thin, validated wrapper around
one adb invocation. VoidRemote doesn't try to be smarter than adb; it
tries to be a smaller, typed surface on top of it, with input validated
against command injection before it ever reaches a shell.
Architecture
Your application
│
▼
voidremote (SDK) ── Device, VoidRemote, PairingSession
│
▼
adb executable ── the real Android Debug Bridge binary
│
▼
Android device ── over USB or Wireless Debugging
Internally, the SDK is layered:
voidremote.api stable public surface (this is what you import)
│
voidremote.controllers composition root — wires services together
│
voidremote.services device discovery, input, monitoring
│
voidremote.adb subprocess execution, output parsing
│
adb binary
Only voidremote.api (re-exported at the top level, from voidremote import
...) is a stable, versioned surface. Everything below it — adb,
services, controllers, models — is an internal implementation detail
that can change between minor versions without notice.
API design
VoidRemote — the client
from voidremote import VoidRemote
client = VoidRemote()
client.start()
client.devices() # -> DeviceList
client.device(serial) # -> Device
client.connect(host, port=5555) # -> Device
client.pair(host, port, code) # -> PairingSession
client.pair_and_connect(host, port, code) # -> Device
client.auto_reconnect() # -> DeviceList
Also usable as a context manager, which starts on entry and stops background monitoring on exit:
with VoidRemote() as client:
for device in client.devices():
print(device.name, device.battery_level)
Device — everything scoped to one device
device.tap(x, y)
device.swipe(x1, y1, x2, y2)
device.text("hello")
device.key_event(keycode)
device.shell("getprop ro.build.version.release")
device.push(local, remote)
device.pull(remote, local)
device.list_dir("/sdcard")
device.install(apk_path)
device.uninstall(package)
device.list_packages()
device.is_installed(package)
device.screenshot(output_path)
device.screenrecord()
device.reboot()
device.monitor(interval=2.0, callback=on_snapshot)
Input methods return self, so they chain:
device.tap(500, 800).text("hello").key_event(66) # tap, type, press Enter
DeviceList — what client.devices() returns
devices = client.devices()
devices.first() # -> Device, raises NoDevicesError if empty
devices.get(serial) # -> Device, raises DeviceNotFoundError
devices.online() # -> DeviceList filtered to online devices
len(devices)
for device in devices: ...
PairingSession — wireless debugging setup
session = client.pair(host="192.168.1.42", port=37831, code="482913")
session.pair() # perform the handshake
device = session.connect() # then connect on the regular ADB port
Exceptions
Everything VoidRemote raises is a VoidRemoteError:
from voidremote import VoidRemoteError, DeviceNotFoundError, NoDevicesError, PairingError
try:
device = client.devices().first()
except NoDevicesError:
print("No devices connected.")
except VoidRemoteError as exc:
print(f"Something went wrong: {exc}")
AdbNotAvailableError, AdbTimeoutError, AdbCommandError,
DeviceNotFoundError, NoDevicesError, PairingError, ConnectionError,
and InvalidArgumentError all subclass it.
Async
AsyncVoidRemote mirrors the sync API as coroutines. adb is a
subprocess-based CLI, not a socket protocol VoidRemote speaks directly, so
this runs the same synchronous code via asyncio.to_thread rather than
native async I/O — which is enough to keep your event loop unblocked, and
enough to run many devices concurrently with asyncio.gather:
import asyncio
from voidremote import AsyncVoidRemote
async def main():
async with AsyncVoidRemote() as client:
devices = await client.devices()
await asyncio.gather(*(d.tap(500, 800) for d in devices))
asyncio.run(main())
Examples
Discover and print info for every connected device:
from voidremote import VoidRemote
with VoidRemote() as client:
for device in client.devices():
print(f"{device.name} {device.android_version} {device.battery_level}%")
Install an APK on every connected device:
from voidremote import VoidRemote
with VoidRemote() as client:
for device in client.devices().online():
device.install("app-release.apk")
Take a screenshot from a specific device:
from voidremote import VoidRemote
client = VoidRemote()
client.start()
client.device("192.168.1.42:5555").screenshot("shot.png")
Run a shell command and use the output:
device = client.devices().first()
version = device.shell("getprop ro.build.version.release")
print(f"Android {version}")
Poll battery and memory in the background:
def on_snapshot(snapshot):
print(f"CPU {snapshot.cpu_usage:.0f}% RAM {snapshot.ram_usage_percent:.0f}% "
f"Battery {snapshot.battery_level}%")
device = client.devices().first()
device.monitor(interval=2.0, callback=on_snapshot)
Handle errors explicitly:
from voidremote import VoidRemote, NoDevicesError, AdbNotAvailableError
try:
client = VoidRemote()
client.start()
device = client.devices().first()
except AdbNotAvailableError:
print("adb isn't installed or isn't on PATH.")
except NoDevicesError:
print("adb is running, but no device is connected.")
Security
Every device path, package name, host, port, and pairing code is validated
before use, and shell arguments are checked against a small blocklist of
injection characters (; & | \ $ < > `) and rejected outright rather than
escaped-and-hoped. See voidremote.utils.security — it's a small module,
worth reading if you're deciding whether to trust this with untrusted
input.
Also included
The voidremote repository also ships two applications built entirely on
top of this SDK — neither adds anything to the SDK's public API, both are
optional installs.
CLI (pip install voidremote[cli]) — a voidremote command covering
device discovery, pairing, input, file transfer, package management,
shell access, and monitoring, with --json output for scripting. See
docs/CLI.md.
Desktop GUI (pip install voidremote[gui]) — a PySide6 application
(voidremote-gui) with a device dashboard, embedded shell, file manager,
and live monitoring graphs. See docs/GUI.md.
Further reading: docs/API.md for the full API reference, docs/ARCHITECTURE.md for how the layers fit together, CONTRIBUTING.md to work on VoidRemote itself.
Requirements
- Python 3.12 or later
adb(Android SDK Platform Tools) onPATH- An Android device with USB debugging or Wireless Debugging enabled
Tested on Linux, macOS, and Windows.
License
MIT — see LICENSE.
Author
V0IDNETWORK — an ongoing, open research effort to document, rigorously and accurately, how the modern Internet's circumvention and surveillance technologies actually work at the protocol level, in support of a more open and resilient Internet.
GitHub · Website · LinkedIn · Instagram · YouTube · TryHackMe · Medium · Telegram · ilianothingg@gmail.com
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file voidremote-2.0.1.tar.gz.
File metadata
- Download URL: voidremote-2.0.1.tar.gz
- Upload date:
- Size: 79.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b64dfecba499fae2019f6589cf7c6432788ee0eaf7343916a95d637014508611
|
|
| MD5 |
ecb29fdebdb92d5dd8371346e9af5108
|
|
| BLAKE2b-256 |
28ff3307a2ef7c06331e733332829fb3a4b9e09ed049a604d9b5c47f4c6e1986
|
File details
Details for the file voidremote-2.0.1-py3-none-any.whl.
File metadata
- Download URL: voidremote-2.0.1-py3-none-any.whl
- Upload date:
- Size: 95.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ab06b8024e454931aedd7b9fc4d9448b84c76352b59b70f21bc1d9a956c4b1f0
|
|
| MD5 |
82768873d5a996f39556ca0f16edf2ee
|
|
| BLAKE2b-256 |
827ff57d2ee144c6ada8b6d35d8d1ce7cc7a28fabda937a643b4bd0b81129759
|