droidrun-core 0.1.0

Android device automation core library — DeviceDriver trait, Portal integration, UI state pipeline
Documentation

droidrun-rs

Pure Rust implementation of Android device automation — a rewrite of the droidrun Python framework's android-driver and portal-controller layers.

Fully async with tokio. Zero Python dependencies.

Features

  • Async ADB client — Native ADB wire protocol + sync protocol over TCP, no adb CLI dependency
  • 70+ ADB operations — Shell, file sync (push/pull/stat/list), port forwarding, reverse forwarding, app management, device info, screen control, logcat streaming
  • Portal integration — Dual-mode communication (TCP + ContentProvider fallback)
  • UI state pipeline — Accessibility tree filtering, formatting, and element resolution
  • Recording driver — Proxy wrapper that logs all actions as JSON
  • CLI tool — Full-featured command-line interface for device automation
  • 120+ tests — Unit tests (83) + integration tests (37) + doc tests (2)

Installation

Prerequisites

  • Rust 1.85.0+
  • ADB server running (adb start-server)
  • Android device/emulator with DroidRun Portal installed

Build from source

git clone https://github.com/Sikrid25/droidrun-rs.git
cd droidrun-rs
cargo build --release

The CLI binary will be at target/release/droidrun.

Quick Start

CLI

# List connected devices
droidrun devices

# Check device + Portal health
droidrun doctor

# Take a screenshot
droidrun screenshot screen.png

# Tap at coordinates
droidrun tap 540 1200

# Type text
droidrun type "hello world" --clear

# Get UI state (formatted)
droidrun state

# Get UI state (raw JSON)
droidrun state --json

# Swipe down
droidrun swipe 540 400 540 1600 --duration 300

# Open an app
droidrun open com.example.app

# Run a shell command
droidrun shell getprop ro.build.version.sdk

As a library

Cargo.toml:

[dependencies]
droidrun-core = { path = "crates/droidrun-core" }
tokio = { version = "1", features = ["full"] }

Basic usage:

use droidrun_core::driver::android::AndroidDriver;
use droidrun_core::driver::DeviceDriver;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Connect to first available device (TCP mode)
    let mut driver = AndroidDriver::new(None, true);
    driver.connect().await?;

    // Take screenshot
    let png = driver.screenshot(true).await?;
    std::fs::write("screen.png", &png)?;

    // Tap
    driver.tap(540, 1200).await?;

    // Type text
    driver.input_text("hello from rust!", false).await?;

    // Get UI tree
    let state = driver.get_ui_tree().await?;
    println!("{}", serde_json::to_string_pretty(&state)?);

    Ok(())
}

Using the state provider pipeline:

use droidrun_core::{AndroidDriver, DeviceDriver};
use droidrun_core::{AndroidStateProvider, ConciseFilter, IndexedFormatter};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut driver = AndroidDriver::new(None, true);
    driver.connect().await?;

    let provider = AndroidStateProvider::new(
        ConciseFilter,
        IndexedFormatter,
        false, // use absolute coordinates
    );

    let state = provider.get_state(&driver).await?;

    println!("Screen: {}x{}", state.screen.width, state.screen.height);
    println!("Elements: {}", state.elements.len());
    println!("\n{}", state.formatted_text);

    // Find element by index
    if let Some(elem) = state.get_element(1) {
        println!("Element 1: {} '{}'", elem.class_name, elem.text);
    }

    Ok(())
}

Low-level ADB operations:

use droidrun_adb::AdbServer;
use std::path::Path;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let server = AdbServer::default();
    let device = server.device().await?;

    // Shell command with exit code
    let result = device.shell2("echo hello").await?;
    println!("exit_code={}, stdout={}", result.exit_code, result.stdout.trim());

    // System properties
    let model = device.prop_model().await?;
    println!("Model: {model}");

    // File sync protocol (push/pull/stat)
    device.push_bytes(b"hello world\n", "/data/local/tmp/test.txt").await?;
    let stat = device.stat("/data/local/tmp/test.txt").await?;
    println!("Size: {} bytes", stat.size);
    let data = device.pull_bytes("/data/local/tmp/test.txt").await?;
    assert_eq!(&data, b"hello world\n");

    // List directory
    let entries = device.list_dir("/system").await?;
    for e in entries.iter().take(5) {
        println!("  {} ({} bytes)", e.name, e.size);
    }

    // App management
    let current = device.app_current().await?;
    println!("Foreground: {}", current.package);

    // Screen info
    let size = device.window_size().await?;
    println!("Screen: {size}");

    // Port forwarding (forward + reverse)
    let port = device.forward(0, 8080).await?;
    device.reverse(9999, 8888).await?;
    device.forward_remove(port).await?;
    device.reverse_remove(9999).await?;

    Ok(())
}

Recording driver:

use droidrun_core::{AndroidDriver, RecordingDriver, DeviceDriver};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut inner = AndroidDriver::new(None, true);
    inner.connect().await?;

    let mut recorder = RecordingDriver::new(inner);

    // All actions are recorded
    recorder.tap(540, 1200).await?;
    recorder.input_text("hello", false).await?;
    recorder.press_key(4).await?; // Back

    // Get recorded actions as JSON
    let actions = recorder.actions();
    println!("{}", serde_json::to_string_pretty(&actions)?);

    Ok(())
}

Architecture

┌──────────────┐
│ droidrun-cli │  CLI tool (clap subcommands)
└──────┬───────┘
       │
┌──────▼────────┐
│ droidrun-core │  DeviceDriver trait, Portal client, UI pipeline
└──────┬────────┘
       │
┌──────▼───────┐
│ droidrun-adb │  Async ADB wire protocol over TCP
└──────────────┘

Crates

Crate Description
droidrun-adb Low-level async ADB client. Implements ADB wire protocol + sync protocol over TCP using tokio. 70+ methods covering: device discovery, shell (with exit codes), file sync (push/pull/stat/list), port forwarding & reverse forwarding, screenshots, input control, app management, device properties, screen info, and logcat streaming.
droidrun-core High-level automation framework. Defines the DeviceDriver trait, Portal APK management, dual-mode Portal communication (TCP + ContentProvider), and the UI state processing pipeline (filter → format → UIState).
droidrun-cli Command-line tool built with clap. Exposes all framework capabilities as subcommands.

Portal Communication

DroidRun Portal is an Android APK that provides accessibility tree access, keyboard input, and screenshot capabilities. The framework communicates with Portal via two transport modes:

Mode How Speed Use case
TCP HTTP requests to Portal's embedded server (port 8080, ADB-forwarded) Fast Default, preferred
ContentProvider adb shell content query/insert commands Slower Automatic fallback

The client automatically falls back from TCP to ContentProvider on failure.

UI State Pipeline

Raw accessibility tree (JSON from Portal)
            ↓
    TreeFilter (ConciseFilter)     — removes off-screen & tiny elements
            ↓
    TreeFormatter (IndexedFormatter) — assigns indices, formats text
            ↓
    UIState {
        elements:       Vec<Element>,      // flattened with indices
        formatted_text: String,            // human-readable output
        phone_state:    PhoneState,        // current app, keyboard, focus
        screen:         ScreenDimensions,  // width x height
    }

Both TreeFilter and TreeFormatter are traits — implement your own for custom processing.

CLI Reference

droidrun [OPTIONS] <COMMAND>

Options:
  -s, --serial <SERIAL>  Device serial number
      --tcp              Use TCP mode (default: true)
  -v, --verbose          Enable debug logging

Commands:
  devices      List connected devices
  setup        Install & configure Portal on device
  doctor       Check device + Portal health
  screenshot   Take a screenshot [default: screenshot.png]
  tap          Tap at coordinates (x, y)
  swipe        Swipe between points (x1, y1, x2, y2) [--duration ms]
  type         Type text into focused field [--clear]
  key          Send key event (3=Home, 4=Back, 66=Enter)
  state        Get UI state [--json]
  apps         List installed apps [--system]
  open         Start an app by package name [--activity name]
  shell        Run a shell command on device

Examples

14 runnable examples across both library crates:

# droidrun-adb examples (8)
cargo run -p droidrun-adb --example basic
cargo run -p droidrun-adb --example screenshot
cargo run -p droidrun-adb --example port_forward
cargo run -p droidrun-adb --example input_control
cargo run -p droidrun-adb --example file_transfer
cargo run -p droidrun-adb --example app_management
cargo run -p droidrun-adb --example reverse_forward
cargo run -p droidrun-adb --example device_info

# droidrun-core examples (6)
cargo run -p droidrun-core --example driver_basics
cargo run -p droidrun-core --example state_provider
cargo run -p droidrun-core --example recording
cargo run -p droidrun-core --example element_search
cargo run -p droidrun-core --example portal_setup
cargo run -p droidrun-core --example app_automation

Testing

122 tests total (83 unit + 37 integration + 2 doc):

# All tests (needs device connected)
cargo test

# Unit tests only (no device needed)
cargo test --lib

# Integration tests
cargo test -p droidrun-adb --test integration -- --nocapture   # 27 tests
cargo test -p droidrun-core --test integration -- --nocapture  # 10 tests

# Skip device tests
SKIP_DEVICE_TESTS=1 cargo test

Test Requirements

  • ADB server running
  • Android emulator or device connected
  • DroidRun Portal APK installed with accessibility service enabled (for droidrun-core tests)

Dependencies

Crate Purpose
tokio Async runtime
reqwest HTTP client for Portal TCP
serde JSON serialization
clap CLI argument parsing
thiserror Error derive macros
tracing Structured logging
async-trait Async trait support
base64 Encoding for keyboard/screenshots

License

MIT