How the Backup and Restore Functionality Works for Debloated Packages in UAD-NG
Universal Android Debloater Next Generation serializes the state of uninstalled and disabled packages to timestamped JSON files and replays those states via generated ADB commands during restoration.
The backup and restore functionality for debloated packages in Universal Android Debloater Next Generation (UAD-NG) allows you to preserve your device cleanup progress across factory resets or device swaps. This Rust-based implementation stores per-user package states in human-readable JSON format under configurable paths, then translates those saved states back into executable shell commands when you need to restore. Understanding this mechanism helps you confidently manage device configurations without fear of losing your carefully curated debloating setup.
Where Backup Data Is Stored
UAD-NG defines backup locations through its global configuration structure. In crates/uad-core/src/config.rs, the Config struct contains GeneralSettings which specifies the root backup directory:
pub struct GeneralSettings {
pub backup_folder: PathBuf, // defaults to CACHE_DIR/backups
…
}
The application resolves this path via Config::load_configuration_file() and creates device-specific subdirectories named after the device's ADB ID. For example, a device with ID emulator-5554 stores its backups at CACHE_DIR/backups/emulator-5554/. Each backup file receives a timestamped filename formatted as %Y-%m-%d_%H-%M-%S.json to prevent collisions and preserve history.
Creating a Backup
The backup process begins in the UI layer when the user triggers Message::BackupDevice, which delegates to handle_backup_device in crates/uad-gui/src/views/settings.rs. The core logic resides in backup_phone within crates/uad-core/src/save.rs:
pub fn backup_phone(
users: Vec<User>,
device_id: String,
phone_packages: &[Vec<CorePackage>],
) -> Result<bool, String> {
// Build serializable structure
let mut backup = PhoneBackup { device_id, ..Default::default() };
for u in users {
let mut user_backup = UserBackup { id: u.id, ..Default::default() };
// Capture all packages for this user (disabled & uninstalled)
for p in phone_packages[u.index].iter().cloned() {
user_backup.packages.push(p);
}
backup.users.push(user_backup);
}
// Serialize to pretty JSON
let json = serde_json::to_string_pretty(&backup)?;
// Ensure directory exists and write file
let backup_dir = Config::load_configuration_file().general.backup_folder;
let backup_path = backup_dir.join(&device_id);
fs::create_dir_all(&backup_path)?;
let filename = format!("{}.json", chrono::Local::now().format("%Y-%m-%d_%H-%M-%S"));
fs::write(backup_path.join(filename), json)?;
Ok(true)
}
The function constructs a PhoneBackup structure containing a Vec<UserBackup>, where each UserBackup holds the user ID and a list of CorePackage entries with their saved states (Uninstalled or Disabled). The resulting JSON is human-readable and contains the full package state snapshot for every user profile on the device.
Listing and Selecting Backups
When you open the restore interface, UAD-NG populates the dropdown using list_available_backups from crates/uad-core/src/save.rs:
pub fn list_available_backups(dir: &Path) -> Vec<DisplayablePath> {
fs::read_dir(dir)
.ok()
.into_iter()
.flatten()
.map(|e| DisplayablePath { path: e.path() })
.collect()
}
This returns a vector of DisplayablePath structs representing available JSON files. Once you select a specific backup file, list_available_backup_user parses the JSON to extract user IDs, allowing the UI to present per-user restore options. This step ensures you can selectively restore package states for specific user profiles rather than applying the entire backup blindly.
The Restore Process
Restoration reverses the backup flow through the restore_backup function in crates/uad-core/src/save.rs. This function deserializes the selected JSON file and reconciles it with the currently connected device:
pub fn restore_backup(
selected_device: &Phone,
packages: &[Vec<CorePackage>],
settings: &DeviceSettings,
) -> Result<RestoreResult, String> {
// Load the chosen JSON file
let data = fs::read_to_string(
settings.backup.selected.as_ref().ok_or("field should be Some type")?.path.clone()
)?;
let phone_backup: PhoneBackup = serde_json::from_str(&data)?;
// Process each user in the backup
for u in phone_backup.users {
// Find matching user on connected device
let i_user = selected_device.user_list
.iter()
.find(|x| x.id == u.id)
.ok_or_else(|| format!("user {} doesn't exist", u.id))?
.index;
// Match packages and generate commands
for (i, backup_pkg) in u.packages.iter().enumerate() {
let current_pkg = packages[i_user]
.iter()
.find(|x| x.name == backup_pkg.name)
.ok_or_else(|| {
format!("Package {} not found for user {}", backup_pkg.name, u.id)
})?;
let cmds = apply_pkg_state_commands(
¤t_pkg,
backup_pkg.state,
settings.backup.selected_user.ok_or("field should be Some type")?,
selected_device,
);
if !cmds.is_empty() {
commands.push(BackupPackage {
i_user,
index: i,
commands: cmds,
});
}
}
}
Ok(RestoreResult { packages: commands, skipped_count: skipped_packages })
}
The restore logic executes several critical validation steps:
- User matching: The code verifies that each
UserBackup.idexists inselected_device.user_list. If a user from the backup is absent on the current device, the function returns an error immediately. - Package reconciliation: For each saved
CorePackage, the code attempts to locate the corresponding package in the livepackageslist. Missing packages increment askipped_countbut do not abort the process. - Command generation:
apply_pkg_state_commandstranslates the saved state (UninstalledorDisabled) into specific ADB shell commands (e.g.,pm uninstall -k --user <uid> <pkg>orpm disable-user <pkg>). - Batch collection: Commands aggregate into
BackupPackagestructs grouped by user index. The function returns aRestoreResultcontaining these command batches and the count of skipped packages.
The GUI or CLI layer then executes these commands sequentially to recreate the exact debloated state captured in the backup.
UI Integration and Event Handling
The settings view in crates/uad-gui/src/views/settings.rs wires the backup logic to interactive elements:
handle_backup_device: Invokesbackup_phoneasynchronously when the user clicks the Backup button.handle_choose_backup_folder: Opens a directory dialog to updategeneral.backup_folderin the configuration.backup_restore_container: Constructs the interface containing the backup button, restore button, backup file picker, and status text fields (self.device.backup.backup_state).
When a user selects a backup file from the dropdown, handle_backup_selected updates self.device.backup.users using list_available_backup_user, enabling granular per-user restoration controls.
Summary
- Backup storage: JSON files written to
CACHE_DIR/backups/<device_id>/<timestamp>.jsonas defined inConfig::load_configuration_file(). - Data structure: Hierarchical
PhoneBackup→UserBackup→CorePackagecapturing package names and states (Uninstalled/Disabled). - Creation:
backup_phoneincrates/uad-core/src/save.rsserializes current package states with timestamped filenames. - Restoration:
restore_backupmatches users by ID, skips missing packages, and generates ADB commands viaapply_pkg_state_commands. - Error handling: Restore aborts if a user is missing, but continues if individual packages are absent, reporting the skip count in
RestoreResult.
Frequently Asked Questions
What file format does UAD-NG use for backups?
UAD-NG uses human-readable JSON files generated by serde_json::to_string_pretty(). The structure consists of a PhoneBackup root object containing device metadata and a list of UserBackup entries, each holding package names and their states (Uninstalled or Disabled). This format allows manual inspection and editing if necessary.
Where does UAD-NG store backup files by default?
By default, the application stores backups in CACHE_DIR/backups/<adb_id>/, where CACHE_DIR is the platform-specific cache directory and <adb_id> represents the device's unique ADB identifier. You can modify this location through GeneralSettings.backup_folder in the configuration or via the UI's "Choose backup folder" dialog.
What happens if a package from the backup no longer exists on the device?
During restoration, if restore_backup encounters a package name in the JSON file that does not exist on the currently connected device, it increments the skipped_count and continues processing the remaining packages. This behavior accommodates app updates or manufacturer changes that remove specific system packages between backup and restore operations.
Can I restore a backup to a different Android device?
You can restore a backup to a different device only if the target device contains the same user profiles (matched by user ID) and the packages referenced in the backup exist on that device. The restore_backup function validates user existence strictly and will abort if a user ID from the backup is missing on the new device.
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 →