How the UAD-ng ADB Module Architecture Works: Type-Safe Builder Pattern Explained

The UAD-ng ADB module implements a thin, type-state wrapper around the Android Debug Bridge CLI, ensuring compile-time validation of command sequences through a consuming builder pattern.

The Universal Android Debloater Next Generation (UAD-ng) interacts with Android devices through a carefully architected abstraction layer. According to the Universal-Debloater-Alliance/universal-android-debloater-next-generation source code, the ADB module architecture follows a low-overhead design that maps each Rust method directly to a single adb CLI command without hidden magic or custom sub-commands.

Core Design Philosophy

The architecture prioritizes thin wrappers over heavy abstraction. Every method in the module translates to exactly one CLI argument, maintaining transparency and debuggability. The design employs strong typing through types like PackageId, PmListPacksFlag, and UserInfo to encode invariants that would otherwise be stringly-typed.

This approach delivers three key benefits:

  • Compile-time safety via the type-state pattern prevents illegal command compositions
  • Zero-overhead abstraction with no hidden state or fallback to raw Command usage
  • Direct traceability where every Rust method maps 1-to-1 to an adb executable invocation

The ACommand Entry Point

The core struct resides in crates/uad-core/src/adb.rs at lines 82-87:

pub struct ACommand(std::process::Command);

The new() constructor spawns a std::process::Command targeting the adb executable. This struct serves as the foundation for all higher-level builders, guaranteeing that every method chain ultimately resolves to a valid adb invocation. Unlike generic process wrappers, ACommand enforces device specificity and context validation at the type level.

Builder Hierarchy and Type-State Pattern

The module implements a consuming builder hierarchy where each stage consumes the previous struct, preventing illegal state transitions at compile time.

ACommand::shell() and ShellCommand

Defined at lines 90-101 in adb.rs, the shell() method initiates an adb shell … session for a specific device:

pub fn shell(self, serial: impl AsRef<str>) -> ShellCommand

This returns a ShellCommand struct (lines 124-162), which wraps the shell context and exposes higher-level sub-commands including pm(), getprop(), reboot(), and a generic raw executor. Once you transition to ShellCommand, you cannot accidentally invoke non-shell ADB operations—the compiler enforces this boundary.

PmCommand for Package Manager Operations

For Android Package Manager operations, the ShellCommand.pm() method returns a PmCommand instance (lines 237-340). This specialized builder handles pm sub-commands such as listing packages, clearing data, and managing components:

pub fn pm(self) -> PmCommand

The type-state pattern ensures you cannot call pm() after issuing a non-shell command, eliminating entire classes of runtime errors through Rust's ownership system.

Command Execution Flow

All builders eventually delegate to the private run() method located at lines 184-215 in crates/uad-core/src/adb.rs:

fn run(self) -> Result<String, String> {
    let mut cmd = self.0;
    info!("Ran command: adb {}", ...);
    match cmd.output() {
        Err(e) => Err("Cannot run ADB, likely not found".into()),
        Ok(o) => {
            let stdout = to_trimmed_utf8(&o.stdout);
            if o.status.success() { Ok(stdout) } else { Err(stderr_or_stdout) }
        }
    }
}

This method handles UTF-8 conversion through the to_trimmed_utf8 helper, error bubbling, and logs the exact command line for debugging. The execution layer remains self-contained, with utility helpers like PackageId validation living within the same module to validate Android package identifiers against official manifest rules.

Integration Across Crates

Other crates consume the ADB API via re-exports from uad_core, maintaining a minimal public surface area.

CLI Integration (uad-cli): The CLI crate drives the ADB layer for commands like list-packages, reboot, and devices. In crates/uad-cli/src/commands.rs (lines 134-174), you'll find invocations such as:

ACommand::new().shell(serial).pm().list_packages_sys(...)

GUI Integration (uad-gui): The graphical interface displays ADB version and device info using the same abstraction. The about.rs file (line 108) retrieves version information through the typed API.

Sync Logic (sync.rs): Background synchronization employs ACommand to clear packages and fetch user lists, demonstrating the module's thread-safe design.

All callers import the type from the core crate:

use uad_core::adb::ACommand;

Practical Code Examples

List Attached Devices

use uad_core::adb::ACommand;

fn list_devices() -> Result<Vec<(String, String)>, String> {
    ACommand::new().devices()
}

Retrieve ADB Version

let version = ACommand::new().version()?; // returns the raw version string
println!("ADB version: {version}");

Query Device Properties

let serial = "emulator-5554";
let prop = ACommand::new()
    .shell(serial)
    .getprop("ro.build.version.release")?;
println!("Android version: {prop}");

List System Packages for Specific Users

let pkgs = ACommand::new()
    .shell("0123456789ABCDEF")
    .pm()
    .list_packages_sys(Some(PmListPacksFlag::OnlyEnabled), Some(0))?;
for p in pkgs {
    println!("Package: {p}");
}

Reboot a Device

ACommand::new()
    .shell("my-device-serial")
    .reboot()?; // triggers `adb -s my-device-serial reboot`

Summary

  • The UAD-ng ADB module architecture resides in crates/uad-core/src/adb.rs and implements a type-safe wrapper around the adb CLI.
  • ACommand serves as the entry point, with consuming builders (ShellCommand, PmCommand) enforcing valid command sequences at compile time.
  • The type-state pattern prevents illegal transitions, such as calling package manager methods outside shell contexts.
  • Command execution flows through a centralized run() method that handles UTF-8 conversion, error propagation, and logging.
  • The module integrates across CLI, GUI, and sync crates through re-exports in uad_core, maintaining a minimal, well-documented public API.

Frequently Asked Questions

What is the type-state pattern in UAD-ng's ADB module?

The type-state pattern uses Rust's ownership system to enforce valid command sequences at compile time. Each builder method consumes the previous struct (e.g., ACommand → ShellCommand → PmCommand), making it impossible to call methods in invalid orders. For example, you cannot invoke pm() without first calling shell() because the compiler tracks the state transitions through distinct types.

How does UAD-ng handle ADB command execution errors?

The private run() method in crates/uad-core/src/adb.rs (lines 184-215) handles all execution errors. It captures std::process::Command output, converts raw bytes to trimmed UTF-8 using to_trimmed_utf8, and returns Result<String, String>. If the adb executable is missing, it returns "Cannot run ADB, likely not found". Successful executions return stdout; failures return stderr or stdout depending on availability.

Where is the core ADB implementation located in the repository?

The core implementation lives in crates/uad-core/src/adb.rs. This file contains the ACommand struct, ShellCommand and PmCommand builders, the run() execution method, and utility helpers like to_trimmed_utf8 and PackageId. Integration points exist in crates/uad-cli/src/commands.rs for CLI operations and crates/uad-gui/src/views/about.rs for GUI functionality.

How does the ADB module ensure type safety for Android packages?

The module uses the PackageId type to validate Android package identifiers according to official manifest rules, preventing malformed package names from reaching the device. Additionally, flags like PmListPacksFlag encode valid package manager options as enumerated types rather than raw strings, eliminating typo-based errors and invalid flag combinations at compile time.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →