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
Commandusage - Direct traceability where every Rust method maps 1-to-1 to an
adbexecutable 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.rsand implements a type-safe wrapper around theadbCLI. - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →