How the ADB Command Wrapper Architecture Uses the Type-State Pattern in UADNG

The Universal Android Debloater Next Generation (UADNG) implements a zero-cost, compile-time safe ADB command builder in crates/uad-core/src/adb.rs using the type-state pattern, where consuming methods like shell() and pm() transform builders through distinct types to enforce valid Android Debug Bridge command sequences.

The Universal Android Debloater Next Generation project provides a Rust-based tool for managing Android device packages through ADB. Instead of constructing raw command strings that risk runtime errors, the ADB command wrapper architecture leverages Rust's ownership system to enforce correct command ordering at compile time. This design lives primarily in the uad-core crate and provides a type-safe API used by both the GUI and CLI components.

Overview of the Type-State Pattern in UADNG

The type-state pattern in UADNG models ADB commands as a linear progression of builder objects. Each struct represents a valid state in the command construction process, and methods consume the current state to produce the next.

This approach prevents illegal sequences—such as calling package manager methods before establishing a shell context—by making them unrepresentable in the type system. The implementation adds virtually no runtime overhead, as the builders are thin wrappers around std::process::Command.

The Builder Hierarchy: From ACommand to PmCommand

The architecture defines three primary builder types in crates/uad-core/src/adb.rs, each wrapping std::process::Command and adding context-specific methods.

ACommand: The Entry Point

ACommand serves as the root builder, initialized via ACommand::new() (lines 86-88). It represents the base adb invocation and provides methods to branch into specific sub-commands:

  • shell(device_serial) - Returns a ShellCommand after appending the -s flag (if provided) and shell argument (lines 94-101)
  • devices() - Lists connected devices
  • version() - Retrieves ADB version information (lines 130-138)

ShellCommand: Device Shell Context

Created by calling shell() on an ACommand, this builder represents an adb shell invocation. It wraps the parent command and enables device-specific operations:

  • pm() - Consumes the shell context and returns a PmCommand (lines 32-35)
  • getprop() - Accesses system properties
  • reboot() - Triggers device restart (lines 48-52)
  • raw() - Executes arbitrary shell commands

PmCommand: Package Manager Operations

The final builder in the standard chain, PmCommand methods append arguments to the pm (package manager) sub-command:

  • list_packages_sys(filter, user_id) - Lists installed packages with optional filtering (lines 48-74)
  • list_users() - Enumerates device users

Both methods eventually call the shared run() implementation defined on ACommand (lines 120-128) to execute the constructed process.

Compile-Time Safety Through Consumption

The type-state pattern enforcement relies on methods taking self by value, consuming the previous builder. This ownership transfer prevents reuse of intermediate states and ensures linear command construction.

Consider the method signatures:

pub fn shell<S: AsRef<str>>(self, device_serial: S) -> ShellCommand;
pub fn pm(self) -> PmCommand;

Since shell() consumes ACommand and pm() consumes ShellCommand, the compiler rejects invalid sequences like ACommand::new().pm() or reusing a shell context after converting it to a package manager command.

Practical Usage Examples

Retrieving ADB Version

The simplest usage queries the ADB version without device interaction:

let version = adb::ACommand::new()
    .version()
    .expect("Failed to get adb version");
println!("adb version: {version}");

Source: crates/uad-core/src/adb.rs (lines 130-138)

Listing System Packages

To list enabled packages for a specific user, chain through the full hierarchy:

let packages = adb::ACommand::new()
    .shell("")                // empty serial uses default device
    .pm()
    .list_packages_sys(Some(adb::PmListPacksFlag::OnlyEnabled), Some(0))
    .expect("Failed to list packages");
for pkg in packages {
    println!("{pkg}");
}

Source: ShellCommand::pm() (lines 32-35) and PmCommand::list_packages_sys (lines 48-74)

Rebooting a Device

For device control operations available at the shell level:

adb::ACommand::new()
    .shell("0123456789ABCDEF")  // specific device serial
    .reboot()
    .expect("Reboot failed");

Source: ShellCommand::reboot (lines 48-52)

Integration in GUI and CLI

The ADB command wrapper architecture serves both the graphical and command-line interfaces uniformly.

In the GUI (crates/uad-gui/src/views/about.rs, line 108), the about view retrieves version information:

let adb_version_text = text(match adb::ACommand::new().version() { … });

In the CLI (crates/uad-cli/src/commands.rs, line 134), REPL commands fetch package data using the same builder chain:

let system_packages = ACommand::new()
    .shell(serial)
    .pm()
    .list_packages_sys(None, None)?;

This shared abstraction ensures consistency across the application while maintaining type safety.

Summary

  • The type-state pattern in UADNG enforces valid ADB command sequences through Rust's type system, preventing runtime errors from malformed commands.
  • The builder hierarchy—ACommand → ShellCommand → PmCommand—models the natural progression of adb shell pm invocations.
  • Each transition consumes the previous builder, making illegal states unrepresentable and providing zero-cost abstractions over std::process::Command.
  • Both GUI and CLI crates in crates/uad-gui and crates/uad-cli leverage the core implementation in crates/uad-core/src/adb.rs.

Frequently Asked Questions

What is the type-state pattern in Rust?

The type-state pattern encodes state machine transitions into the type system itself. In UADNG, each valid stage of ADB command construction becomes a distinct struct (like ShellCommand or PmCommand), and methods only exist on types where those operations are valid. This moves error detection from runtime to compile time.

Why does UADNG use consuming methods (taking self by value) instead of &mut self?

Consuming methods ensure that once you transition from ACommand to ShellCommand, you cannot accidentally reuse the original ACommand or call methods out of order. This prevents bugs where a developer might try to execute a partially constructed command or modify a shell context after converting it to a package manager operation.

Can I extend the ADB wrapper to support new sub-commands?

Yes. The architecture allows extension by defining new builder structs that consume appropriate predecessors. For example, to add logcat support, you could implement a LogcatCommand returned by ShellCommand::logcat(), following the same pattern of consuming self and appending arguments before returning the new type.

Where is the actual ADB process execution handled?

The run() method implemented on ACommand (lines 120-128 in crates/uad-core/src/adb.rs) handles the final execution. All builders in the chain ultimately invoke this method through the wrapped std::process::Command, capturing output and handling errors consistently across the application.

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 →