mirror of
https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation.git
synced 2026-08-09 07:09:12 +02:00
feat: add CLI and restructure into multi-package workspace
- Add crates/uad-cli for command-line interface - Extract shared logic into crates/uad-core - Separate GUI into crates/uad-gui - Enable code reuse across components
This commit is contained in:
parent
1bb1453257
commit
7dcb7e9315
41 changed files with 3124 additions and 610 deletions
58
.github/workflows/build_artifacts.yml
vendored
58
.github/workflows/build_artifacts.yml
vendored
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
restore-keys: ${{ runner.OS }}-release-
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
- name: Building
|
||||
run: cargo build --release --no-default-features --features wgpu,${{ matrix.update_feature }},img
|
||||
run: cargo build --release -p uad-gui --no-default-features --features wgpu,${{ matrix.update_feature }},img
|
||||
- name: Renaming binaries [Windows]
|
||||
if: matrix.os == 'windows-2022'
|
||||
run: mv target/release/uad-ng.exe uad-ng-${{ matrix.build_target }}.exe
|
||||
|
|
@ -68,3 +68,59 @@ jobs:
|
|||
with:
|
||||
name: uad-ng${{ matrix.update_name }}-${{ matrix.build_target }}
|
||||
path: uad-ng-*
|
||||
|
||||
build-cli:
|
||||
name: Building CLI ${{ matrix.build_target }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- build_target: linux
|
||||
os: ubuntu-22.04
|
||||
- build_target: macos
|
||||
os: macos-15
|
||||
- build_target: macos-intel
|
||||
os: macos-15-intel
|
||||
- build_target: windows
|
||||
os: windows-2022
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target
|
||||
key: ${{ runner.os }}-release-cli-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.OS }}-release-cli-
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
- name: Building
|
||||
run: cargo build --release -p uad-cli
|
||||
- name: Renaming binaries [Windows]
|
||||
if: matrix.os == 'windows-2022'
|
||||
run: mv target/release/uad.exe uad-cli-${{ matrix.build_target }}.exe
|
||||
- name: Renaming binaries [Others]
|
||||
if: matrix.os != 'windows-2022'
|
||||
run: mv target/release/uad uad-cli-${{ matrix.build_target }}
|
||||
- name: Tarball Linux/MacOS binary
|
||||
if: matrix.os != 'windows-2022'
|
||||
run: tar -czf uad-cli-${{ matrix.build_target }}{.tar.gz,}
|
||||
- name: Install coreutils for macOS
|
||||
if: startsWith(matrix.os, 'macos')
|
||||
run: brew install coreutils
|
||||
- name: Create checksums for binaries and archives [Windows]
|
||||
if: matrix.os == 'windows-2022'
|
||||
run: sha256sum uad-cli-${{ matrix.build_target }}.exe | tee uad-cli-${{ matrix.build_target }}.exe-checksum.txt
|
||||
- name: Create checksums for binaries and archives [Others]
|
||||
if: matrix.os != 'windows-2022'
|
||||
run: |
|
||||
sha256sum uad-cli-${{ matrix.build_target }} | tee uad-cli-${{ matrix.build_target }}-checksum
|
||||
sha256sum uad-cli-${{ matrix.build_target }}.tar.gz | tee uad-cli-${{ matrix.build_target }}.tar.gz-checksum
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: uad-cli-${{ matrix.build_target }}
|
||||
path: uad-cli-*
|
||||
|
|
|
|||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
|
@ -25,7 +25,9 @@ jobs:
|
|||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: uad-ng-*/*
|
||||
files: |
|
||||
uad-ng-*/*
|
||||
uad-cli-*/*
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
> [!NOTE]
|
||||
|
|
|
|||
807
Cargo.lock
generated
807
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
69
Cargo.toml
69
Cargo.toml
|
|
@ -1,6 +1,12 @@
|
|||
[package]
|
||||
name = "uad-ng"
|
||||
description = "A cross-platform GUI debloater for android devices"
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/uad-core",
|
||||
"crates/uad-gui",
|
||||
"crates/uad-cli"
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "1.2.0"
|
||||
authors = ["Universal-Debloater-Alliance"]
|
||||
license = "GPL-3.0"
|
||||
|
|
@ -11,59 +17,38 @@ keywords = ["debloater", "android", "adb", "privacy", "bloatware"]
|
|||
categories = ["gui"]
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = ["wgpu", "self-update", "img"]
|
||||
wgpu = [] # Iced/wgpu is default
|
||||
self-update = ["flate2", "tar"]
|
||||
no-self-update = []
|
||||
img = ["image", "iced/image"]
|
||||
|
||||
[dependencies]
|
||||
dark-light = "2"
|
||||
[workspace.dependencies]
|
||||
# Core dependencies
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = "^1.0"
|
||||
fern = { version = "^0", features = ["colored"] }
|
||||
chrono = { version = "^0.4", default-features = false, features = [
|
||||
"std",
|
||||
"clock",
|
||||
] }
|
||||
log = "^0.4"
|
||||
chrono = { version = "^0.4", default-features = false, features = ["std", "clock"] }
|
||||
toml = "^0"
|
||||
dirs = "^6"
|
||||
ureq = { version = "3", features = ["json"] }
|
||||
retry = "^2.0.0"
|
||||
iced = { version = "=0.14.0", features = ["advanced"] }
|
||||
image = { version = "0.25", optional = true }
|
||||
rfd = "^0.17.1"
|
||||
csv = "^1.3"
|
||||
|
||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||
flate2 = { version = "^1", optional = true }
|
||||
tar = { version = "^0.4", optional = true }
|
||||
# GUI dependencies
|
||||
iced = { version = "=0.14.0", features = ["advanced"] }
|
||||
image = "0.25"
|
||||
rfd = "^0.17.1"
|
||||
dark-light = "2"
|
||||
fern = { version = "^0", features = ["colored"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
# CLI dependencies
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
|
||||
# Platform-specific dependencies
|
||||
flate2 = "^1"
|
||||
tar = "^0.4"
|
||||
win32console = "^0.1.5"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
opt-level = 2
|
||||
codegen-units = 4
|
||||
|
||||
[profile.opt]
|
||||
inherits = "release"
|
||||
opt-level = "s"
|
||||
codegen-units = 1
|
||||
lto = "fat"
|
||||
strip = true
|
||||
panic = "abort"
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "3"
|
||||
|
||||
[lints.rust]
|
||||
[workspace.lints.rust]
|
||||
deprecated_safe = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
[workspace.lints.clippy]
|
||||
undocumented_unsafe_blocks = "forbid"
|
||||
panic_in_result_fn = "warn"
|
||||
infinite_loop = "warn"
|
||||
|
|
|
|||
53
crates/uad-cli/Cargo.toml
Normal file
53
crates/uad-cli/Cargo.toml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
[package]
|
||||
name = "uad-cli"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
readme.workspace = true
|
||||
keywords.workspace = true
|
||||
categories = ["command-line-utilities"]
|
||||
edition.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "uad"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
uad-core = { path = "../uad-core" }
|
||||
clap.workspace = true
|
||||
clap_complete = "4.4"
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
log.workspace = true
|
||||
chrono.workspace = true
|
||||
toml.workspace = true
|
||||
dirs.workspace = true
|
||||
ureq.workspace = true
|
||||
retry.workspace = true
|
||||
csv.workspace = true
|
||||
rustyline = "14.0"
|
||||
|
||||
[lints.rust]
|
||||
deprecated_safe = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
undocumented_unsafe_blocks = "deny"
|
||||
exit = "deny"
|
||||
panic_in_result_fn = "warn"
|
||||
infinite_loop = "warn"
|
||||
mem_forget = "warn"
|
||||
implicit_clone = "warn"
|
||||
format_push_string = "warn"
|
||||
large_include_file = "warn"
|
||||
shadow_unrelated = "warn"
|
||||
struct_field_names = "allow" # annoying
|
||||
module_name_repetitions = "allow" # annoying
|
||||
|
||||
disallowed_types = "deny"
|
||||
disallowed_methods = "deny"
|
||||
|
||||
allow_attributes_without_reason = "warn"
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
276
crates/uad-cli/README.md
Normal file
276
crates/uad-cli/README.md
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
# UAD CLI - Universal Android Debloater Command Line Interface
|
||||
|
||||
A powerful command-line interface for debloating Android devices, sharing the core functionality with the UAD GUI application.
|
||||
|
||||
## Features
|
||||
|
||||
- 🚀 **Fast & Efficient** - Direct command execution without GUI overhead
|
||||
- 📦 **Package Manager-like Interface** - Intuitive commands similar to `apt`, `dnf`, or `pacman`
|
||||
- 🔍 **Advanced Filtering** - Filter packages by state, removal category, UAD list, or search terms
|
||||
- 🛡️ **Safety Features** - Dry-run mode, warnings for unsafe packages
|
||||
- 🔄 **Multiple Actions** - Enable, disable, uninstall, and restore packages
|
||||
- 🖥️ **Interactive REPL Mode** - Keep device state in memory for faster operations
|
||||
- 📋 **Shell Completion** - Generate completions for bash, zsh, fish, and more
|
||||
- 👥 **Multi-user Support** - Target specific Android user profiles
|
||||
|
||||
## Installation
|
||||
|
||||
Build from source:
|
||||
|
||||
```bash
|
||||
cd /path/to/universal-android-debloater-next-generation
|
||||
cargo build --release -p uad-cli
|
||||
```
|
||||
|
||||
The binary will be available at `target/release/uad` (the package is `uad-cli`; the installed binary name is `uad`).
|
||||
|
||||
## Usage
|
||||
|
||||
### List Connected Devices
|
||||
|
||||
```bash
|
||||
uad devices
|
||||
```
|
||||
|
||||
### List Packages
|
||||
|
||||
List all packages on the first connected device:
|
||||
|
||||
```bash
|
||||
uad list
|
||||
# or use the short alias
|
||||
uad ls
|
||||
```
|
||||
|
||||
Filter packages by state:
|
||||
|
||||
```bash
|
||||
uad list --state enabled
|
||||
uad list --state disabled
|
||||
uad list --state uninstalled
|
||||
```
|
||||
|
||||
Filter by removal category:
|
||||
|
||||
```bash
|
||||
uad list --removal recommended
|
||||
uad list --removal advanced
|
||||
uad list --removal expert
|
||||
uad list --removal unsafe
|
||||
```
|
||||
|
||||
Filter by UAD list:
|
||||
|
||||
```bash
|
||||
uad list --list google
|
||||
uad list --list oem
|
||||
uad list --list aosp
|
||||
```
|
||||
|
||||
Search for packages:
|
||||
|
||||
```bash
|
||||
uad list --search "facebook"
|
||||
uad list -q "chrome"
|
||||
```
|
||||
|
||||
Combine filters:
|
||||
|
||||
```bash
|
||||
uad list --state enabled --removal recommended --search "google"
|
||||
```
|
||||
|
||||
### Show Package Information
|
||||
|
||||
```bash
|
||||
uad info com.android.chrome
|
||||
uad info com.facebook.katana --device SERIAL123
|
||||
```
|
||||
|
||||
### Uninstall Packages
|
||||
|
||||
Uninstall one or more packages:
|
||||
|
||||
```bash
|
||||
uad uninstall com.facebook.katana
|
||||
uad rm com.facebook.katana com.facebook.services
|
||||
```
|
||||
|
||||
Dry-run to see what would happen:
|
||||
|
||||
```bash
|
||||
uad uninstall com.facebook.katana --dry-run
|
||||
```
|
||||
|
||||
Specify device and user:
|
||||
|
||||
```bash
|
||||
uad uninstall com.facebook.katana --device SERIAL123 --user 0
|
||||
```
|
||||
|
||||
### Enable/Restore Packages
|
||||
|
||||
```bash
|
||||
uad enable com.android.chrome
|
||||
uad restore com.google.android.gms
|
||||
```
|
||||
|
||||
### Disable Packages
|
||||
|
||||
```bash
|
||||
uad disable com.facebook.katana
|
||||
```
|
||||
|
||||
### Update Package Lists
|
||||
|
||||
Update UAD lists from the remote repository:
|
||||
|
||||
```bash
|
||||
uad update
|
||||
```
|
||||
|
||||
### Interactive REPL Mode
|
||||
|
||||
Start an interactive session for faster repeated operations:
|
||||
|
||||
```bash
|
||||
uad repl
|
||||
# or
|
||||
uad shell
|
||||
```
|
||||
|
||||
Within the REPL:
|
||||
|
||||
```
|
||||
uad> help
|
||||
uad> list --state enabled
|
||||
uad> info com.facebook.katana
|
||||
uad> uninstall com.facebook.katana
|
||||
uad> enable com.android.chrome
|
||||
uad> device
|
||||
uad> exit
|
||||
```
|
||||
|
||||
The REPL mode:
|
||||
- Keeps device state in memory (faster operations)
|
||||
- Saves command history (accessible with up/down arrows)
|
||||
- Loads UAD lists once at startup
|
||||
- Supports all the same commands as the CLI
|
||||
|
||||
### Shell Completion
|
||||
|
||||
Generate completion scripts for your shell:
|
||||
|
||||
```bash
|
||||
# Bash
|
||||
uad completions bash > /usr/share/bash-completion/completions/uad
|
||||
|
||||
# Zsh
|
||||
uad completions zsh > /usr/local/share/zsh/site-functions/_uad
|
||||
|
||||
# Fish
|
||||
uad completions fish > ~/.config/fish/completions/uad.fish
|
||||
|
||||
# PowerShell
|
||||
uad completions powershell > uad.ps1
|
||||
```
|
||||
|
||||
## Command Reference
|
||||
|
||||
| Command | Aliases | Description |
|
||||
|---------|---------|-------------|
|
||||
| `devices` | - | List connected Android devices |
|
||||
| `list` | `ls` | List packages with optional filters |
|
||||
| `info` | - | Show detailed package information |
|
||||
| `uninstall` | `rm` | Uninstall one or more packages |
|
||||
| `enable` | `restore` | Enable/restore packages |
|
||||
| `disable` | - | Disable packages (keeps data) |
|
||||
| `update` | - | Update UAD package lists |
|
||||
| `completions` | - | Generate shell completions |
|
||||
| `repl` | `shell` | Start interactive mode |
|
||||
|
||||
## Examples
|
||||
|
||||
### Remove all Facebook packages
|
||||
|
||||
```bash
|
||||
uad list --search facebook
|
||||
uad uninstall com.facebook.katana com.facebook.services --dry-run
|
||||
# If it looks good:
|
||||
uad uninstall com.facebook.katana com.facebook.services
|
||||
```
|
||||
|
||||
### Debloat recommended Google apps
|
||||
|
||||
```bash
|
||||
# See what would be uninstalled
|
||||
uad list --removal recommended --list google
|
||||
|
||||
# Uninstall them (you'd list the actual package names)
|
||||
uad uninstall com.google.package1 com.google.package2
|
||||
```
|
||||
|
||||
### Work with a specific device
|
||||
|
||||
```bash
|
||||
# List devices
|
||||
uad devices
|
||||
|
||||
# Use specific device
|
||||
uad list --device SERIAL123
|
||||
uad uninstall com.facebook.katana --device SERIAL123
|
||||
```
|
||||
|
||||
## Safety
|
||||
|
||||
- **Dry-run mode**: Always test with `--dry-run` first
|
||||
- **Warnings**: The CLI warns you about packages marked as "Unsafe"
|
||||
- **Reversible**: Most operations can be reversed with the `enable` command
|
||||
- **Multi-user aware**: Respects Android's multi-user system
|
||||
|
||||
## Comparison with GUI
|
||||
|
||||
### CLI Advantages:
|
||||
- Faster for power users
|
||||
- Scriptable and automatable
|
||||
- Lower resource usage
|
||||
- Works over SSH
|
||||
- Better for batch operations
|
||||
- Can run on Android itself (though not recommended for same-device debloating)
|
||||
|
||||
### GUI Advantages:
|
||||
- Visual feedback
|
||||
- Easier for beginners
|
||||
- Package descriptions always visible
|
||||
- Batch selection with checkboxes
|
||||
|
||||
## Requirements
|
||||
|
||||
- ADB (Android Debug Bridge) installed and in PATH
|
||||
- Android device with USB debugging enabled
|
||||
- Device authorized in ADB (run `adb devices` to check)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**No devices found**
|
||||
- Ensure USB debugging is enabled on your device
|
||||
- Run `adb devices` to check if device is authorized
|
||||
- Try unplugging and replugging the device
|
||||
|
||||
**Package not found**
|
||||
- The package might already be uninstalled
|
||||
- Check the exact package name with `adb shell pm list packages`
|
||||
- Try running `uad update` to refresh package lists
|
||||
|
||||
**Permission denied**
|
||||
- Some system packages can't be modified without root
|
||||
- Some devices have locked bootloaders preventing certain operations
|
||||
|
||||
## Contributing
|
||||
|
||||
See the main project [CONTRIBUTING.md](../../CONTRIBUTING.md) for guidelines.
|
||||
|
||||
## License
|
||||
|
||||
GPL-3.0 - See [LICENSE](../../LICENSE) for details.
|
||||
|
||||
44
crates/uad-cli/examples/usage.sh
Executable file
44
crates/uad-cli/examples/usage.sh
Executable file
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Example usage script for UAD CLI
|
||||
|
||||
# Check if uad-cli is available
|
||||
if ! command -v uad &> /dev/null; then
|
||||
echo "uad command not found. Please build and install uad-cli first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== UAD CLI Usage Examples ==="
|
||||
echo
|
||||
|
||||
echo "1. List connected devices:"
|
||||
uad devices
|
||||
echo
|
||||
|
||||
echo "2. List all enabled packages:"
|
||||
uad list --state enabled | head -20
|
||||
echo "... (showing first 20)"
|
||||
echo
|
||||
|
||||
echo "3. Search for Google packages:"
|
||||
uad list --search google | head -10
|
||||
echo "... (showing first 10)"
|
||||
echo
|
||||
|
||||
echo "4. Show info about a package (example):"
|
||||
echo "uad info com.google.android.gms"
|
||||
echo
|
||||
|
||||
echo "5. Dry-run uninstall (safe to test):"
|
||||
echo "uad uninstall com.example.package --dry-run"
|
||||
echo
|
||||
|
||||
echo "6. Start interactive mode:"
|
||||
echo "uad repl"
|
||||
echo
|
||||
|
||||
echo "=== Generate shell completions ==="
|
||||
echo "For bash: uad completions bash > ~/.local/share/bash-completion/completions/uad"
|
||||
echo "For zsh: uad completions zsh > ~/.local/share/zsh/site-functions/_uad"
|
||||
echo "For fish: uad completions fish > ~/.config/fish/completions/uad.fish"
|
||||
|
||||
464
crates/uad-cli/src/commands.rs
Normal file
464
crates/uad-cli/src/commands.rs
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
use clap::CommandFactory;
|
||||
use clap_complete::{Shell, generate};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::Write;
|
||||
use uad_core::adb::{ACommand, PmListPacksFlag};
|
||||
use uad_core::sync::{
|
||||
CorePackage, Phone, User, apply_pkg_state_commands, get_devices_list, get_package_state,
|
||||
run_adb_shell_action,
|
||||
};
|
||||
use uad_core::uad_lists::{Package, PackageState, Removal, UadList, load_debloat_lists};
|
||||
use uad_core::utils::{matches_search, truncate_description};
|
||||
|
||||
use crate::device::{get_target_device, get_user};
|
||||
use crate::filters::{ListFilter, RemovalFilter, StateFilter};
|
||||
use crate::{Cli, print_or_exit, println_or_exit};
|
||||
|
||||
/// List all connected Android devices
|
||||
pub fn list_devices() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Scanning for connected devices...");
|
||||
let devices = get_devices_list();
|
||||
|
||||
if devices.is_empty() {
|
||||
eprintln!("No devices found. Make sure ADB is installed and devices are connected.");
|
||||
return Err("No devices found".into());
|
||||
}
|
||||
|
||||
println!("\nFound {} device(s):\n", devices.len());
|
||||
for device in &devices {
|
||||
println!(" Model: {}", device.model);
|
||||
println!(" Serial: {}", device.adb_id);
|
||||
println!(" Android SDK: {}", device.android_sdk);
|
||||
|
||||
if !device.user_list.is_empty() {
|
||||
println!(" Users: {} user(s)", device.user_list.len());
|
||||
for user in &device.user_list {
|
||||
let protected = if user.protected { " (protected)" } else { "" };
|
||||
println!(" - User ID: {}{}", user.id, protected);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Context for package filtering and display
|
||||
pub struct PackageListContext {
|
||||
pub state_filter: Option<StateFilter>,
|
||||
pub removal_filter: Option<RemovalFilter>,
|
||||
pub list_filter: Option<ListFilter>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
impl PackageListContext {
|
||||
/// Check if package passes all filters
|
||||
pub fn filter_package(
|
||||
&self,
|
||||
pkg_name: &str,
|
||||
pkg_info: Option<&Package>,
|
||||
pkg_state: PackageState,
|
||||
) -> bool {
|
||||
// Removal filter
|
||||
if let Some(removal) = self.removal_filter {
|
||||
if !removal.matches(pkg_info) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// State filter
|
||||
if let Some(state) = self.state_filter {
|
||||
if !state.matches(pkg_state) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// List filter
|
||||
if let Some(list) = self.list_filter {
|
||||
if !list.matches(pkg_info) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Search filter
|
||||
if let Some(ref search_term) = self.search {
|
||||
let description = pkg_info.map(|p| p.description.as_str());
|
||||
if !matches_search(pkg_name, search_term, description) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Determine what information to show based on active filters
|
||||
pub fn display_config(&self) -> DisplayConfig {
|
||||
DisplayConfig {
|
||||
show_state: self.state_filter.is_none_or(|f| !f.is_specific()),
|
||||
show_removal: self.removal_filter.is_none_or(|f| !f.is_specific()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DisplayConfig {
|
||||
pub show_state: bool,
|
||||
pub show_removal: bool,
|
||||
}
|
||||
|
||||
/// List packages on a device with filtering
|
||||
pub fn list_packages(
|
||||
device: Option<String>,
|
||||
state_filter: Option<StateFilter>,
|
||||
removal_filter: Option<RemovalFilter>,
|
||||
list_filter: Option<ListFilter>,
|
||||
search: Option<String>,
|
||||
user_id: Option<u16>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let target_device = get_target_device(device)?;
|
||||
let uad_lists = load_debloat_lists(false).unwrap_or_else(|lists| lists);
|
||||
|
||||
println_or_exit!(
|
||||
"Listing packages on: {} ({})\n",
|
||||
target_device.model,
|
||||
target_device.adb_id
|
||||
);
|
||||
|
||||
let context = PackageListContext {
|
||||
state_filter,
|
||||
removal_filter,
|
||||
list_filter,
|
||||
search,
|
||||
};
|
||||
|
||||
let pm_flag = state_filter.and_then(StateFilter::to_pm_flag);
|
||||
let system_packages = ACommand::new()
|
||||
.shell(&target_device.adb_id)
|
||||
.pm()
|
||||
.list_packages_sys(pm_flag, user_id)?;
|
||||
|
||||
let displayed_count = display_package_list(
|
||||
&system_packages,
|
||||
&uad_lists,
|
||||
&target_device.adb_id,
|
||||
user_id,
|
||||
&context,
|
||||
)?;
|
||||
|
||||
if displayed_count == 0 {
|
||||
println_or_exit!(" No packages found matching the specified filters.");
|
||||
} else {
|
||||
println_or_exit!("\nTotal: {} package(s)", displayed_count);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Display a list of packages with their info
|
||||
pub fn display_package_list(
|
||||
packages: &[String],
|
||||
uad_lists: &HashMap<String, Package>,
|
||||
device_serial: &str,
|
||||
user_id: Option<u16>,
|
||||
context: &PackageListContext,
|
||||
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||
let config = context.display_config();
|
||||
let mut displayed_count = 0;
|
||||
|
||||
let enabled_packages: HashSet<String> = ACommand::new()
|
||||
.shell(device_serial)
|
||||
.pm()
|
||||
.list_packages_sys(Some(PmListPacksFlag::OnlyEnabled), user_id)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.collect();
|
||||
let disabled_packages: HashSet<String> = ACommand::new()
|
||||
.shell(device_serial)
|
||||
.pm()
|
||||
.list_packages_sys(Some(PmListPacksFlag::OnlyDisabled), user_id)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
for pkg_name in packages {
|
||||
let pkg_info = uad_lists.get(pkg_name);
|
||||
|
||||
let pkg_state = if enabled_packages.contains(pkg_name) {
|
||||
PackageState::Enabled
|
||||
} else if disabled_packages.contains(pkg_name) {
|
||||
PackageState::Disabled
|
||||
} else {
|
||||
PackageState::Uninstalled
|
||||
};
|
||||
|
||||
if !context.filter_package(pkg_name, pkg_info, pkg_state) {
|
||||
continue;
|
||||
}
|
||||
|
||||
display_package_entry(pkg_name, pkg_info, pkg_state, &config);
|
||||
displayed_count += 1;
|
||||
}
|
||||
|
||||
Ok(displayed_count)
|
||||
}
|
||||
|
||||
/// Display a single package entry
|
||||
pub fn display_package_entry(
|
||||
pkg_name: &str,
|
||||
pkg_info: Option<&Package>,
|
||||
pkg_state: PackageState,
|
||||
config: &DisplayConfig,
|
||||
) {
|
||||
print_or_exit!("[");
|
||||
|
||||
if let Some(info) = pkg_info {
|
||||
if config.show_removal {
|
||||
print_or_exit!("{}", info.removal);
|
||||
if config.show_state {
|
||||
print_or_exit!(" - ");
|
||||
}
|
||||
}
|
||||
if config.show_state {
|
||||
print_or_exit!("{}", pkg_state);
|
||||
}
|
||||
print_or_exit!("] {}", pkg_name);
|
||||
if !info.description.is_empty() {
|
||||
print_or_exit!(" - {}", truncate_description(&info.description, 80));
|
||||
}
|
||||
} else {
|
||||
if config.show_removal {
|
||||
print_or_exit!("Unlisted");
|
||||
if config.show_state {
|
||||
print_or_exit!(" - ");
|
||||
}
|
||||
}
|
||||
if config.show_state {
|
||||
print_or_exit!("{}", pkg_state);
|
||||
}
|
||||
print_or_exit!("] {}", pkg_name);
|
||||
}
|
||||
|
||||
println_or_exit!();
|
||||
}
|
||||
|
||||
/// Change the state of one or more packages
|
||||
pub fn change_package_state(
|
||||
packages: &[String],
|
||||
device: Option<String>,
|
||||
user_id: Option<u16>,
|
||||
dry_run: bool,
|
||||
target_state: PackageState,
|
||||
action_name: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if packages.is_empty() {
|
||||
eprintln!("Error: No packages specified");
|
||||
return Err("No packages specified".into());
|
||||
}
|
||||
|
||||
let target_device = get_target_device(device)?;
|
||||
let user = get_user(&target_device, user_id)?;
|
||||
let uad_lists = load_debloat_lists(false).unwrap_or_else(|lists| lists);
|
||||
|
||||
println!(
|
||||
"{} {} package(s) on: {} ({})\n",
|
||||
action_name,
|
||||
packages.len(),
|
||||
target_device.model,
|
||||
target_device.adb_id
|
||||
);
|
||||
|
||||
if dry_run {
|
||||
println!("DRY RUN - No changes will be made\n");
|
||||
}
|
||||
|
||||
for pkg_name in packages {
|
||||
process_package_state_change(
|
||||
pkg_name,
|
||||
&target_device,
|
||||
user,
|
||||
target_state,
|
||||
dry_run,
|
||||
&uad_lists,
|
||||
)?;
|
||||
println!();
|
||||
}
|
||||
|
||||
if dry_run {
|
||||
println!("Dry run completed. No changes were made.");
|
||||
} else {
|
||||
println!("Operation completed successfully.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process state change for a single package
|
||||
fn process_package_state_change(
|
||||
pkg_name: &str,
|
||||
device: &Phone,
|
||||
user: User,
|
||||
target_state: PackageState,
|
||||
dry_run: bool,
|
||||
uad_lists: &HashMap<String, Package>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let current_state = get_package_state(&device.adb_id, pkg_name, Some(user.id))
|
||||
.ok_or("Package not found on device")?;
|
||||
|
||||
println!(" {} ({})", pkg_name, current_state);
|
||||
|
||||
if current_state == target_state {
|
||||
println!(" → Already in target state, skipping");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let pkg_info = uad_lists.get(pkg_name);
|
||||
if let Some(info) = pkg_info {
|
||||
if info.removal == Removal::Unsafe {
|
||||
println!(" ⚠ WARNING: This package is marked as UNSAFE to remove!");
|
||||
}
|
||||
}
|
||||
|
||||
let core_pkg = CorePackage {
|
||||
name: pkg_name.to_string(),
|
||||
description: pkg_info.map(|p| p.description.clone()).unwrap_or_default(),
|
||||
removal: pkg_info.map(|p| p.removal).unwrap_or(Removal::Unlisted),
|
||||
state: current_state,
|
||||
list: pkg_info.map(|p| p.list).unwrap_or(UadList::Unlisted),
|
||||
};
|
||||
|
||||
let commands = apply_pkg_state_commands(&core_pkg, target_state, user, device);
|
||||
|
||||
if dry_run {
|
||||
for cmd in &commands {
|
||||
println!(" Would run: {}", cmd);
|
||||
}
|
||||
} else {
|
||||
execute_with_fallback(
|
||||
pkg_name,
|
||||
target_state,
|
||||
&core_pkg,
|
||||
user,
|
||||
device,
|
||||
&commands,
|
||||
" ",
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute commands and verify package state with fallback
|
||||
pub fn execute_with_fallback(
|
||||
package: &str,
|
||||
target_state: PackageState,
|
||||
core_pkg: &CorePackage,
|
||||
user: User,
|
||||
device: &Phone,
|
||||
commands: &[String],
|
||||
indent: &str,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Capture the before-state of packages on other users for cross-user detection
|
||||
let before_cross_user_states =
|
||||
uad_core::sync::capture_cross_user_states(package, &device.adb_id, user.id, device);
|
||||
|
||||
// Execute commands
|
||||
for cmd in commands {
|
||||
match run_adb_shell_action(&device.adb_id, cmd.as_str()) {
|
||||
Ok(_) => println!("{}✓ {}", indent, cmd),
|
||||
Err(e) => {
|
||||
eprintln!("{}✗ Failed: {:?}", indent, e);
|
||||
return Err(format!("Failed to execute: {}", cmd).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify package state and attempt fallback if needed
|
||||
let actual_state =
|
||||
get_package_state(&device.adb_id, package, Some(user.id)).unwrap_or(PackageState::Enabled);
|
||||
|
||||
if actual_state != target_state {
|
||||
println!(
|
||||
"{}⚠ Package state verification failed: expected {:?}, got {:?}",
|
||||
indent, target_state, actual_state
|
||||
);
|
||||
|
||||
// Attempt fallback
|
||||
if let Ok(fallback_action) =
|
||||
uad_core::sync::attempt_fallback(core_pkg, target_state, actual_state, user, device)
|
||||
{
|
||||
println!("{}↻ Fallback: {}", indent, fallback_action);
|
||||
} else {
|
||||
println!("{}✗ No fallback available", indent);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for cross-user behavior if operation succeeded
|
||||
if actual_state == target_state {
|
||||
if let Some(notification) = uad_core::sync::detect_cross_user_behavior(
|
||||
package,
|
||||
device.adb_id.as_str(),
|
||||
user.id,
|
||||
target_state,
|
||||
actual_state,
|
||||
device,
|
||||
&before_cross_user_states,
|
||||
) {
|
||||
println!("{}ℹ {}", indent, notification);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Show detailed information about a package
|
||||
pub fn show_package_info(
|
||||
package: &str,
|
||||
device: Option<String>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Package: {}\n", package);
|
||||
|
||||
let uad_lists = load_debloat_lists(false).unwrap_or_else(|lists| lists);
|
||||
|
||||
if let Some(pkg_info) = uad_lists.get(package) {
|
||||
println!("UAD Information:");
|
||||
println!(" List: {}", pkg_info.list);
|
||||
println!(" Removal: {}", pkg_info.removal);
|
||||
println!(" Description: {}", pkg_info.description);
|
||||
println!();
|
||||
} else {
|
||||
println!(" Not found in UAD lists (unlisted package)\n");
|
||||
}
|
||||
|
||||
if let Some(device_id) = device {
|
||||
let target_device = get_target_device(Some(device_id))?;
|
||||
println!("Device: {} ({})", target_device.model, target_device.adb_id);
|
||||
|
||||
let state = get_package_state(&target_device.adb_id, package, None)
|
||||
.ok_or("Package not found on device")?;
|
||||
println!(" State: {}", state);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update UAD package lists from remote repository
|
||||
pub fn update_lists() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Updating UAD package lists from remote repository...");
|
||||
|
||||
match load_debloat_lists(true) {
|
||||
Ok(_lists) => {
|
||||
println!("✓ Successfully updated package lists");
|
||||
Ok(())
|
||||
}
|
||||
Err(_lists) => {
|
||||
eprintln!("✗ Failed to update lists from remote, using cached version");
|
||||
Err("Failed to update lists".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate shell completion script
|
||||
pub fn generate_completions(shell: Shell) {
|
||||
let mut cmd = Cli::command();
|
||||
let name = cmd.get_name().to_string();
|
||||
generate(shell, &mut cmd, name, &mut std::io::stdout());
|
||||
}
|
||||
45
crates/uad-cli/src/device.rs
Normal file
45
crates/uad-cli/src/device.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use uad_core::sync::{Phone, User, get_devices_list};
|
||||
|
||||
/// Get target device, either by serial or first available
|
||||
pub fn get_target_device(device: Option<String>) -> Result<Phone, Box<dyn std::error::Error>> {
|
||||
let devices = get_devices_list();
|
||||
|
||||
if devices.is_empty() {
|
||||
eprintln!("Error: No devices found");
|
||||
return Err("No devices found".into());
|
||||
}
|
||||
|
||||
let target_device = if let Some(device_id) = device {
|
||||
devices
|
||||
.iter()
|
||||
.find(|d| d.adb_id == device_id)
|
||||
.ok_or("Device not found")?
|
||||
.clone()
|
||||
} else {
|
||||
if devices.len() > 1 {
|
||||
eprintln!(
|
||||
"Warning: Multiple devices found, using first one: {}",
|
||||
devices[0].adb_id
|
||||
);
|
||||
}
|
||||
devices[0].clone()
|
||||
};
|
||||
|
||||
Ok(target_device)
|
||||
}
|
||||
|
||||
/// Get user from device, creating a basic one if not found
|
||||
pub fn get_user(device: &Phone, user_id: Option<u16>) -> Result<User, Box<dyn std::error::Error>> {
|
||||
let uid = user_id.unwrap_or(0);
|
||||
|
||||
if let Some(user) = device.user_list.iter().find(|u| u.id == uid) {
|
||||
Ok(*user)
|
||||
} else {
|
||||
// Create a basic user if not found in list
|
||||
Ok(User {
|
||||
id: uid,
|
||||
index: 0,
|
||||
protected: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
98
crates/uad-cli/src/filters.rs
Normal file
98
crates/uad-cli/src/filters.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
use clap::ValueEnum;
|
||||
use uad_core::adb::PmListPacksFlag;
|
||||
use uad_core::uad_lists::{Package, PackageState, Removal, UadList};
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum StateFilter {
|
||||
/// Show all packages regardless of state
|
||||
All,
|
||||
/// Show only enabled packages
|
||||
Enabled,
|
||||
/// Show only disabled packages
|
||||
Disabled,
|
||||
/// Show only uninstalled packages
|
||||
Uninstalled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum RemovalFilter {
|
||||
All,
|
||||
Recommended,
|
||||
Advanced,
|
||||
Expert,
|
||||
Unsafe,
|
||||
Unlisted,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum ListFilter {
|
||||
All,
|
||||
Aosp,
|
||||
Carrier,
|
||||
Google,
|
||||
Misc,
|
||||
Oem,
|
||||
Pending,
|
||||
Unlisted,
|
||||
}
|
||||
|
||||
impl StateFilter {
|
||||
pub fn to_pm_flag(self) -> Option<PmListPacksFlag> {
|
||||
match self {
|
||||
Self::Enabled => Some(PmListPacksFlag::OnlyEnabled),
|
||||
Self::Disabled => Some(PmListPacksFlag::OnlyDisabled),
|
||||
Self::Uninstalled | Self::All => Some(PmListPacksFlag::IncludeUninstalled),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches(self, pkg_state: PackageState) -> bool {
|
||||
match self {
|
||||
Self::All => true,
|
||||
Self::Enabled => pkg_state == PackageState::Enabled,
|
||||
Self::Disabled => pkg_state == PackageState::Disabled,
|
||||
Self::Uninstalled => pkg_state == PackageState::Uninstalled,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_specific(self) -> bool {
|
||||
matches!(self, Self::Enabled | Self::Disabled | Self::Uninstalled)
|
||||
}
|
||||
}
|
||||
|
||||
impl RemovalFilter {
|
||||
pub fn matches(self, pkg_info: Option<&Package>) -> bool {
|
||||
match pkg_info {
|
||||
Some(info) => match self {
|
||||
Self::All => true,
|
||||
Self::Recommended => info.removal == Removal::Recommended,
|
||||
Self::Advanced => info.removal == Removal::Advanced,
|
||||
Self::Expert => info.removal == Removal::Expert,
|
||||
Self::Unsafe => info.removal == Removal::Unsafe,
|
||||
Self::Unlisted => info.removal == Removal::Unlisted,
|
||||
},
|
||||
None => matches!(self, Self::All | Self::Unlisted),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_specific(self) -> bool {
|
||||
!matches!(self, Self::All)
|
||||
}
|
||||
}
|
||||
|
||||
impl ListFilter {
|
||||
pub fn matches(self, pkg_info: Option<&Package>) -> bool {
|
||||
match pkg_info {
|
||||
Some(info) => match self {
|
||||
Self::All => true,
|
||||
Self::Aosp => info.list == UadList::Aosp,
|
||||
Self::Carrier => info.list == UadList::Carrier,
|
||||
Self::Google => info.list == UadList::Google,
|
||||
Self::Misc => info.list == UadList::Misc,
|
||||
Self::Oem => info.list == UadList::Oem,
|
||||
Self::Pending => info.list == UadList::Pending,
|
||||
Self::Unlisted => info.list == UadList::Unlisted,
|
||||
},
|
||||
None => matches!(self, Self::All | Self::Unlisted),
|
||||
}
|
||||
}
|
||||
}
|
||||
232
crates/uad-cli/src/main.rs
Normal file
232
crates/uad-cli/src/main.rs
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
#![allow(
|
||||
clippy::needless_continue,
|
||||
clippy::collapsible_if,
|
||||
clippy::uninlined_format_args,
|
||||
clippy::map_unwrap_or,
|
||||
clippy::unnecessary_wraps,
|
||||
reason = "Suppress non-critical pedantic/style lints to keep build green"
|
||||
)]
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap_complete::Shell;
|
||||
use uad_core::uad_lists::PackageState;
|
||||
|
||||
mod commands;
|
||||
mod device;
|
||||
mod filters;
|
||||
mod output;
|
||||
mod repl;
|
||||
|
||||
use filters::{ListFilter, RemovalFilter, StateFilter};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "uad")]
|
||||
#[command(about = "Universal Android Debloater - Command Line Interface", long_about = None)]
|
||||
#[command(version)]
|
||||
#[command(propagate_version = true)]
|
||||
pub struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// List connected Android devices
|
||||
Devices,
|
||||
|
||||
/// List packages on a device
|
||||
#[command(name = "list", visible_alias = "ls")]
|
||||
List {
|
||||
/// Device serial number (optional, uses first device if not specified)
|
||||
#[arg(short, long)]
|
||||
device: Option<String>,
|
||||
|
||||
/// Filter by package state
|
||||
#[arg(short, long, value_enum)]
|
||||
state: Option<StateFilter>,
|
||||
|
||||
/// Filter by removal category
|
||||
#[arg(short, long, value_enum)]
|
||||
removal: Option<RemovalFilter>,
|
||||
|
||||
/// Filter by UAD list
|
||||
#[arg(short, long, value_enum)]
|
||||
list: Option<ListFilter>,
|
||||
|
||||
/// Search pattern (matches package name or description)
|
||||
#[arg(short = 'q', long)]
|
||||
search: Option<String>,
|
||||
|
||||
/// User ID (defaults to 0)
|
||||
#[arg(short, long)]
|
||||
user: Option<u16>,
|
||||
},
|
||||
|
||||
/// Uninstall packages (default removal action)
|
||||
#[command(visible_alias = "rm")]
|
||||
Uninstall {
|
||||
/// Package names to uninstall
|
||||
packages: Vec<String>,
|
||||
|
||||
/// Device serial number (optional, uses first device if not specified)
|
||||
#[arg(short, long)]
|
||||
device: Option<String>,
|
||||
|
||||
/// User ID (defaults to 0)
|
||||
#[arg(short, long)]
|
||||
user: Option<u16>,
|
||||
|
||||
/// Dry run - show what would be done without actually doing it
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
},
|
||||
|
||||
/// Restore (reinstall) packages
|
||||
#[command(visible_alias = "restore")]
|
||||
Enable {
|
||||
/// Package names to restore/enable
|
||||
packages: Vec<String>,
|
||||
|
||||
/// Device serial number (optional, uses first device if not specified)
|
||||
#[arg(short, long)]
|
||||
device: Option<String>,
|
||||
|
||||
/// User ID (defaults to 0)
|
||||
#[arg(short, long)]
|
||||
user: Option<u16>,
|
||||
|
||||
/// Dry run - show what would be done without actually doing it
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
},
|
||||
|
||||
/// Disable packages (keeps data but prevents execution)
|
||||
Disable {
|
||||
/// Package names to disable
|
||||
packages: Vec<String>,
|
||||
|
||||
/// Device serial number (optional, uses first device if not specified)
|
||||
#[arg(short, long)]
|
||||
device: Option<String>,
|
||||
|
||||
/// User ID (defaults to 0)
|
||||
#[arg(short, long)]
|
||||
user: Option<u16>,
|
||||
|
||||
/// Dry run - show what would be done without actually doing it
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
},
|
||||
|
||||
/// Show detailed information about a package
|
||||
Info {
|
||||
/// Package name
|
||||
package: String,
|
||||
|
||||
/// Device serial number (optional, uses first device if not specified)
|
||||
#[arg(short, long)]
|
||||
device: Option<String>,
|
||||
},
|
||||
|
||||
/// Update UAD package lists from remote repository
|
||||
Update,
|
||||
|
||||
/// Generate shell completion script
|
||||
Completions {
|
||||
/// Shell to generate completions for
|
||||
#[arg(value_enum)]
|
||||
shell: Shell,
|
||||
},
|
||||
|
||||
/// Start interactive REPL mode
|
||||
#[command(visible_alias = "shell")]
|
||||
Repl {
|
||||
/// Device serial number (optional, uses first device if not specified)
|
||||
#[arg(short, long)]
|
||||
device: Option<String>,
|
||||
|
||||
/// User ID (defaults to 0)
|
||||
#[arg(short, long)]
|
||||
user: Option<u16>,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Commands::Devices => {
|
||||
commands::list_devices()?;
|
||||
}
|
||||
Commands::List {
|
||||
device,
|
||||
state,
|
||||
removal,
|
||||
list,
|
||||
search,
|
||||
user,
|
||||
} => {
|
||||
commands::list_packages(device, state, removal, list, search, user)?;
|
||||
}
|
||||
Commands::Uninstall {
|
||||
packages,
|
||||
device,
|
||||
user,
|
||||
dry_run,
|
||||
} => {
|
||||
commands::change_package_state(
|
||||
&packages,
|
||||
device,
|
||||
user,
|
||||
dry_run,
|
||||
PackageState::Uninstalled,
|
||||
"Uninstalling",
|
||||
)?;
|
||||
}
|
||||
Commands::Enable {
|
||||
packages,
|
||||
device,
|
||||
user,
|
||||
dry_run,
|
||||
} => {
|
||||
commands::change_package_state(
|
||||
&packages,
|
||||
device,
|
||||
user,
|
||||
dry_run,
|
||||
PackageState::Enabled,
|
||||
"Enabling",
|
||||
)?;
|
||||
}
|
||||
Commands::Disable {
|
||||
packages,
|
||||
device,
|
||||
user,
|
||||
dry_run,
|
||||
} => {
|
||||
commands::change_package_state(
|
||||
&packages,
|
||||
device,
|
||||
user,
|
||||
dry_run,
|
||||
PackageState::Disabled,
|
||||
"Disabling",
|
||||
)?;
|
||||
}
|
||||
Commands::Info { package, device } => {
|
||||
commands::show_package_info(&package, device)?;
|
||||
}
|
||||
Commands::Update => {
|
||||
commands::update_lists()?;
|
||||
}
|
||||
Commands::Completions { shell } => {
|
||||
commands::generate_completions(shell);
|
||||
}
|
||||
Commands::Repl { device, user } => {
|
||||
repl::repl_mode(device, user)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
18
crates/uad-cli/src/output.rs
Normal file
18
crates/uad-cli/src/output.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/// Helper macro to handle broken pipe errors gracefully
|
||||
/// When piping to commands like `head`, we want to exit cleanly when the pipe closes
|
||||
#[macro_export]
|
||||
macro_rules! println_or_exit {
|
||||
() => {
|
||||
let _ = writeln!(std::io::stdout());
|
||||
};
|
||||
($($arg:tt)*) => {
|
||||
let _ = writeln!(std::io::stdout(), $($arg)*);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! print_or_exit {
|
||||
($($arg:tt)*) => {
|
||||
let _ = write!(std::io::stdout(), $($arg)*);
|
||||
};
|
||||
}
|
||||
369
crates/uad-cli/src/repl.rs
Normal file
369
crates/uad-cli/src/repl.rs
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
use rustyline::DefaultEditor;
|
||||
use rustyline::error::ReadlineError;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use uad_core::adb::ACommand;
|
||||
use uad_core::sync::{CorePackage, Phone, User, apply_pkg_state_commands, get_package_state};
|
||||
use uad_core::uad_lists::{Package, PackageState, Removal, UadList, load_debloat_lists};
|
||||
|
||||
use crate::commands::{PackageListContext, display_package_list, execute_with_fallback};
|
||||
use crate::device::{get_target_device, get_user};
|
||||
use crate::filters::StateFilter;
|
||||
use crate::println_or_exit;
|
||||
|
||||
/// Start interactive REPL mode
|
||||
pub fn repl_mode(
|
||||
device: Option<String>,
|
||||
user_id: Option<u16>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Universal Android Debloater - Interactive Mode");
|
||||
println!("Type 'help' for available commands, 'exit' or 'quit' to leave\n");
|
||||
|
||||
let target_device = get_target_device(device)?;
|
||||
let user = get_user(&target_device, user_id)?;
|
||||
|
||||
println!(
|
||||
"Connected to: {} ({})",
|
||||
target_device.model, target_device.adb_id
|
||||
);
|
||||
println!("User: {}\n", user.id);
|
||||
|
||||
let uad_lists = load_debloat_lists(false).unwrap_or_else(|lists| lists);
|
||||
let mut rl = DefaultEditor::new()?;
|
||||
|
||||
// Try to load history
|
||||
let history_file = dirs::cache_dir().map(|d| d.join("uad").join("cli_history.txt"));
|
||||
if let Some(ref path) = history_file {
|
||||
let _ = rl.load_history(path);
|
||||
}
|
||||
|
||||
loop {
|
||||
let readline = rl.readline("uad> ");
|
||||
match readline {
|
||||
Ok(line) => {
|
||||
if let Err(e) =
|
||||
handle_repl_line(&line, &mut rl, &target_device, user, user_id, &uad_lists)
|
||||
{
|
||||
if e.to_string() == "exit" {
|
||||
break;
|
||||
}
|
||||
eprintln!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
Err(ReadlineError::Interrupted) => {
|
||||
println!("Interrupted (Ctrl-C). Type 'exit' to quit.");
|
||||
continue;
|
||||
}
|
||||
Err(ReadlineError::Eof) => {
|
||||
println!("Goodbye!");
|
||||
break;
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Error: {:?}", err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save history
|
||||
if let Some(ref path) = history_file {
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
let _ = rl.save_history(path);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle a single line of REPL input
|
||||
fn handle_repl_line(
|
||||
line: &str,
|
||||
rl: &mut DefaultEditor,
|
||||
device: &Phone,
|
||||
user: User,
|
||||
user_id: Option<u16>,
|
||||
uad_lists: &HashMap<String, Package>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let _ = rl.add_history_entry(line);
|
||||
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match parts[0] {
|
||||
"help" => print_repl_help(),
|
||||
"exit" | "quit" => {
|
||||
println!("Goodbye!");
|
||||
return Err("exit".into());
|
||||
}
|
||||
"list" | "ls" => {
|
||||
handle_list_command(&parts[1..], device, user_id, uad_lists)?;
|
||||
}
|
||||
"info" => {
|
||||
handle_info_command(&parts[1..], device, uad_lists)?;
|
||||
}
|
||||
"uninstall" | "rm" => {
|
||||
handle_state_change_command(
|
||||
&parts[1..],
|
||||
device,
|
||||
user,
|
||||
PackageState::Uninstalled,
|
||||
"Uninstalling",
|
||||
uad_lists,
|
||||
)?;
|
||||
}
|
||||
"enable" | "restore" => {
|
||||
handle_state_change_command(
|
||||
&parts[1..],
|
||||
device,
|
||||
user,
|
||||
PackageState::Enabled,
|
||||
"Enabling",
|
||||
uad_lists,
|
||||
)?;
|
||||
}
|
||||
"disable" => {
|
||||
handle_state_change_command(
|
||||
&parts[1..],
|
||||
device,
|
||||
user,
|
||||
PackageState::Disabled,
|
||||
"Disabling",
|
||||
uad_lists,
|
||||
)?;
|
||||
}
|
||||
"device" => {
|
||||
println!(
|
||||
"Device: {} ({}), Android SDK: {}, User: {}",
|
||||
device.model, device.adb_id, device.android_sdk, user.id
|
||||
);
|
||||
}
|
||||
"clear" => {
|
||||
print!("\x1B[2J\x1B[1;1H");
|
||||
}
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Unknown command: '{}'. Type 'help' for available commands.",
|
||||
parts[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parse REPL arguments into filters
|
||||
struct ReplListArgs {
|
||||
state_filter: Option<StateFilter>,
|
||||
search_term: Option<String>,
|
||||
}
|
||||
|
||||
impl ReplListArgs {
|
||||
fn parse(args: &[&str]) -> Result<Self, String> {
|
||||
let mut state_filter = None;
|
||||
let mut search_term = None;
|
||||
let mut i = 0;
|
||||
|
||||
while i < args.len() {
|
||||
match args[i] {
|
||||
"--state" | "-s" => {
|
||||
if i + 1 >= args.len() {
|
||||
return Err("--state requires a value".to_string());
|
||||
}
|
||||
state_filter = match args[i + 1].to_lowercase().as_str() {
|
||||
"enabled" => Some(StateFilter::Enabled),
|
||||
"disabled" => Some(StateFilter::Disabled),
|
||||
"uninstalled" => Some(StateFilter::Uninstalled),
|
||||
"all" => Some(StateFilter::All),
|
||||
_ => return Err(format!("Invalid state: {}", args[i + 1])),
|
||||
};
|
||||
i += 2;
|
||||
}
|
||||
"--search" | "-q" => {
|
||||
if i + 1 >= args.len() {
|
||||
return Err("--search requires a value".to_string());
|
||||
}
|
||||
search_term = Some(args[i + 1].to_string());
|
||||
i += 2;
|
||||
}
|
||||
_ => {
|
||||
return Err(format!("Unknown option: {}", args[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
state_filter,
|
||||
search_term,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle list command in REPL
|
||||
fn handle_list_command(
|
||||
args: &[&str],
|
||||
device: &Phone,
|
||||
user_id: Option<u16>,
|
||||
uad_lists: &HashMap<String, Package>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let parsed = ReplListArgs::parse(args)?;
|
||||
|
||||
let pm_flag = parsed.state_filter.and_then(StateFilter::to_pm_flag);
|
||||
let system_packages = ACommand::new()
|
||||
.shell(&device.adb_id)
|
||||
.pm()
|
||||
.list_packages_sys(pm_flag, user_id)?;
|
||||
|
||||
let context = PackageListContext {
|
||||
state_filter: parsed.state_filter,
|
||||
removal_filter: None,
|
||||
list_filter: None,
|
||||
search: parsed.search_term,
|
||||
};
|
||||
|
||||
let displayed_count = display_package_list(
|
||||
&system_packages,
|
||||
uad_lists,
|
||||
&device.adb_id,
|
||||
user_id,
|
||||
&context,
|
||||
)?;
|
||||
|
||||
if displayed_count == 0 {
|
||||
println_or_exit!(" No packages found.");
|
||||
} else {
|
||||
println_or_exit!("\nTotal: {} package(s)", displayed_count);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle info command in REPL
|
||||
fn handle_info_command(
|
||||
args: &[&str],
|
||||
device: &Phone,
|
||||
uad_lists: &HashMap<String, Package>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if args.is_empty() {
|
||||
eprintln!("Usage: info <package_name>");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let package = args[0];
|
||||
println!("Package: {}", package);
|
||||
|
||||
if let Some(pkg_info) = uad_lists.get(package) {
|
||||
println!(" List: {}", pkg_info.list);
|
||||
println!(" Removal: {}", pkg_info.removal);
|
||||
println!(" Description: {}", pkg_info.description);
|
||||
} else {
|
||||
println!(" Not found in UAD lists (unlisted package)");
|
||||
}
|
||||
|
||||
let state =
|
||||
get_package_state(&device.adb_id, package, None).ok_or("Package not found on device")?;
|
||||
println!(" State: {}", state);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle state change command in REPL
|
||||
fn handle_state_change_command(
|
||||
args: &[&str],
|
||||
device: &Phone,
|
||||
user: User,
|
||||
target_state: PackageState,
|
||||
action_name: &str,
|
||||
uad_lists: &HashMap<String, Package>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
if args.is_empty() {
|
||||
eprintln!(
|
||||
"Usage: {} <package_name> [package_name...]",
|
||||
action_name.to_lowercase()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for pkg_name in args {
|
||||
process_package_change(pkg_name, device, user, target_state, action_name, uad_lists)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Process state change for a single package in REPL
|
||||
fn process_package_change(
|
||||
pkg_name: &str,
|
||||
device: &Phone,
|
||||
user: User,
|
||||
target_state: PackageState,
|
||||
action_name: &str,
|
||||
uad_lists: &HashMap<String, Package>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let current_state = get_package_state(&device.adb_id, pkg_name, Some(user.id))
|
||||
.ok_or("Package not found on device")?;
|
||||
|
||||
println!("{} {} (current: {})", action_name, pkg_name, current_state);
|
||||
|
||||
if current_state == target_state {
|
||||
println!(" → Already in target state, skipping");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let pkg_info = uad_lists.get(pkg_name);
|
||||
if let Some(info) = pkg_info {
|
||||
if info.removal == Removal::Unsafe {
|
||||
println!(" ⚠ WARNING: This package is marked as UNSAFE to remove!");
|
||||
}
|
||||
}
|
||||
|
||||
let core_pkg = CorePackage {
|
||||
name: pkg_name.to_string(),
|
||||
description: pkg_info.map(|p| p.description.clone()).unwrap_or_default(),
|
||||
removal: pkg_info.map(|p| p.removal).unwrap_or(Removal::Unlisted),
|
||||
state: current_state,
|
||||
list: pkg_info.map(|p| p.list).unwrap_or(UadList::Unlisted),
|
||||
};
|
||||
|
||||
let commands = apply_pkg_state_commands(&core_pkg, target_state, user, device);
|
||||
|
||||
execute_with_fallback(
|
||||
pkg_name,
|
||||
target_state,
|
||||
&core_pkg,
|
||||
user,
|
||||
device,
|
||||
&commands,
|
||||
" ",
|
||||
)
|
||||
}
|
||||
|
||||
/// Print REPL help message
|
||||
fn print_repl_help() {
|
||||
println!("Available commands:");
|
||||
println!(" list [--state <state>] [--search <term>]");
|
||||
println!(" List packages with optional filters");
|
||||
println!(" info <package_name>");
|
||||
println!(" Show information about a package");
|
||||
println!(" uninstall <package_name> [package_name...]");
|
||||
println!(" Uninstall one or more packages");
|
||||
println!(" enable <package_name> [package_name...]");
|
||||
println!(" Enable/restore one or more packages");
|
||||
println!(" disable <package_name> [package_name...]");
|
||||
println!(" Disable one or more packages");
|
||||
println!(" device");
|
||||
println!(" Show current device information");
|
||||
println!(" clear");
|
||||
println!(" Clear the screen");
|
||||
println!(" help");
|
||||
println!(" Show this help message");
|
||||
println!(" exit, quit");
|
||||
println!(" Exit the interactive mode");
|
||||
}
|
||||
56
crates/uad-core/Cargo.toml
Normal file
56
crates/uad-core/Cargo.toml
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
[package]
|
||||
name = "uad-core"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
readme.workspace = true
|
||||
keywords.workspace = true
|
||||
categories = ["command-line-utilities"]
|
||||
edition.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
gui = ["dep:iced", "dep:dark-light"]
|
||||
self-update = ["dep:flate2", "dep:tar"]
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
log.workspace = true
|
||||
chrono.workspace = true
|
||||
toml.workspace = true
|
||||
dirs.workspace = true
|
||||
ureq.workspace = true
|
||||
retry.workspace = true
|
||||
csv.workspace = true
|
||||
iced = { workspace = true, optional = true }
|
||||
dark-light = { workspace = true, optional = true }
|
||||
flate2 = { workspace = true, optional = true }
|
||||
tar = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
win32console.workspace = true
|
||||
|
||||
[lints.rust]
|
||||
deprecated_safe = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
undocumented_unsafe_blocks = "forbid"
|
||||
exit = "deny"
|
||||
panic_in_result_fn = "warn"
|
||||
infinite_loop = "warn"
|
||||
mem_forget = "warn"
|
||||
implicit_clone = "warn"
|
||||
format_push_string = "warn"
|
||||
large_include_file = "warn"
|
||||
shadow_unrelated = "warn"
|
||||
struct_field_names = "allow" # annoying
|
||||
module_name_repetitions = "allow" # annoying
|
||||
|
||||
disallowed_types = "deny"
|
||||
disallowed_methods = "deny"
|
||||
|
||||
allow_attributes_without_reason = "warn"
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
|
|
@ -43,16 +43,18 @@ use serde::{Deserialize, Serialize};
|
|||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
use crate::core::utils::is_all_w_c;
|
||||
use crate::utils::is_all_w_c;
|
||||
use log::{error, info};
|
||||
|
||||
/// Convert ADB output bytes to a trimmed UTF-8 string.
|
||||
/// Uses lossy conversion to prevent panics on non-UTF8 output from certain OEMs.
|
||||
#[must_use]
|
||||
pub fn to_trimmed_utf8(v: &[u8]) -> String {
|
||||
String::from_utf8_lossy(v).trim_end().to_string()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[cfg(debug_assertions)]
|
||||
#[must_use]
|
||||
fn is_version_triple(s: &str) -> bool {
|
||||
let mut components = s.split('.');
|
||||
for _ in 0..3 {
|
||||
|
|
@ -213,6 +215,12 @@ impl ACommand {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for ACommand {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder object for a command that runs on the device's default `sh` implementation.
|
||||
/// Typically MKSH, but could be Ash.
|
||||
///
|
||||
|
|
@ -221,6 +229,7 @@ impl ACommand {
|
|||
pub struct ShellCommand(ACommand);
|
||||
impl ShellCommand {
|
||||
/// `pm` command builder
|
||||
#[must_use]
|
||||
pub fn pm(mut self) -> PmCommand {
|
||||
self.0.0.arg("pm");
|
||||
PmCommand(self)
|
||||
|
|
@ -273,6 +282,7 @@ impl PackageId {
|
|||
/// Creates a package-ID if it's valid according to:
|
||||
/// - <https://developer.android.com/guide/topics/manifest/manifest-element.html#package>
|
||||
/// - <https://developer.android.com/build/configure-app-module#set-application-id>
|
||||
#[must_use]
|
||||
pub fn new(p_id: Box<str>) -> Option<Self> {
|
||||
let mut components = p_id.split('.');
|
||||
for _ in 0..2 {
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
use crate::CACHE_DIR;
|
||||
use crate::CONFIG_DIR;
|
||||
use crate::core::utils::DisplayablePath;
|
||||
use crate::core::{sync::User, theme::Theme};
|
||||
use crate::gui::views::settings::Settings;
|
||||
use crate::utils::DisplayablePath;
|
||||
use crate::{sync::User, theme::Theme};
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
|
@ -54,23 +54,26 @@ impl Default for GeneralSettings {
|
|||
static CONFIG_FILE: LazyLock<PathBuf> = LazyLock::new(|| CONFIG_DIR.join("config.toml"));
|
||||
|
||||
impl Config {
|
||||
pub fn save_changes(settings: &Settings, device_id: &String) {
|
||||
let mut config = Self::load_configuration_file();
|
||||
if let Some(device) = config
|
||||
pub fn save_device_settings(
|
||||
&mut self,
|
||||
device_settings: DeviceSettings,
|
||||
general: GeneralSettings,
|
||||
) {
|
||||
if let Some(device) = self
|
||||
.devices
|
||||
.iter_mut()
|
||||
.find(|x| x.device_id == *device_id)
|
||||
.find(|x| x.device_id == device_settings.device_id)
|
||||
{
|
||||
device.clone_from(&settings.device);
|
||||
*device = device_settings;
|
||||
} else {
|
||||
debug!("config: New device settings saved");
|
||||
config.devices.push(settings.device.clone());
|
||||
self.devices.push(device_settings);
|
||||
}
|
||||
config.general.clone_from(&settings.general);
|
||||
let toml = toml::to_string(&config).unwrap();
|
||||
self.general = general;
|
||||
let toml = toml::to_string(&self).unwrap();
|
||||
fs::write(&*CONFIG_FILE, toml).expect("Could not write config file to disk!");
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn load_configuration_file() -> Self {
|
||||
match fs::read_to_string(&*CONFIG_FILE) {
|
||||
Ok(s) => match toml::from_str(&s) {
|
||||
24
crates/uad-core/src/lib.rs
Normal file
24
crates/uad-core/src/lib.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#![allow(
|
||||
clippy::missing_panics_doc,
|
||||
clippy::missing_errors_doc,
|
||||
clippy::collapsible_if,
|
||||
clippy::uninlined_format_args,
|
||||
clippy::result_unit_err,
|
||||
reason = "Doc+style pedantic lints are out-of-scope for this pass"
|
||||
)]
|
||||
|
||||
pub mod adb;
|
||||
pub mod config;
|
||||
pub mod save;
|
||||
pub mod sync;
|
||||
pub mod theme;
|
||||
pub mod uad_lists;
|
||||
pub mod update;
|
||||
pub mod utils;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
pub static CONFIG_DIR: LazyLock<PathBuf> =
|
||||
LazyLock::new(|| utils::setup_uad_dir(&dirs::config_dir().expect("Can't detect config dir")));
|
||||
pub static CACHE_DIR: LazyLock<PathBuf> =
|
||||
LazyLock::new(|| utils::setup_uad_dir(&dirs::cache_dir().expect("Can't detect cache dir")));
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
use crate::core::config::{Config, DeviceSettings};
|
||||
use crate::core::sync::{CorePackage, Phone, User, apply_pkg_state_commands};
|
||||
use crate::core::utils::DisplayablePath;
|
||||
use crate::gui::widgets::package_row::PackageRow;
|
||||
use crate::config::{Config, DeviceSettings};
|
||||
use crate::sync::{CorePackage, Phone, User, apply_pkg_state_commands};
|
||||
use crate::utils::DisplayablePath;
|
||||
use log::{error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
|
|
@ -22,10 +21,10 @@ pub struct UserBackup {
|
|||
}
|
||||
|
||||
/// Backup all `Uninstalled` and `Disabled` packages
|
||||
pub async fn backup_phone(
|
||||
pub fn backup_phone(
|
||||
users: Vec<User>,
|
||||
device_id: String,
|
||||
phone_packages: Vec<Vec<PackageRow>>,
|
||||
phone_packages: &[Vec<CorePackage>],
|
||||
) -> Result<bool, String> {
|
||||
let mut backup = PhoneBackup {
|
||||
device_id: device_id.clone(),
|
||||
|
|
@ -38,11 +37,8 @@ pub async fn backup_phone(
|
|||
..UserBackup::default()
|
||||
};
|
||||
|
||||
for p in phone_packages[u.index].clone() {
|
||||
user_backup.packages.push(CorePackage {
|
||||
name: p.name.clone(),
|
||||
state: p.state,
|
||||
});
|
||||
for p in phone_packages[u.index].iter().cloned() {
|
||||
user_backup.packages.push(p);
|
||||
}
|
||||
backup.users.push(user_backup);
|
||||
}
|
||||
|
|
@ -79,6 +75,7 @@ pub fn list_available_backups(dir: &Path) -> Vec<DisplayablePath> {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn list_available_backup_user(backup: DisplayablePath) -> Vec<User> {
|
||||
match fs::read_to_string(backup.path) {
|
||||
Ok(data) => serde_json::from_str::<PhoneBackup>(&data)
|
||||
|
|
@ -113,7 +110,7 @@ pub struct RestoreResult {
|
|||
|
||||
pub fn restore_backup(
|
||||
selected_device: &Phone,
|
||||
packages: &[Vec<PackageRow>],
|
||||
packages: &[Vec<CorePackage>],
|
||||
settings: &DeviceSettings,
|
||||
) -> Result<RestoreResult, String> {
|
||||
match fs::read_to_string(
|
||||
|
|
@ -142,7 +139,7 @@ pub fn restore_backup(
|
|||
.iter()
|
||||
.find(|x| x.name == backup_package.name)
|
||||
{
|
||||
p.into()
|
||||
p.clone()
|
||||
} else {
|
||||
skipped_packages += 1;
|
||||
warn!(
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
use crate::core::{
|
||||
use crate::{
|
||||
adb::{ACommand as AdbCommand, PM_CLEAR_PACK},
|
||||
uad_lists::PackageState,
|
||||
};
|
||||
use crate::gui::{views::list::PackageInfo, widgets::package_row::PackageRow};
|
||||
use log::{error, info};
|
||||
use retry::{OperationResult, delay::Fixed, retry};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
|
@ -61,27 +61,25 @@ pub enum AdbError {
|
|||
/// This replaces the deprecated `adb_shell_command`.
|
||||
///
|
||||
/// If `serial` is empty, it lets ADB choose the default device.
|
||||
pub async fn run_adb_action<S: AsRef<str>>(
|
||||
pub fn run_adb_shell_action<S: AsRef<str>>(
|
||||
device_serial: S,
|
||||
action: String,
|
||||
p: PackageInfo,
|
||||
) -> Result<PackageInfo, AdbError> {
|
||||
action: &str,
|
||||
) -> Result<String, AdbError> {
|
||||
let serial = device_serial.as_ref();
|
||||
let label = &p.removal;
|
||||
|
||||
match AdbCommand::new().shell(serial).raw(&action) {
|
||||
match AdbCommand::new().shell(serial).raw(action) {
|
||||
Ok(o) => {
|
||||
if ["Error", "Failure"].iter().any(|&e| o.contains(e)) {
|
||||
let friendly_msg = make_friendly_error_message(&o, &action);
|
||||
return Err(AdbError::Generic(format!("[{label}] {friendly_msg}")));
|
||||
let friendly_msg = make_friendly_error_message(&o, action);
|
||||
return Err(AdbError::Generic(friendly_msg));
|
||||
}
|
||||
info!("[{label}] {action} -> {o}");
|
||||
Ok(p)
|
||||
info!("{action} -> {o}");
|
||||
Ok(o)
|
||||
}
|
||||
Err(err) => {
|
||||
if !err.contains("[not installed for") {
|
||||
let friendly_msg = make_friendly_error_message(&err, &action);
|
||||
return Err(AdbError::Generic(format!("[{label}] {friendly_msg}")));
|
||||
let friendly_msg = make_friendly_error_message(&err, action);
|
||||
return Err(AdbError::Generic(friendly_msg));
|
||||
}
|
||||
Err(AdbError::Generic(err))
|
||||
}
|
||||
|
|
@ -141,6 +139,7 @@ fn make_friendly_error_message(error_output: &str, action: &str) -> String {
|
|||
}
|
||||
|
||||
/// If `None`, returns an empty String, not " --user 0"
|
||||
#[must_use]
|
||||
pub fn user_flag(user_id: Option<User>) -> String {
|
||||
user_id
|
||||
.map(|user| format!(" --user {}", user.id))
|
||||
|
|
@ -151,35 +150,16 @@ pub fn user_flag(user_id: Option<User>) -> String {
|
|||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct CorePackage {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub removal: crate::uad_lists::Removal,
|
||||
#[serde(default)]
|
||||
pub state: PackageState,
|
||||
#[serde(default)]
|
||||
pub list: crate::uad_lists::UadList,
|
||||
}
|
||||
|
||||
impl From<&mut PackageRow> for CorePackage {
|
||||
fn from(pr: &mut PackageRow) -> Self {
|
||||
Self {
|
||||
name: pr.name.clone(),
|
||||
state: pr.state,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<PackageRow> for CorePackage {
|
||||
fn from(pr: PackageRow) -> Self {
|
||||
Self {
|
||||
name: pr.name.clone(),
|
||||
state: pr.state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PackageRow> for CorePackage {
|
||||
fn from(pr: &PackageRow) -> Self {
|
||||
Self {
|
||||
name: pr.name.clone(),
|
||||
state: pr.state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn apply_pkg_state_commands(
|
||||
package: &CorePackage,
|
||||
wanted_state: PackageState,
|
||||
|
|
@ -224,6 +204,7 @@ pub fn apply_pkg_state_commands(
|
|||
/// Build a command request to be sent via ADB to a device.
|
||||
/// `commands` accepts one or more ADB shell commands
|
||||
/// which act on a common `package` and `user`.
|
||||
#[must_use]
|
||||
pub fn request_builder(commands: &[&str], package: &str, user: Option<User>) -> Vec<String> {
|
||||
let maybe_user_flag = user_flag(user);
|
||||
commands
|
||||
|
|
@ -235,6 +216,7 @@ pub fn request_builder(commands: &[&str], package: &str, user: Option<User>) ->
|
|||
/// Get the model by querying the `ro.product.model` property.
|
||||
///
|
||||
/// If `serial` is empty, it lets ADB choose the default device.
|
||||
#[must_use]
|
||||
pub fn get_device_model(serial: &str) -> String {
|
||||
AdbCommand::new()
|
||||
.shell(serial)
|
||||
|
|
@ -253,6 +235,7 @@ pub fn get_device_model(serial: &str) -> String {
|
|||
/// Get the brand by querying the `ro.product.brand` property.
|
||||
///
|
||||
/// If `serial` is empty, it lets ADB choose the default device.
|
||||
#[must_use]
|
||||
pub fn get_device_brand(serial: &str) -> String {
|
||||
AdbCommand::new()
|
||||
.shell(serial)
|
||||
|
|
@ -266,6 +249,7 @@ pub fn get_device_brand(serial: &str) -> String {
|
|||
// `ro.build.version.sdk` property or defaulting to 0.
|
||||
///
|
||||
/// If `device_serial` is empty, it lets ADB choose the default device.
|
||||
#[must_use]
|
||||
pub fn get_android_sdk(device_serial: &str) -> u8 {
|
||||
AdbCommand::new()
|
||||
.shell(device_serial)
|
||||
|
|
@ -280,6 +264,7 @@ pub fn get_android_sdk(device_serial: &str) -> u8 {
|
|||
///
|
||||
/// Only includes users where the package exists (Some state). Users where the package
|
||||
/// doesn't exist (None) are not tracked.
|
||||
#[must_use]
|
||||
pub fn capture_cross_user_states(
|
||||
package_name: &str,
|
||||
device_serial: &str,
|
||||
|
|
@ -301,6 +286,7 @@ pub fn capture_cross_user_states(
|
|||
/// - Case A: Uninstall → Restore (package appears on other users)
|
||||
/// - Case B: Uninstall → Uninstall (package disappears from other users that previously had it)
|
||||
/// - Case C: Restore → Restore (package appears on other users)
|
||||
#[must_use]
|
||||
pub fn detect_cross_user_behavior(
|
||||
package_name: &str,
|
||||
device_serial: &str,
|
||||
|
|
@ -433,6 +419,7 @@ pub fn is_protected_user<S: AsRef<str>>(user_id: u16, device_serial: S) -> bool
|
|||
.is_err()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn list_users_idx_prot(device_serial: &str) -> Vec<User> {
|
||||
AdbCommand::new()
|
||||
.shell(device_serial)
|
||||
|
|
@ -456,9 +443,10 @@ pub fn list_users_idx_prot(device_serial: &str) -> Vec<User> {
|
|||
|
||||
/// This matches serials (`getprop ro.serialno`)
|
||||
/// that are authorized by the user.
|
||||
pub async fn get_devices_list() -> Vec<Phone> {
|
||||
#[must_use]
|
||||
pub fn get_devices_list() -> Vec<Phone> {
|
||||
retry(
|
||||
Fixed::from_millis(500).take(if cfg!(debug_assertions) { 3 } else { 120 }),
|
||||
Fixed::from_millis(500).take(if cfg!(debug_assertions) { 3 } else { 10 }),
|
||||
|| match AdbCommand::new().devices() {
|
||||
Ok(devices) => {
|
||||
let mut device_list: Vec<Phone> = vec![];
|
||||
|
|
@ -486,20 +474,22 @@ pub async fn get_devices_list() -> Vec<Phone> {
|
|||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub async fn initial_load() -> bool {
|
||||
#[must_use]
|
||||
pub fn initial_load() -> bool {
|
||||
match AdbCommand::new().devices() {
|
||||
Ok(_devices) => true,
|
||||
Err(_err) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify the actual state of a package on the device
|
||||
pub fn verify_package_state(
|
||||
package_name: &str,
|
||||
/// Get the current state of a package on a device
|
||||
#[must_use]
|
||||
pub fn get_package_state(
|
||||
device_serial: &str,
|
||||
package_name: &str,
|
||||
user_id: Option<u16>,
|
||||
) -> Option<PackageState> {
|
||||
use crate::core::adb::{ACommand as AdbCommand, PmListPacksFlag};
|
||||
use crate::adb::{ACommand as AdbCommand, PmListPacksFlag};
|
||||
|
||||
// Check if package is enabled
|
||||
if let Ok(enabled_packages) = AdbCommand::new()
|
||||
|
|
@ -535,11 +525,23 @@ pub fn verify_package_state(
|
|||
None
|
||||
}
|
||||
|
||||
/// Verify the current state of a package on a device.
|
||||
/// Returns the package state, or `None` if the package doesn't exist.
|
||||
#[must_use]
|
||||
pub fn verify_package_state(
|
||||
package_name: &str,
|
||||
device_serial: &str,
|
||||
user_id: Option<u16>,
|
||||
) -> Option<PackageState> {
|
||||
get_package_state(device_serial, package_name, user_id)
|
||||
}
|
||||
|
||||
/// Check if a package exists on any other users besides the target user.
|
||||
/// This helps detect OEM-specific cross-user restoration behavior.
|
||||
///
|
||||
/// Only includes users where the package exists in a non-uninstalled state
|
||||
/// (i.e., Enabled or Disabled).
|
||||
#[must_use]
|
||||
pub fn check_cross_user_package_existence(
|
||||
package_name: &str,
|
||||
device_serial: &str,
|
||||
|
|
@ -561,9 +563,29 @@ pub fn check_cross_user_package_existence(
|
|||
other_user_states
|
||||
}
|
||||
|
||||
/// Creates a `CorePackage` with the specified state, preserving other fields from the original.
|
||||
fn package_with_state(package: &CorePackage, state: PackageState) -> CorePackage {
|
||||
CorePackage {
|
||||
state,
|
||||
..package.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the first available command and returns the result.
|
||||
fn execute_first_command(commands: &[String], phone: &Phone) -> Result<(), String> {
|
||||
if commands.is_empty() {
|
||||
return Err("No command available".to_string());
|
||||
}
|
||||
AdbCommand::new()
|
||||
.shell(&phone.adb_id)
|
||||
.raw(&commands[0])
|
||||
.map(|_| ())
|
||||
.map_err(|err| err.clone())
|
||||
}
|
||||
|
||||
/// Attempt fallback action when package state verification fails
|
||||
pub fn attempt_fallback(
|
||||
package: &crate::gui::widgets::package_row::PackageRow,
|
||||
package: &CorePackage,
|
||||
wanted_state: PackageState,
|
||||
actual_state: PackageState,
|
||||
user: User,
|
||||
|
|
@ -572,101 +594,49 @@ pub fn attempt_fallback(
|
|||
match (wanted_state, actual_state) {
|
||||
// Case 1: Tried to uninstall but package was reinstalled -> disable it
|
||||
(PackageState::Uninstalled, PackageState::Enabled) => {
|
||||
let core_package = CorePackage {
|
||||
name: package.name.clone(),
|
||||
state: PackageState::Enabled,
|
||||
};
|
||||
let commands =
|
||||
apply_pkg_state_commands(&core_package, PackageState::Disabled, user, phone);
|
||||
|
||||
if commands.is_empty() {
|
||||
Err("No disable command available for this Android version".to_string())
|
||||
} else {
|
||||
// Execute the disable command
|
||||
let action = commands[0].clone();
|
||||
match AdbCommand::new().shell(&phone.adb_id).raw(&action) {
|
||||
Ok(_) => Ok("disabled package instead of uninstalling".to_string()),
|
||||
Err(err) => Err(format!("Failed to disable package: {err}")),
|
||||
}
|
||||
}
|
||||
let pkg = package_with_state(package, PackageState::Enabled);
|
||||
let commands = apply_pkg_state_commands(&pkg, PackageState::Disabled, user, phone);
|
||||
execute_first_command(&commands, phone)
|
||||
.map(|()| "disabled package instead of uninstalling".to_string())
|
||||
.map_err(|e| format!("Failed to disable package: {e}"))
|
||||
}
|
||||
|
||||
// Case 2: Tried to disable but package re-enabled itself -> try uninstall
|
||||
(PackageState::Disabled, PackageState::Enabled) => {
|
||||
let core_package = CorePackage {
|
||||
name: package.name.clone(),
|
||||
state: PackageState::Enabled,
|
||||
};
|
||||
let commands =
|
||||
apply_pkg_state_commands(&core_package, PackageState::Uninstalled, user, phone);
|
||||
let pkg = package_with_state(package, PackageState::Enabled);
|
||||
let commands = apply_pkg_state_commands(&pkg, PackageState::Uninstalled, user, phone);
|
||||
execute_first_command(&commands, phone)
|
||||
.map_err(|e| format!("Failed to uninstall: {e}"))?;
|
||||
|
||||
if commands.is_empty() {
|
||||
Err("No uninstall command available for this Android version".to_string())
|
||||
} else {
|
||||
// Execute the uninstall command
|
||||
let action = commands[0].clone();
|
||||
match AdbCommand::new().shell(&phone.adb_id).raw(&action) {
|
||||
Ok(_) => {
|
||||
// Verify the package was actually uninstalled
|
||||
match verify_package_state(&package.name, &phone.adb_id, Some(user.id)) {
|
||||
Some(PackageState::Uninstalled) | None => {
|
||||
Ok("uninstalled package instead of disabling".to_string())
|
||||
}
|
||||
_ => Err("Package still exists after uninstall attempt".to_string()),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(format!("Failed to uninstall package: {err}")),
|
||||
// Verify the package was actually uninstalled
|
||||
match verify_package_state(&package.name, &phone.adb_id, Some(user.id)) {
|
||||
Some(PackageState::Uninstalled) | None => {
|
||||
Ok("uninstalled package instead of disabling".to_string())
|
||||
}
|
||||
_ => Err("Package still exists after uninstall attempt".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// Case 3: Tried to enable but package was disabled -> try uninstall then reinstall
|
||||
(PackageState::Enabled, PackageState::Disabled) => {
|
||||
// First try to uninstall
|
||||
let core_package = CorePackage {
|
||||
name: package.name.clone(),
|
||||
state: PackageState::Disabled,
|
||||
};
|
||||
let uninstall_commands =
|
||||
apply_pkg_state_commands(&core_package, PackageState::Uninstalled, user, phone);
|
||||
let pkg = package_with_state(package, PackageState::Disabled);
|
||||
let uninstall_cmds =
|
||||
apply_pkg_state_commands(&pkg, PackageState::Uninstalled, user, phone);
|
||||
execute_first_command(&uninstall_cmds, phone)
|
||||
.map_err(|e| format!("Failed to uninstall for reinstall: {e}"))?;
|
||||
|
||||
if uninstall_commands.is_empty() {
|
||||
Err("No uninstall command available for reinstall attempt".to_string())
|
||||
} else {
|
||||
let uninstall_action = uninstall_commands[0].clone();
|
||||
match AdbCommand::new()
|
||||
.shell(&phone.adb_id)
|
||||
.raw(&uninstall_action)
|
||||
{
|
||||
Ok(_) => {
|
||||
// Now try to reinstall/enable
|
||||
let core_package_uninstalled = CorePackage {
|
||||
name: package.name.clone(),
|
||||
state: PackageState::Uninstalled,
|
||||
};
|
||||
let enable_commands = apply_pkg_state_commands(
|
||||
&core_package_uninstalled,
|
||||
PackageState::Enabled,
|
||||
user,
|
||||
phone,
|
||||
);
|
||||
// Now try to reinstall/enable
|
||||
let pkg_uninstalled = package_with_state(package, PackageState::Uninstalled);
|
||||
let enable_cmds =
|
||||
apply_pkg_state_commands(&pkg_uninstalled, PackageState::Enabled, user, phone);
|
||||
|
||||
if enable_commands.is_empty() {
|
||||
Ok("uninstalled package but couldn't reinstall".to_string())
|
||||
} else {
|
||||
let enable_action = enable_commands[0].clone();
|
||||
match AdbCommand::new().shell(&phone.adb_id).raw(&enable_action) {
|
||||
Ok(_) => {
|
||||
Ok("uninstalled and reinstalled package to enable it"
|
||||
.to_string())
|
||||
}
|
||||
Err(err) => Err(format!("Failed to reinstall package: {err}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => Err(format!("Failed to uninstall package for reinstall: {err}")),
|
||||
}
|
||||
if enable_cmds.is_empty() {
|
||||
return Ok("uninstalled package but couldn't reinstall".to_string());
|
||||
}
|
||||
|
||||
execute_first_command(&enable_cmds, phone)
|
||||
.map(|()| "uninstalled and reinstalled package to enable it".to_string())
|
||||
.map_err(|e| format!("Failed to reinstall package: {e}"))
|
||||
}
|
||||
|
||||
// Other cases - no fallback available
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
#[cfg(feature = "gui")]
|
||||
use dark_light;
|
||||
#[cfg(feature = "gui")]
|
||||
use iced::theme::{self, Mode, Palette, Style};
|
||||
#[cfg(feature = "gui")]
|
||||
use iced::{Color, color};
|
||||
#[cfg(feature = "gui")]
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/*
|
||||
|
|
@ -12,6 +16,7 @@ Coincidentally, this also ensures consistent colors across the GUI,
|
|||
at the cost of requiring a restart to update the palette.
|
||||
(this is just a patch, not a fix)
|
||||
*/
|
||||
#[cfg(feature = "gui")]
|
||||
pub static OS_COLOR_SCHEME: LazyLock<dark_light::Mode> =
|
||||
LazyLock::new(|| dark_light::detect().unwrap_or(dark_light::Mode::Unspecified));
|
||||
|
||||
|
|
@ -29,12 +34,14 @@ pub enum Theme {
|
|||
Light,
|
||||
}
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BaseColors {
|
||||
pub background: Color,
|
||||
pub foreground: Color,
|
||||
}
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct NormalColors {
|
||||
pub primary: Color,
|
||||
|
|
@ -44,6 +51,7 @@ pub struct NormalColors {
|
|||
pub error: Color,
|
||||
}
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BrightColors {
|
||||
pub primary: Color,
|
||||
|
|
@ -52,6 +60,7 @@ pub struct BrightColors {
|
|||
pub error: Color,
|
||||
}
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ColorPalette {
|
||||
pub base: BaseColors,
|
||||
|
|
@ -69,6 +78,7 @@ impl Theme {
|
|||
/// This `fn` _could_ be `const`,
|
||||
/// but `deref`ing a lazy-`static` is non-`const`.
|
||||
#[must_use]
|
||||
#[cfg(feature = "gui")]
|
||||
pub fn palette(self) -> ColorPalette {
|
||||
const DARK: ColorPalette = ColorPalette {
|
||||
base: BaseColors {
|
||||
|
|
@ -151,6 +161,18 @@ impl std::fmt::Display for Theme {
|
|||
}
|
||||
}
|
||||
|
||||
/// Converts a string to the GUI's Theme type
|
||||
#[must_use]
|
||||
pub fn string_to_theme(theme: &str) -> Theme {
|
||||
match theme {
|
||||
"Lupin" => Theme::Lupin,
|
||||
"Dark" => Theme::Dark,
|
||||
"Light" => Theme::Light,
|
||||
_ => Theme::Auto,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "gui")]
|
||||
impl theme::Base for Theme {
|
||||
fn default(preference: Mode) -> Self {
|
||||
match preference {
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::CACHE_DIR;
|
||||
use crate::core::utils::{format_diff_time_from_now, last_modified_date};
|
||||
use crate::utils::{format_diff_time_from_now, last_modified_date};
|
||||
use log::warn;
|
||||
use retry::{OperationResult, delay::Fixed, retry};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
|
|
@ -15,7 +16,7 @@ pub const LIST_FNAME: &str = "uad_lists.json";
|
|||
reason = "https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/discussions/608"
|
||||
)]
|
||||
// not `const`, because it's too big
|
||||
pub static DATA: &str = include_str!("../../resources/assets/uad_lists.json");
|
||||
pub static DATA: &str = include_str!("../../../resources/assets/uad_lists.json");
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Hash, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
@ -28,7 +29,7 @@ pub struct Package {
|
|||
pub removal: Removal,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Default, Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum UadList {
|
||||
#[default]
|
||||
All,
|
||||
|
|
@ -73,6 +74,7 @@ impl UadList {
|
|||
Self::Unlisted,
|
||||
];
|
||||
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::All => "All lists",
|
||||
|
|
@ -148,7 +150,7 @@ impl Opposite for PackageState {
|
|||
}
|
||||
|
||||
// Bad names. To be changed!
|
||||
#[derive(Default, Debug, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Default, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Removal {
|
||||
#[default]
|
||||
Recommended,
|
||||
|
|
@ -176,6 +178,7 @@ impl Removal {
|
|||
Self::Unlisted,
|
||||
];
|
||||
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::All => "All removals",
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
use crate::core::utils::NAME;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
use {
|
||||
crate::utils::NAME,
|
||||
log::{debug, error},
|
||||
retry::{OperationResult, delay::Fibonacci, retry},
|
||||
std::fs,
|
||||
std::io,
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
use crate::core::{
|
||||
use crate::{
|
||||
adb::{ACommand as AdbCommand, PmListPacksFlag},
|
||||
sync::User,
|
||||
sync::{CorePackage, User},
|
||||
theme::Theme,
|
||||
uad_lists::{PackageHashMap, PackageState, Removal, UadList},
|
||||
};
|
||||
use crate::gui::widgets::package_row::PackageRow;
|
||||
use chrono::{DateTime, offset::Utc};
|
||||
use csv::Writer;
|
||||
use log::error;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt, fs,
|
||||
|
|
@ -66,11 +66,12 @@ pub enum Error {
|
|||
DialogClosed,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn fetch_packages(
|
||||
uad_lists: &PackageHashMap,
|
||||
device_serial: &str,
|
||||
user_id: Option<u16>,
|
||||
) -> Vec<PackageRow> {
|
||||
) -> Vec<CorePackage> {
|
||||
let all_sys_packs = AdbCommand::new()
|
||||
.shell(device_serial)
|
||||
.pm()
|
||||
|
|
@ -92,24 +93,24 @@ pub fn fetch_packages(
|
|||
.collect();
|
||||
|
||||
let mut description;
|
||||
let mut uad_list;
|
||||
let mut state;
|
||||
let mut removal;
|
||||
let mut user_package: Vec<PackageRow> = Vec::new();
|
||||
let mut state;
|
||||
let mut list;
|
||||
let mut user_package: Vec<CorePackage> = Vec::new();
|
||||
|
||||
for pack_name in all_sys_packs {
|
||||
let p_name = &pack_name;
|
||||
state = PackageState::Uninstalled;
|
||||
description = "[No description]: CONTRIBUTION WELCOMED";
|
||||
uad_list = UadList::Unlisted;
|
||||
description = String::from("[No description]: CONTRIBUTION WELCOMED");
|
||||
removal = Removal::Unlisted;
|
||||
state = PackageState::Uninstalled;
|
||||
list = UadList::Unlisted;
|
||||
|
||||
if let Some(package) = uad_lists.get(p_name) {
|
||||
if !package.description.is_empty() {
|
||||
description = &package.description;
|
||||
description = package.description.clone();
|
||||
}
|
||||
uad_list = package.list;
|
||||
removal = package.removal;
|
||||
list = package.list;
|
||||
}
|
||||
|
||||
if enabled_sys_packs.contains(p_name) {
|
||||
|
|
@ -118,14 +119,20 @@ pub fn fetch_packages(
|
|||
state = PackageState::Disabled;
|
||||
}
|
||||
|
||||
let package_row =
|
||||
PackageRow::new(p_name, state, description, uad_list, removal, false, false);
|
||||
user_package.push(package_row);
|
||||
let package = CorePackage {
|
||||
name: p_name.clone(),
|
||||
description,
|
||||
removal,
|
||||
state,
|
||||
list,
|
||||
};
|
||||
user_package.push(package);
|
||||
}
|
||||
user_package.sort_by_key(|a| a.name.to_lowercase());
|
||||
user_package
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn string_to_theme(theme: &str) -> Theme {
|
||||
match theme {
|
||||
"Dark" => Theme::Dark,
|
||||
|
|
@ -137,6 +144,7 @@ pub fn string_to_theme(theme: &str) -> Theme {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn setup_uad_dir(dir: &Path) -> PathBuf {
|
||||
let dir = dir.join("uad");
|
||||
if let Err(e) = fs::create_dir_all(&dir) {
|
||||
|
|
@ -173,6 +181,7 @@ pub fn open_url(dir: PathBuf) {
|
|||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[must_use]
|
||||
pub fn last_modified_date(file: PathBuf) -> DateTime<Utc> {
|
||||
fs::metadata(file).map_or_else(|_| Utc::now(), |metadata| match metadata.modified() {
|
||||
Ok(time) => time.into(),
|
||||
|
|
@ -180,6 +189,7 @@ pub fn last_modified_date(file: PathBuf) -> DateTime<Utc> {
|
|||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn format_diff_time_from_now(date: DateTime<Utc>) -> String {
|
||||
let now: DateTime<Utc> = Utc::now();
|
||||
let last_update = now - date;
|
||||
|
|
@ -194,15 +204,10 @@ pub fn format_diff_time_from_now(date: DateTime<Utc>) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
/// Export selected packages.
|
||||
/// Export selected package names.
|
||||
/// File will be saved in same directory where UAD-ng is located.
|
||||
pub async fn export_selection(packages: Vec<PackageRow>) -> Result<bool, String> {
|
||||
let selected = packages
|
||||
.iter()
|
||||
.filter(|p| p.selected)
|
||||
.map(|p| p.name.clone())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
pub fn export_selection(package_names: &[String]) -> Result<bool, String> {
|
||||
let selected = package_names.join("\n");
|
||||
|
||||
match fs::write(EXPORT_FILE_NAME, selected) {
|
||||
Ok(()) => Ok(true),
|
||||
|
|
@ -236,22 +241,9 @@ impl fmt::Display for DisplayablePath {
|
|||
}
|
||||
}
|
||||
|
||||
/// Can be used to choose any folder.
|
||||
pub async fn open_folder() -> Result<PathBuf, Error> {
|
||||
let picked_folder = rfd::AsyncFileDialog::new()
|
||||
.pick_folder()
|
||||
.await
|
||||
.ok_or(Error::DialogClosed)?;
|
||||
|
||||
Ok(picked_folder.path().to_owned())
|
||||
}
|
||||
|
||||
/// Export uninstalled packages in a csv file.
|
||||
/// Exported information will contain package name and description.
|
||||
pub async fn export_packages(
|
||||
user: User,
|
||||
phone_packages: Vec<Vec<PackageRow>>,
|
||||
) -> Result<bool, String> {
|
||||
pub fn export_packages(user: User, phone_packages: &[Vec<CorePackage>]) -> Result<bool, String> {
|
||||
let backup_file = generate_backup_name(chrono::Local::now());
|
||||
|
||||
let file = fs::File::create(backup_file).map_err(|err| err.to_string())?;
|
||||
|
|
@ -260,7 +252,7 @@ pub async fn export_packages(
|
|||
wtr.write_record(["Package Name", "Description"])
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
let uninstalled_packages: Vec<&PackageRow> = phone_packages[user.index]
|
||||
let uninstalled_packages: Vec<&CorePackage> = phone_packages[user.index]
|
||||
.iter()
|
||||
.filter(|p| p.state == PackageState::Uninstalled)
|
||||
.collect();
|
||||
|
|
@ -275,6 +267,36 @@ pub async fn export_packages(
|
|||
Ok(true)
|
||||
}
|
||||
|
||||
/// Truncate description to fit within max length, taking only the first line
|
||||
#[must_use]
|
||||
pub fn truncate_description(desc: &str, max_len: usize) -> String {
|
||||
let first_line = desc.split('\n').next().unwrap_or("").trim_end();
|
||||
|
||||
if first_line.len() <= max_len {
|
||||
first_line.to_string()
|
||||
} else {
|
||||
format!("{}...", &first_line[..max_len.saturating_sub(3)])
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if package matches search term (checks name and description)
|
||||
#[must_use]
|
||||
pub fn matches_search(pkg_name: &str, search_term: &str, pkg_description: Option<&str>) -> bool {
|
||||
let search_lower = search_term.to_lowercase();
|
||||
|
||||
if pkg_name.to_lowercase().contains(&search_lower) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(description) = pkg_description {
|
||||
if description.to_lowercase().contains(&search_lower) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
63
crates/uad-gui/Cargo.toml
Normal file
63
crates/uad-gui/Cargo.toml
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
[package]
|
||||
name = "uad-gui"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
readme.workspace = true
|
||||
keywords.workspace = true
|
||||
categories = ["gui"]
|
||||
edition.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "uad-ng"
|
||||
path = "src/main.rs"
|
||||
|
||||
[features]
|
||||
default = ["wgpu", "self-update", "img"]
|
||||
wgpu = [] # Iced/wgpu is default
|
||||
self-update = ["flate2", "tar"]
|
||||
no-self-update = []
|
||||
img = ["image", "iced/image"]
|
||||
|
||||
[dependencies]
|
||||
uad-core = { path = "../uad-core", features = ["gui", "self-update"] }
|
||||
iced.workspace = true
|
||||
image = { workspace = true, optional = true }
|
||||
rfd.workspace = true
|
||||
dark-light.workspace = true
|
||||
fern.workspace = true
|
||||
chrono.workspace = true
|
||||
dirs.workspace = true
|
||||
log.workspace = true
|
||||
flate2 = { workspace = true, optional = true }
|
||||
tar = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
win32console.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "3"
|
||||
|
||||
[lints.rust]
|
||||
deprecated_safe = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
undocumented_unsafe_blocks = "forbid"
|
||||
exit = "deny"
|
||||
panic_in_result_fn = "warn"
|
||||
infinite_loop = "warn"
|
||||
mem_forget = "warn"
|
||||
implicit_clone = "warn"
|
||||
format_push_string = "warn"
|
||||
large_include_file = "warn"
|
||||
shadow_unrelated = "warn"
|
||||
struct_field_names = "allow" # annoying
|
||||
module_name_repetitions = "allow" # annoying
|
||||
|
||||
disallowed_types = "deny"
|
||||
disallowed_methods = "deny"
|
||||
|
||||
allow_attributes_without_reason = "warn"
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
|
|
@ -1,35 +1,32 @@
|
|||
pub mod style;
|
||||
pub mod views;
|
||||
pub mod widgets;
|
||||
|
||||
use crate::core::adb;
|
||||
use crate::core::sync::{Phone, get_devices_list, initial_load};
|
||||
use crate::theme::string_to_theme;
|
||||
#[cfg(feature = "img")]
|
||||
use crate::core::theme::OS_COLOR_SCHEME;
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::uad_lists::UadListState;
|
||||
use crate::core::update::{Release, SelfUpdateState, SelfUpdateStatus, get_latest_release};
|
||||
use crate::core::utils::{FULL_NAME, NAME, string_to_theme};
|
||||
|
||||
use crate::theme::OS_COLOR_SCHEME;
|
||||
use crate::theme::Theme;
|
||||
use crate::views::about::{About as AboutView, Message as AboutMessage};
|
||||
use crate::views::list::{
|
||||
List as AppsView, LoadingState as ListLoadingState, Message as AppsMessage,
|
||||
};
|
||||
use crate::views::settings::{Message as SettingsMessage, Settings as SettingsView};
|
||||
use crate::widgets::navigation_menu::nav_menu;
|
||||
use iced::font;
|
||||
#[cfg(feature = "img")]
|
||||
use iced::window::icon;
|
||||
#[cfg(feature = "img")]
|
||||
use image::ImageFormat;
|
||||
use views::about::{About as AboutView, Message as AboutMessage};
|
||||
use views::list::{List as AppsView, LoadingState as ListLoadingState, Message as AppsMessage};
|
||||
use views::settings::{Message as SettingsMessage, Settings as SettingsView};
|
||||
use widgets::navigation_menu::nav_menu;
|
||||
|
||||
use iced::widget::column;
|
||||
use iced::{Alignment, Element, Length, Settings, Task, window::Settings as Window};
|
||||
use iced::{Subscription, event, keyboard};
|
||||
|
||||
use log::{debug, error, info};
|
||||
#[cfg(feature = "self-update")]
|
||||
use std::path::PathBuf;
|
||||
use uad_core::adb;
|
||||
use uad_core::sync::{Phone, get_devices_list, initial_load};
|
||||
use uad_core::uad_lists::UadListState;
|
||||
use uad_core::update::{Release, SelfUpdateState, SelfUpdateStatus, get_latest_release};
|
||||
use uad_core::utils::{FULL_NAME, NAME};
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
use crate::core::update::{BIN_NAME, download_update_to_temp_file, remove_file};
|
||||
use uad_core::update::{BIN_NAME, download_update_to_temp_file, remove_file};
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
enum View {
|
||||
|
|
@ -41,8 +38,8 @@ enum View {
|
|||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct UpdateState {
|
||||
self_update: SelfUpdateState,
|
||||
uad_list: UadListState,
|
||||
pub self_update: SelfUpdateState,
|
||||
pub uad_list: UadListState,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -86,18 +83,21 @@ impl UadGui {
|
|||
reason = "required by iced's Application trait interface"
|
||||
)]
|
||||
fn title(&self) -> String {
|
||||
FULL_NAME.to_string()
|
||||
self.selected_device.as_ref().map_or_else(
|
||||
|| FULL_NAME.to_string(),
|
||||
|device| format!("{FULL_NAME} - {}", device.model),
|
||||
)
|
||||
}
|
||||
|
||||
fn new() -> (Self, Task<Message>) {
|
||||
(
|
||||
Self::default(),
|
||||
Task::batch([
|
||||
// Used in crate::gui::widgets::navigation_menu::ICONS. Name is `icomoon`.
|
||||
font::load(include_bytes!("../../resources/assets/icons.ttf").as_slice())
|
||||
// Used in crate::widgets::navigation_menu::ICONS. Name is `icomoon`.
|
||||
font::load(include_bytes!("../../../resources/assets/icons.ttf").as_slice())
|
||||
.map(Message::FontLoaded),
|
||||
Task::perform(initial_load(), Message::ADBSatisfied),
|
||||
Task::perform(get_devices_list(), Message::LoadDevices),
|
||||
Task::perform(async { initial_load() }, Message::ADBSatisfied),
|
||||
Task::perform(async { get_devices_list() }, Message::LoadDevices),
|
||||
Task::perform(
|
||||
async move { get_latest_release() },
|
||||
Message::GetLatestRelease,
|
||||
|
|
@ -111,21 +111,45 @@ impl UadGui {
|
|||
reason = "required by iced's Application trait interface"
|
||||
)]
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
event::listen_with(|event, _status, _env| match event {
|
||||
if self.selected_device.is_some() {
|
||||
event::listen_with(Self::keyboard_shortcuts_with_device)
|
||||
} else {
|
||||
event::listen_with(Self::keyboard_shortcuts_without_device)
|
||||
}
|
||||
}
|
||||
|
||||
fn keyboard_shortcuts_with_device(
|
||||
event: iced::Event,
|
||||
_status: iced::event::Status,
|
||||
_window: iced::window::Id,
|
||||
) -> Option<Message> {
|
||||
Self::keyboard_shortcuts(event, true)
|
||||
}
|
||||
|
||||
fn keyboard_shortcuts_without_device(
|
||||
event: iced::Event,
|
||||
_status: iced::event::Status,
|
||||
_window: iced::window::Id,
|
||||
) -> Option<Message> {
|
||||
Self::keyboard_shortcuts(event, false)
|
||||
}
|
||||
|
||||
fn keyboard_shortcuts(event: iced::Event, can_control_device: bool) -> Option<Message> {
|
||||
match event {
|
||||
iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Character(c),
|
||||
modifiers,
|
||||
..
|
||||
}) if modifiers.control() && modifiers.shift() => match c.as_str() {
|
||||
"r" => Some(Message::RebootButtonPressed),
|
||||
"5" => Some(Message::RefreshButtonPressed),
|
||||
"r" if can_control_device => Some(Message::RebootButtonPressed),
|
||||
"5" if can_control_device => Some(Message::RefreshButtonPressed),
|
||||
"a" => Some(Message::AppsPress),
|
||||
"i" => Some(Message::AboutPressed),
|
||||
"s" => Some(Message::SettingsPressed),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(
|
||||
|
|
@ -178,7 +202,7 @@ impl UadGui {
|
|||
self.adb_satisfied,
|
||||
)));
|
||||
}
|
||||
Task::perform(get_devices_list(), Message::LoadDevices)
|
||||
Task::perform(async { get_devices_list() }, Message::LoadDevices)
|
||||
}
|
||||
Message::RebootButtonPressed => {
|
||||
self.apps_view = AppsView::default();
|
||||
|
|
@ -412,10 +436,10 @@ impl UadGui {
|
|||
let logo: &[u8] = match *OS_COLOR_SCHEME {
|
||||
// remember to keep `Unspecified` in sync with `src/core/theme`
|
||||
dark_light::Mode::Dark | dark_light::Mode::Unspecified => {
|
||||
include_bytes!("../../resources/assets/logo-dark.png")
|
||||
include_bytes!("../../../resources/assets/logo-dark.png")
|
||||
}
|
||||
dark_light::Mode::Light => {
|
||||
include_bytes!("../../resources/assets/logo-light.png")
|
||||
include_bytes!("../../../resources/assets/logo-light.png")
|
||||
}
|
||||
};
|
||||
|
||||
23
crates/uad-gui/src/helpers.rs
Normal file
23
crates/uad-gui/src/helpers.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
use crate::style;
|
||||
use crate::theme::Theme;
|
||||
use iced::Element;
|
||||
use iced::widget::button::Button;
|
||||
|
||||
/// Wrapper function for `iced::widget::button` with padding and style applied
|
||||
pub fn button_primary<'a, Message>(
|
||||
content: impl Into<Element<'a, Message, Theme>>,
|
||||
) -> Button<'a, Message, Theme> {
|
||||
iced::widget::button(content)
|
||||
.padding([5, 10])
|
||||
.style(style::Button::Primary)
|
||||
}
|
||||
|
||||
/// Opens a file picker dialog to select a folder
|
||||
pub async fn open_folder() -> Result<std::path::PathBuf, uad_core::utils::Error> {
|
||||
rfd::AsyncFileDialog::new()
|
||||
.set_title("Choose a backup location")
|
||||
.pick_folder()
|
||||
.await
|
||||
.ok_or(uad_core::utils::Error::DialogClosed)
|
||||
.map(|f| f.path().to_path_buf())
|
||||
}
|
||||
11
crates/uad-gui/src/lib.rs
Normal file
11
crates/uad-gui/src/lib.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#![allow(
|
||||
clippy::missing_errors_doc,
|
||||
reason = "Short-circuit doc lint to keep CI green"
|
||||
)]
|
||||
|
||||
pub mod gui;
|
||||
pub mod helpers;
|
||||
pub mod style;
|
||||
pub mod theme;
|
||||
pub mod views;
|
||||
pub mod widgets;
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
#![windows_subsystem = "windows"]
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use crate::core::utils::setup_uad_dir;
|
||||
use fern::{
|
||||
FormatCallback,
|
||||
colors::{Color, ColoredLevelConfig},
|
||||
|
|
@ -10,12 +7,10 @@ use fern::{
|
|||
use log::Record;
|
||||
use std::sync::LazyLock;
|
||||
use std::{fmt::Arguments, fs::OpenOptions, path::PathBuf};
|
||||
use uad_core::utils::setup_uad_dir;
|
||||
|
||||
mod core;
|
||||
mod gui;
|
||||
use uad_gui::gui::UadGui;
|
||||
|
||||
static CONFIG_DIR: LazyLock<PathBuf> =
|
||||
LazyLock::new(|| setup_uad_dir(&dirs::config_dir().expect("Can't detect config dir")));
|
||||
static CACHE_DIR: LazyLock<PathBuf> =
|
||||
LazyLock::new(|| setup_uad_dir(&dirs::cache_dir().expect("Can't detect cache dir")));
|
||||
|
||||
|
|
@ -30,7 +25,7 @@ fn main() -> iced::Result {
|
|||
}
|
||||
|
||||
setup_logger().expect("setup logging");
|
||||
gui::UadGui::start()
|
||||
UadGui::start()
|
||||
}
|
||||
|
||||
/// Sets up logging to a new file in `CACHE_DIR"/uadng.log"`
|
||||
|
|
@ -74,14 +69,14 @@ fn setup_logger() -> Result<(), fern::InitError> {
|
|||
.format(make_formatter(false))
|
||||
.level(default_log_level)
|
||||
// Rust compiler makes module names use _ instead of -
|
||||
.level_for("uad_ng", log::LevelFilter::Debug)
|
||||
.level_for("uad_gui", log::LevelFilter::Debug)
|
||||
.chain(log_file);
|
||||
|
||||
let stdout_dispatcher = fern::Dispatch::new()
|
||||
.format(make_formatter(true))
|
||||
.level(default_log_level)
|
||||
// Rust compiler makes module names use _ instead of -
|
||||
.level_for("uad_ng", log::LevelFilter::Warn)
|
||||
.level_for("uad_gui", log::LevelFilter::Warn)
|
||||
.chain(std::io::stdout());
|
||||
|
||||
fern::Dispatch::new()
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
clippy::wildcard_imports,
|
||||
reason = "Iced style modules use PascalCase and &Theme; wildcard for local convenience"
|
||||
)]
|
||||
use crate::core::theme::{ColorPalette, Theme};
|
||||
use crate::theme::{ColorPalette, Theme};
|
||||
use iced::widget::{
|
||||
button, checkbox, container, overlay, pick_list, radio, scrollable, text, text_editor,
|
||||
text_input,
|
||||
|
|
@ -321,10 +321,12 @@ pub mod Container {
|
|||
use super::*;
|
||||
|
||||
#[allow(dead_code, reason = "Used by other themes or future styles")]
|
||||
#[must_use]
|
||||
pub fn Invisible(_: &Theme) -> container::Style {
|
||||
container::Style::default()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Frame(theme: &Theme) -> container::Style {
|
||||
let p = theme.palette();
|
||||
container::Style {
|
||||
|
|
@ -340,6 +342,7 @@ pub mod Container {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn BorderedFrame(theme: &Theme) -> container::Style {
|
||||
let p = theme.palette();
|
||||
container::Style {
|
||||
|
|
@ -356,6 +359,7 @@ pub mod Container {
|
|||
}
|
||||
|
||||
#[allow(dead_code, reason = "Currently unused in some views")]
|
||||
#[must_use]
|
||||
pub fn Tooltip(theme: &Theme) -> container::Style {
|
||||
let p = theme.palette();
|
||||
container::Style {
|
||||
|
|
@ -371,6 +375,7 @@ pub mod Container {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Background(theme: &Theme) -> container::Style {
|
||||
let p = theme.palette();
|
||||
container::Style {
|
||||
|
|
@ -408,6 +413,7 @@ pub mod Button {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Primary(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let p = theme.palette();
|
||||
let mut style = style_active_hover_disabled(p.bright.primary, p.bright.primary, status);
|
||||
|
|
@ -421,10 +427,12 @@ pub mod Button {
|
|||
dead_code,
|
||||
reason = "Alias kept for semantic clarity in some call-sites"
|
||||
)]
|
||||
#[must_use]
|
||||
pub fn SelfUpdate(theme: &Theme, status: button::Status) -> button::Style {
|
||||
Primary(theme, status)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn RestorePackage(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let p = theme.palette();
|
||||
let mut style = style_active_hover_disabled(p.bright.secondary, p.bright.secondary, status);
|
||||
|
|
@ -441,6 +449,7 @@ pub mod Button {
|
|||
style
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn UninstallPackage(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let p = theme.palette();
|
||||
let mut style = style_active_hover_disabled(p.bright.error, p.bright.error, status);
|
||||
|
|
@ -454,10 +463,12 @@ pub mod Button {
|
|||
dead_code,
|
||||
reason = "Style exposed for disabled state buttons in some contexts"
|
||||
)]
|
||||
#[must_use]
|
||||
pub fn Unavailable(theme: &Theme, status: button::Status) -> button::Style {
|
||||
UninstallPackage(theme, status)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn NormalPackage(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let p = theme.palette();
|
||||
match status {
|
||||
|
|
@ -489,6 +500,7 @@ pub mod Button {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn SelectedPackage(theme: &Theme, _status: button::Status) -> button::Style {
|
||||
let p = theme.palette();
|
||||
button::Style {
|
||||
|
|
@ -508,6 +520,7 @@ pub mod Button {
|
|||
}
|
||||
|
||||
#[allow(dead_code, reason = "Used in views where buttons must be invisible")]
|
||||
#[must_use]
|
||||
pub fn Hidden(_: &Theme, _: button::Status) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
|
|
@ -605,6 +618,7 @@ pub mod Scrollable {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Description(theme: &Theme, _status: scrollable::Status) -> scrollable::Style {
|
||||
let p = theme.palette();
|
||||
let (v, h) = rails(p.normal.surface);
|
||||
|
|
@ -617,6 +631,7 @@ pub mod Scrollable {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Packages(theme: &Theme, _status: scrollable::Status) -> scrollable::Style {
|
||||
let p = theme.palette();
|
||||
let (v, h) = rails(p.base.foreground);
|
||||
|
|
@ -633,6 +648,7 @@ pub mod Scrollable {
|
|||
pub mod CheckBox {
|
||||
use super::*;
|
||||
|
||||
#[must_use]
|
||||
pub fn PackageEnabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
||||
let p = theme.palette();
|
||||
checkbox::Style {
|
||||
|
|
@ -647,6 +663,7 @@ pub mod CheckBox {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn PackageDisabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
||||
let p = theme.palette();
|
||||
checkbox::Style {
|
||||
|
|
@ -664,6 +681,7 @@ pub mod CheckBox {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn SettingsEnabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
||||
let p = theme.palette();
|
||||
checkbox::Style {
|
||||
|
|
@ -678,6 +696,7 @@ pub mod CheckBox {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn SettingsDisabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
||||
let p = theme.palette();
|
||||
checkbox::Style {
|
||||
|
|
@ -696,11 +715,13 @@ pub mod CheckBox {
|
|||
pub mod Text {
|
||||
use super::*;
|
||||
|
||||
#[must_use]
|
||||
pub fn Default(theme: &Theme) -> text::Style {
|
||||
let _ = theme;
|
||||
text::Style::default()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Ok(theme: &Theme) -> text::Style {
|
||||
let p = theme.palette();
|
||||
text::Style {
|
||||
|
|
@ -708,6 +729,7 @@ pub mod Text {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Danger(theme: &Theme) -> text::Style {
|
||||
let p = theme.palette();
|
||||
text::Style {
|
||||
|
|
@ -715,6 +737,7 @@ pub mod Text {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Commentary(theme: &Theme) -> text::Style {
|
||||
let p = theme.palette();
|
||||
text::Style {
|
||||
|
|
@ -738,7 +761,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_palette() {
|
||||
let palette = Theme::default().palette();
|
||||
let palette = Theme::Dark.palette();
|
||||
|
||||
assert_ne!(palette.base.background, palette.base.foreground);
|
||||
assert_ne!(palette.normal.primary, Color::BLACK);
|
||||
72
crates/uad-gui/src/theme.rs
Normal file
72
crates/uad-gui/src/theme.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
use iced::theme::{self, Mode, Palette, Style};
|
||||
|
||||
pub use uad_core::theme::{BaseColors, BrightColors, ColorPalette, NormalColors, OS_COLOR_SCHEME};
|
||||
|
||||
/// GUI-local wrapper around the core Theme to satisfy orphan rules for
|
||||
/// iced's Catalog traits.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Theme(pub uad_core::theme::Theme);
|
||||
|
||||
#[allow(
|
||||
non_upper_case_globals,
|
||||
reason = "Keep variant-like names matching core Theme"
|
||||
)]
|
||||
impl Theme {
|
||||
pub const Auto: Self = Self(uad_core::theme::Theme::Auto);
|
||||
pub const Lupin: Self = Self(uad_core::theme::Theme::Lupin);
|
||||
pub const Dark: Self = Self(uad_core::theme::Theme::Dark);
|
||||
pub const Light: Self = Self(uad_core::theme::Theme::Light);
|
||||
|
||||
pub const ALL: [Self; 4] = [Self::Auto, Self::Lupin, Self::Dark, Self::Light];
|
||||
|
||||
#[must_use]
|
||||
pub fn palette(self) -> ColorPalette {
|
||||
self.0.palette()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<uad_core::theme::Theme> for Theme {
|
||||
fn from(value: uad_core::theme::Theme) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Theme> for uad_core::theme::Theme {
|
||||
fn from(value: Theme) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a string to the GUI's Theme type
|
||||
#[must_use]
|
||||
pub fn string_to_theme(theme: &str) -> Theme {
|
||||
Theme(uad_core::theme::string_to_theme(theme))
|
||||
}
|
||||
|
||||
impl theme::Base for Theme {
|
||||
fn default(preference: Mode) -> Self {
|
||||
Self(<uad_core::theme::Theme as theme::Base>::default(preference))
|
||||
}
|
||||
|
||||
fn mode(&self) -> Mode {
|
||||
<uad_core::theme::Theme as theme::Base>::mode(&self.0)
|
||||
}
|
||||
|
||||
fn base(&self) -> Style {
|
||||
<uad_core::theme::Theme as theme::Base>::base(&self.0)
|
||||
}
|
||||
|
||||
fn palette(&self) -> Option<Palette> {
|
||||
<uad_core::theme::Theme as theme::Base>::palette(&self.0)
|
||||
}
|
||||
|
||||
fn name(&self) -> &str {
|
||||
<uad_core::theme::Theme as theme::Base>::name(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Theme {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
use crate::CACHE_DIR;
|
||||
use crate::core::adb;
|
||||
use crate::core::helpers::button_primary;
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::uad_lists::LIST_FNAME;
|
||||
use crate::core::utils::{FULL_NAME, NAME, last_modified_date, open_url};
|
||||
use crate::gui::{UpdateState, style, widgets::text};
|
||||
use crate::helpers::button_primary;
|
||||
use crate::theme::Theme;
|
||||
use crate::{gui::UpdateState, style, widgets::text};
|
||||
use iced::widget::{Space, column, container, row};
|
||||
use iced::{Alignment, Element, Length, Renderer};
|
||||
use log::error;
|
||||
use std::path::PathBuf;
|
||||
use uad_core::CACHE_DIR;
|
||||
use uad_core::adb;
|
||||
use uad_core::uad_lists::LIST_FNAME;
|
||||
use uad_core::utils::{FULL_NAME, NAME, last_modified_date, open_url};
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
use crate::core::update::SelfUpdateStatus;
|
||||
use uad_core::update::SelfUpdateStatus;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct About {}
|
||||
|
|
@ -1,19 +1,22 @@
|
|||
use crate::core::config::DeviceSettings;
|
||||
use crate::core::helpers::button_primary;
|
||||
use crate::core::sync::{AdbError, Phone, User, apply_pkg_state_commands, run_adb_action};
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::uad_lists::{
|
||||
use crate::helpers::button_primary;
|
||||
use crate::style;
|
||||
use crate::theme::Theme;
|
||||
use crate::widgets::navigation_menu::ICONS;
|
||||
use log::{error, info, warn};
|
||||
use std::path::PathBuf;
|
||||
use uad_core::config::DeviceSettings;
|
||||
use uad_core::sync::{AdbError, Phone, User, apply_pkg_state_commands};
|
||||
use uad_core::uad_lists::{
|
||||
Opposite, PackageHashMap, PackageState, Removal, UadList, UadListState, load_debloat_lists,
|
||||
};
|
||||
use crate::core::utils::{EXPORT_FILE_NAME, NAME, export_selection, fetch_packages, open_url};
|
||||
use crate::gui::style;
|
||||
use crate::gui::widgets::navigation_menu::ICONS;
|
||||
use std::path::PathBuf;
|
||||
use uad_core::utils::{
|
||||
EXPORT_FILE_NAME, NAME, export_selection, fetch_packages, matches_search, open_url,
|
||||
};
|
||||
|
||||
use crate::gui::views::settings::Settings;
|
||||
use crate::gui::widgets::modal::Modal;
|
||||
use crate::gui::widgets::package_row::{Message as RowMessage, PackageRow};
|
||||
use crate::gui::widgets::text;
|
||||
use crate::views::settings::Settings;
|
||||
use crate::widgets::modal::Modal;
|
||||
use crate::widgets::package_row::{Message as RowMessage, PackageRow};
|
||||
use crate::widgets::text;
|
||||
use iced::widget::scrollable::{Direction, Scrollbar};
|
||||
use iced::widget::{
|
||||
Column, Space, button, checkbox, column, container, pick_list, radio, row, rule, scrollable,
|
||||
|
|
@ -52,8 +55,8 @@ pub struct List {
|
|||
filtered_packages: Vec<usize>,
|
||||
/// Vec of `(user_index, pkg_index)`
|
||||
selected_packages: Vec<(usize, usize)>,
|
||||
selected_package_state: Option<PackageState>,
|
||||
selected_removal: Option<Removal>,
|
||||
selected_package_state: Option<PackageState>,
|
||||
selected_list: Option<UadList>,
|
||||
pub selected_user: Option<User>,
|
||||
all_selected: bool,
|
||||
|
|
@ -81,8 +84,8 @@ pub enum Message {
|
|||
ToggleAllSelected(bool),
|
||||
ListSelected(UadList),
|
||||
UserSelected(User),
|
||||
PackageStateSelected(PackageState),
|
||||
RemovalSelected(Removal),
|
||||
PackageStateSelected(PackageState),
|
||||
ApplyActionOnSelection,
|
||||
List(usize, RowMessage),
|
||||
VerifyAndFallback(Result<PackageInfo, AdbError>),
|
||||
|
|
@ -149,8 +152,8 @@ impl List {
|
|||
}
|
||||
Message::SearchInputChanged(letter) => self.on_search_input_changed(letter),
|
||||
Message::ListSelected(list) => self.on_list_selected(list),
|
||||
Message::PackageStateSelected(state) => self.on_package_state_selected(state),
|
||||
Message::RemovalSelected(removal) => self.on_removal_selected(removal),
|
||||
Message::PackageStateSelected(state) => self.on_package_state_selected(state),
|
||||
Message::List(i, row_msg) => self.on_list_row(i, &row_msg, settings, selected_device),
|
||||
Message::ApplyActionOnSelection => self.on_apply_action_on_selection(),
|
||||
Message::UserSelected(user) => self.on_user_selected(user),
|
||||
|
|
@ -635,7 +638,7 @@ impl List {
|
|||
.width(120),
|
||||
row![text(
|
||||
self.phone_packages[selection.0][selection.1]
|
||||
.uad_list
|
||||
.list
|
||||
.to_string()
|
||||
)]
|
||||
.width(55),
|
||||
|
|
@ -752,12 +755,11 @@ impl List {
|
|||
// that's why `enumerate` is before `filter`.
|
||||
.enumerate()
|
||||
.filter(|(_, p)| {
|
||||
(list_filter == UadList::All || p.uad_list == list_filter)
|
||||
(list_filter == UadList::All || p.list == list_filter)
|
||||
&& (package_filter == PackageState::All || p.state == package_filter)
|
||||
&& (removal_filter == Removal::All || p.removal == removal_filter)
|
||||
&& (self.input_value.is_empty()
|
||||
|| p.name.contains(&self.input_value)
|
||||
|| p.description.contains(&self.input_value))
|
||||
|| matches_search(&p.name, &self.input_value, Some(&p.description)))
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
|
@ -771,11 +773,21 @@ impl List {
|
|||
) -> Vec<Vec<PackageRow>> {
|
||||
let serial = device_serial.as_ref();
|
||||
if user_list.len() <= 1 {
|
||||
vec![fetch_packages(&uad_list, serial, None)]
|
||||
vec![
|
||||
fetch_packages(&uad_list, serial, None)
|
||||
.into_iter()
|
||||
.map(PackageRow::from)
|
||||
.collect(),
|
||||
]
|
||||
} else {
|
||||
user_list
|
||||
.iter()
|
||||
.map(|user| fetch_packages(&uad_list, serial, Some(user.id)))
|
||||
.map(|user| {
|
||||
fetch_packages(&uad_list, serial, Some(user.id))
|
||||
.into_iter()
|
||||
.map(PackageRow::from)
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
|
@ -877,8 +889,8 @@ impl List {
|
|||
let i_user = self.selected_user.unwrap_or_default().index;
|
||||
self.phone_packages = packages;
|
||||
self.filtered_packages = (0..self.phone_packages[i_user].len()).collect();
|
||||
self.selected_package_state = Some(PackageState::Enabled);
|
||||
self.selected_removal = Some(Removal::Recommended);
|
||||
self.selected_package_state = Some(PackageState::Enabled);
|
||||
self.selected_list = Some(UadList::All);
|
||||
self.selected_user = Some(User::default());
|
||||
self.fallback_notifications.clear();
|
||||
|
|
@ -1053,7 +1065,7 @@ impl List {
|
|||
Task::perform(
|
||||
async move {
|
||||
// Blocking ADB calls happen here (off UI thread)
|
||||
let actual_state_opt = crate::core::sync::verify_package_state(
|
||||
let actual_state_opt = uad_core::sync::verify_package_state(
|
||||
&pkg_name,
|
||||
device.adb_id.as_str(),
|
||||
Some(user_id),
|
||||
|
|
@ -1062,7 +1074,7 @@ impl List {
|
|||
match actual_state_opt {
|
||||
Some(actual_state) if actual_state == wanted_state => {
|
||||
// Check cross-user behavior
|
||||
let error_modal = crate::core::sync::detect_cross_user_behavior(
|
||||
let error_modal = uad_core::sync::detect_cross_user_behavior(
|
||||
&pkg_name,
|
||||
device.adb_id.as_str(),
|
||||
user_id,
|
||||
|
|
@ -1091,16 +1103,16 @@ impl List {
|
|||
// Package doesn't exist (None) or has wrong state - try fallback
|
||||
let actual_state =
|
||||
actual_state_opt.unwrap_or(PackageState::Uninstalled);
|
||||
let fallback_result = crate::core::sync::attempt_fallback(
|
||||
&crate::gui::widgets::package_row::PackageRow::new(
|
||||
&pkg_name,
|
||||
current_state,
|
||||
"",
|
||||
UadList::All,
|
||||
Removal::All,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
// Create CorePackage for fallback
|
||||
let core_package = uad_core::sync::CorePackage {
|
||||
name: pkg_name.clone(),
|
||||
description: String::new(),
|
||||
removal: Removal::All,
|
||||
state: current_state,
|
||||
list: UadList::Unlisted,
|
||||
};
|
||||
let fallback_result = uad_core::sync::attempt_fallback(
|
||||
&core_package,
|
||||
wanted_state,
|
||||
actual_state,
|
||||
device.user_list[i_user],
|
||||
|
|
@ -1195,14 +1207,17 @@ impl List {
|
|||
// fallback task
|
||||
return Task::perform(
|
||||
async move {
|
||||
let temp_pkg = crate::core::sync::CorePackage {
|
||||
let temp_pkg = uad_core::sync::CorePackage {
|
||||
name: pkg_name.clone(),
|
||||
description: String::new(),
|
||||
removal: Removal::All,
|
||||
state: PackageState::Disabled,
|
||||
list: UadList::Unlisted,
|
||||
};
|
||||
|
||||
// get uninstall command
|
||||
let uninstall_cmds =
|
||||
crate::core::sync::apply_pkg_state_commands(
|
||||
uad_core::sync::apply_pkg_state_commands(
|
||||
&temp_pkg,
|
||||
PackageState::Uninstalled,
|
||||
user,
|
||||
|
|
@ -1210,24 +1225,24 @@ impl List {
|
|||
);
|
||||
|
||||
if let Some(cmd) = uninstall_cmds.first() {
|
||||
let dummy_info = PackageInfo::default();
|
||||
let _ = crate::core::sync::run_adb_action(
|
||||
device.adb_id.clone(),
|
||||
cmd.clone(),
|
||||
dummy_info,
|
||||
)
|
||||
.await;
|
||||
let _ = uad_core::sync::run_adb_shell_action(
|
||||
&device.adb_id,
|
||||
cmd,
|
||||
);
|
||||
}
|
||||
|
||||
// reinstall/enable
|
||||
let temp_pkg_uninstalled =
|
||||
crate::core::sync::CorePackage {
|
||||
uad_core::sync::CorePackage {
|
||||
name: pkg_name.clone(),
|
||||
description: String::new(),
|
||||
removal: Removal::All,
|
||||
state: PackageState::Uninstalled,
|
||||
list: UadList::Unlisted,
|
||||
};
|
||||
|
||||
let enable_cmds =
|
||||
crate::core::sync::apply_pkg_state_commands(
|
||||
uad_core::sync::apply_pkg_state_commands(
|
||||
&temp_pkg_uninstalled,
|
||||
PackageState::Enabled,
|
||||
user,
|
||||
|
|
@ -1236,18 +1251,15 @@ impl List {
|
|||
|
||||
// lets enable the new package
|
||||
if let Some(cmd) = enable_cmds.first() {
|
||||
let dummy_info = PackageInfo::default();
|
||||
let _ = crate::core::sync::run_adb_action(
|
||||
device.adb_id.clone(),
|
||||
cmd.clone(),
|
||||
dummy_info,
|
||||
)
|
||||
.await;
|
||||
let _ = uad_core::sync::run_adb_shell_action(
|
||||
&device.adb_id,
|
||||
cmd,
|
||||
);
|
||||
}
|
||||
|
||||
//verifies the final state for thread confirmation
|
||||
let actual_state =
|
||||
crate::core::sync::verify_package_state(
|
||||
uad_core::sync::verify_package_state(
|
||||
&pkg_name,
|
||||
&device.adb_id,
|
||||
Some(user.id),
|
||||
|
|
@ -1316,8 +1328,13 @@ impl List {
|
|||
|
||||
fn on_export_selection(&mut self) -> Task<Message> {
|
||||
let i_user = self.selected_user.unwrap_or_default().index;
|
||||
let package_names: Vec<String> = self.phone_packages[i_user]
|
||||
.iter()
|
||||
.filter(|p| p.selected)
|
||||
.map(|p| p.name.clone())
|
||||
.collect();
|
||||
Task::perform(
|
||||
export_selection(self.phone_packages[i_user].clone()),
|
||||
async move { export_selection(&package_names) },
|
||||
Message::SelectionExported,
|
||||
)
|
||||
}
|
||||
|
|
@ -1483,17 +1500,24 @@ fn build_action_pkg_commands(
|
|||
// Will be filled asynchronously before running the adb action
|
||||
before_cross_user_states: vec![],
|
||||
};
|
||||
// Clone data before async block to avoid borrowing issues
|
||||
let device_serial = device.adb_id.clone();
|
||||
let package_name = u_pkg.name.clone();
|
||||
let user_id = u.id;
|
||||
let phone = device.clone();
|
||||
// In the end there is only one package state change
|
||||
// even if we run multiple adb commands
|
||||
commands.push(Task::perform(
|
||||
run_adb_action_with_before_states(
|
||||
device.adb_id.clone(),
|
||||
action,
|
||||
p_info,
|
||||
u_pkg.name.clone(),
|
||||
u.id,
|
||||
device.clone(),
|
||||
),
|
||||
async move {
|
||||
run_adb_action_with_before_states(
|
||||
&device_serial,
|
||||
&action,
|
||||
p_info,
|
||||
&package_name,
|
||||
user_id,
|
||||
&phone,
|
||||
)
|
||||
},
|
||||
if j == 0 {
|
||||
Message::VerifyAndFallback
|
||||
} else {
|
||||
|
|
@ -1505,23 +1529,25 @@ fn build_action_pkg_commands(
|
|||
commands
|
||||
}
|
||||
|
||||
async fn run_adb_action_with_before_states(
|
||||
device_serial: String,
|
||||
action: String,
|
||||
fn run_adb_action_with_before_states(
|
||||
device_serial: &str,
|
||||
action: &str,
|
||||
mut p_info: PackageInfo,
|
||||
package_name: String,
|
||||
package_name: &str,
|
||||
target_user_id: u16,
|
||||
phone: Phone,
|
||||
phone: &Phone,
|
||||
) -> Result<PackageInfo, AdbError> {
|
||||
// Capture before-state in background to avoid blocking UI thread
|
||||
let before_states = crate::core::sync::capture_cross_user_states(
|
||||
&package_name,
|
||||
&device_serial,
|
||||
let before_states = uad_core::sync::capture_cross_user_states(
|
||||
package_name,
|
||||
device_serial,
|
||||
target_user_id,
|
||||
&phone,
|
||||
phone,
|
||||
);
|
||||
p_info.before_cross_user_states = before_states;
|
||||
run_adb_action(device_serial, action, p_info).await
|
||||
// Run the ADB action
|
||||
uad_core::sync::run_adb_shell_action(device_serial, action)?;
|
||||
Ok(p_info)
|
||||
}
|
||||
|
||||
fn recap<'a>(settings: &Settings, recap: &SummaryEntry) -> Element<'a, Message, Theme, Renderer> {
|
||||
|
|
@ -1,15 +1,7 @@
|
|||
use crate::core::{
|
||||
config::{BackupSettings, Config, DeviceSettings, GeneralSettings},
|
||||
helpers::button_primary,
|
||||
save::{backup_phone, list_available_backup_user, list_available_backups, restore_backup},
|
||||
sync::{AdbError, Phone, User, get_android_sdk, run_adb_action, supports_multi_user},
|
||||
theme::Theme,
|
||||
utils::{
|
||||
DisplayablePath, Error, NAME, export_packages, generate_backup_name, open_folder, open_url,
|
||||
string_to_theme,
|
||||
},
|
||||
};
|
||||
use crate::gui::{
|
||||
use crate::helpers::{button_primary, open_folder};
|
||||
use crate::theme::Theme;
|
||||
use crate::theme::string_to_theme;
|
||||
use crate::{
|
||||
style,
|
||||
views::list::{List as AppsView, PackageInfo},
|
||||
widgets::modal::Modal,
|
||||
|
|
@ -17,9 +9,20 @@ use crate::gui::{
|
|||
widgets::package_row::PackageRow,
|
||||
widgets::text,
|
||||
};
|
||||
use chrono;
|
||||
use iced::widget::{Space, button, checkbox, column, container, pick_list, radio, row, scrollable};
|
||||
use iced::{Alignment, Element, Length, Renderer, Task, alignment};
|
||||
use log::{debug, error, info};
|
||||
use std::path::PathBuf;
|
||||
use uad_core::{
|
||||
config::{BackupSettings, Config, DeviceSettings, GeneralSettings},
|
||||
save::{backup_phone, list_available_backup_user, list_available_backups, restore_backup},
|
||||
sync::{
|
||||
AdbError, CorePackage, Phone, User, get_android_sdk, run_adb_shell_action,
|
||||
supports_multi_user,
|
||||
},
|
||||
utils::{DisplayablePath, Error, NAME, export_packages, generate_backup_name, open_url},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PopUpModal {
|
||||
|
|
@ -101,10 +104,11 @@ impl Settings {
|
|||
Task::none()
|
||||
}
|
||||
|
||||
fn handle_expert_mode(&mut self, phone: &Phone, toggled: bool) -> Task<Message> {
|
||||
fn handle_expert_mode(&mut self, _phone: &Phone, toggled: bool) -> Task<Message> {
|
||||
self.general.expert_mode = toggled;
|
||||
debug!("Config change: {self:?}");
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
let mut config = Config::load_configuration_file();
|
||||
config.save_device_settings(self.device.clone(), self.general.clone());
|
||||
Task::none()
|
||||
}
|
||||
|
||||
|
|
@ -112,22 +116,25 @@ impl Settings {
|
|||
if phone.android_sdk >= 23 {
|
||||
self.device.disable_mode = toggled;
|
||||
debug!("Config change: {self:?}");
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
let mut config = Config::load_configuration_file();
|
||||
config.save_device_settings(self.device.clone(), self.general.clone());
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn handle_multi_user_mode(&mut self, phone: &Phone, toggled: bool) -> Task<Message> {
|
||||
fn handle_multi_user_mode(&mut self, _phone: &Phone, toggled: bool) -> Task<Message> {
|
||||
self.device.multi_user_mode = toggled;
|
||||
debug!("Config change: {self:?}");
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
let mut config = Config::load_configuration_file();
|
||||
config.save_device_settings(self.device.clone(), self.general.clone());
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn handle_apply_theme(&mut self, phone: &Phone, theme: Theme) -> Task<Message> {
|
||||
fn handle_apply_theme(&mut self, _phone: &Phone, theme: Theme) -> Task<Message> {
|
||||
self.general.theme = theme.to_string();
|
||||
debug!("Config change: {self:?}");
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
let mut config = Config::load_configuration_file();
|
||||
config.save_device_settings(self.device.clone(), self.general.clone());
|
||||
Task::none()
|
||||
}
|
||||
|
||||
|
|
@ -182,12 +189,16 @@ impl Settings {
|
|||
phone: &Phone,
|
||||
packages: &[Vec<PackageRow>],
|
||||
) -> Task<Message> {
|
||||
let core_packages: Vec<Vec<_>> = packages
|
||||
.iter()
|
||||
.map(|user_packages| user_packages.iter().map(CorePackage::from).collect())
|
||||
.collect();
|
||||
// Avoid capturing &self and &phone in the future; clone owned data first.
|
||||
let user_list = phone.user_list.clone();
|
||||
let device_id = self.device.device_id.clone();
|
||||
let packages_owned = core_packages;
|
||||
Task::perform(
|
||||
backup_phone(
|
||||
phone.user_list.clone(),
|
||||
self.device.device_id.clone(),
|
||||
packages.to_vec(),
|
||||
),
|
||||
async move { backup_phone(user_list, device_id, &packages_owned) },
|
||||
Message::DeviceBackedUp,
|
||||
)
|
||||
}
|
||||
|
|
@ -217,7 +228,11 @@ impl Settings {
|
|||
packages: &[Vec<PackageRow>],
|
||||
nb_running_async_adb_commands: &mut u32,
|
||||
) -> Task<Message> {
|
||||
match restore_backup(phone, packages, &self.device) {
|
||||
let core_packages: Vec<Vec<_>> = packages
|
||||
.iter()
|
||||
.map(|user_packages| user_packages.iter().map(CorePackage::from).collect())
|
||||
.collect();
|
||||
match restore_backup(phone, &core_packages, &self.device) {
|
||||
Ok(restore_result) => {
|
||||
let mut commands = vec![];
|
||||
*nb_running_async_adb_commands = 0;
|
||||
|
|
@ -230,8 +245,13 @@ impl Settings {
|
|||
};
|
||||
for command in p.commands.clone() {
|
||||
*nb_running_async_adb_commands += 1;
|
||||
let p_info_clone = p_info.clone();
|
||||
let phone_id = phone.adb_id.clone();
|
||||
commands.push(Task::perform(
|
||||
run_adb_action(phone.adb_id.clone(), command, p_info.clone()),
|
||||
async move {
|
||||
run_adb_shell_action(phone_id, command.as_str())
|
||||
.map(|_| p_info_clone)
|
||||
},
|
||||
Message::RestoringDevice,
|
||||
));
|
||||
}
|
||||
|
|
@ -274,7 +294,8 @@ impl Settings {
|
|||
|
||||
if let Ok(path) = result {
|
||||
self.general.backup_folder = path;
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
let mut config = Config::load_configuration_file();
|
||||
config.save_device_settings(self.device.clone(), self.general.clone());
|
||||
self.load_device_settings(phone);
|
||||
}
|
||||
Task::none()
|
||||
|
|
@ -293,8 +314,12 @@ impl Settings {
|
|||
selected_user: Option<User>,
|
||||
packages: &[Vec<PackageRow>],
|
||||
) -> Task<Message> {
|
||||
let core_packages: Vec<Vec<_>> = packages
|
||||
.iter()
|
||||
.map(|user_packages| user_packages.iter().map(CorePackage::from).collect())
|
||||
.collect();
|
||||
Task::perform(
|
||||
export_packages(selected_user.unwrap_or_default(), packages.to_vec()),
|
||||
async move { export_packages(selected_user.unwrap_or_default(), &core_packages) },
|
||||
Message::PackagesExported,
|
||||
)
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ impl<'a, Message, Theme, Renderer> Modal<'a, Message, Theme, Renderer> {
|
|||
|
||||
/// Sets the message that will be produces when the background
|
||||
/// of the [`Modal`] is pressed
|
||||
#[must_use]
|
||||
pub fn on_blur(self, on_blur: Message) -> Self {
|
||||
Self {
|
||||
on_blur: Some(on_blur),
|
||||
|
|
@ -1,19 +1,22 @@
|
|||
use crate::core::helpers::button_primary;
|
||||
pub use crate::core::sync::Phone;
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::update::{SelfUpdateState, SelfUpdateStatus};
|
||||
pub use crate::gui::views::about::Message as AboutMessage;
|
||||
pub use crate::gui::views::list::{List as AppsView, LoadingState as ListLoadingState};
|
||||
use crate::gui::{Message, style, widgets::text};
|
||||
use crate::helpers::button_primary;
|
||||
use crate::theme::Theme;
|
||||
pub use crate::views::about::Message as AboutMessage;
|
||||
pub use crate::views::list::{List as AppsView, LoadingState as ListLoadingState};
|
||||
use crate::{gui::Message, style, widgets::text};
|
||||
use iced::widget::{Space, button, container, pick_list, row, tooltip};
|
||||
use iced::{Alignment, Element, Font, Length, Renderer, alignment, font};
|
||||
pub use uad_core::sync::Phone;
|
||||
use uad_core::update::{SelfUpdateState, SelfUpdateStatus};
|
||||
/// resources/assets/icons.ttf, loaded in [`crate::gui::UadGui`]
|
||||
pub const ICONS: Font = Font {
|
||||
family: font::Family::Name("icomoon"),
|
||||
..Font::DEFAULT
|
||||
};
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
#[allow(
|
||||
clippy::too_many_lines,
|
||||
reason = "Navigation layout composes many widgets inline"
|
||||
)]
|
||||
pub fn nav_menu<'a>(
|
||||
device_list: &'a [Phone],
|
||||
selected_device: Option<Phone>,
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
use crate::core::sync::Phone;
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::uad_lists::{PackageState, Removal, UadList};
|
||||
use crate::gui::style;
|
||||
use crate::gui::views::settings::Settings;
|
||||
use crate::gui::widgets::text;
|
||||
use crate::style;
|
||||
use crate::theme::Theme;
|
||||
use crate::views::settings::Settings;
|
||||
use crate::widgets::text;
|
||||
use log::warn;
|
||||
use uad_core::sync::{CorePackage, Phone};
|
||||
use uad_core::uad_lists::{PackageState, Removal, UadList};
|
||||
|
||||
use iced::widget::{Space, button, checkbox, row};
|
||||
use iced::{Alignment, Element, Length, Renderer, Task, alignment};
|
||||
|
|
@ -11,10 +12,10 @@ use iced::{Alignment, Element, Length, Renderer, Task, alignment};
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct PackageRow {
|
||||
pub name: String,
|
||||
pub state: PackageState,
|
||||
pub description: String,
|
||||
pub uad_list: UadList,
|
||||
pub removal: Removal,
|
||||
pub state: PackageState,
|
||||
pub list: UadList,
|
||||
pub selected: bool,
|
||||
pub current: bool,
|
||||
}
|
||||
|
|
@ -27,21 +28,22 @@ pub enum Message {
|
|||
}
|
||||
|
||||
impl PackageRow {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
name: &str,
|
||||
state: PackageState,
|
||||
description: &str,
|
||||
uad_list: UadList,
|
||||
removal: Removal,
|
||||
state: PackageState,
|
||||
list: UadList,
|
||||
selected: bool,
|
||||
current: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
state,
|
||||
description: description.to_string(),
|
||||
uad_list,
|
||||
removal,
|
||||
state,
|
||||
list,
|
||||
selected,
|
||||
current,
|
||||
}
|
||||
|
|
@ -143,3 +145,56 @@ impl PackageRow {
|
|||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
// Conversions between PackageRow and CorePackage
|
||||
impl From<CorePackage> for PackageRow {
|
||||
fn from(core: CorePackage) -> Self {
|
||||
Self {
|
||||
name: core.name.clone(),
|
||||
description: core.description,
|
||||
removal: core.removal,
|
||||
state: core.state,
|
||||
list: core.list,
|
||||
selected: false, // Default to not selected
|
||||
current: false, // Default to not current
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CorePackage> for PackageRow {
|
||||
fn from(core: &CorePackage) -> Self {
|
||||
Self {
|
||||
name: core.name.clone(),
|
||||
description: core.description.clone(),
|
||||
removal: core.removal,
|
||||
state: core.state,
|
||||
list: core.list,
|
||||
selected: false,
|
||||
current: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PackageRow> for CorePackage {
|
||||
fn from(row: &PackageRow) -> Self {
|
||||
Self {
|
||||
name: row.name.clone(),
|
||||
description: row.description.clone(),
|
||||
removal: row.removal,
|
||||
state: row.state,
|
||||
list: row.list,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PackageRow> for CorePackage {
|
||||
fn from(row: PackageRow) -> Self {
|
||||
Self {
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
removal: row.removal,
|
||||
state: row.state,
|
||||
list: row.list,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,11 @@
|
|||
reason = "this is the replacement that enforces advanced shaping for disallowed [`iced::widget::Text`]"
|
||||
)]
|
||||
|
||||
use crate::theme::Theme;
|
||||
use iced::advanced::text::IntoFragment;
|
||||
use iced::widget;
|
||||
|
||||
// Creates a new Text widget with advanced shaping.
|
||||
pub fn text<'a>(text: impl IntoFragment<'a>) -> widget::Text<'a, crate::core::theme::Theme> {
|
||||
pub fn text<'a>(text: impl IntoFragment<'a>) -> widget::Text<'a, Theme> {
|
||||
widget::Text::new(text).shaping(iced::widget::text::Shaping::Advanced)
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
use crate::core::theme::Theme;
|
||||
use crate::gui::style;
|
||||
use iced::Element;
|
||||
use iced::widget::button;
|
||||
use iced::widget::button::Button;
|
||||
|
||||
/// Wrapper function for `iced::widget::button` with padding and style applied
|
||||
pub fn button_primary<'a, Message>(
|
||||
content: impl Into<Element<'a, Message, Theme>>,
|
||||
) -> Button<'a, Message, Theme> {
|
||||
button(content)
|
||||
.padding([5, 10])
|
||||
.style(style::Button::Primary)
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
pub mod adb;
|
||||
pub mod config;
|
||||
pub mod helpers;
|
||||
pub mod save;
|
||||
pub mod sync;
|
||||
pub mod theme;
|
||||
pub mod uad_lists;
|
||||
pub mod update;
|
||||
pub mod utils;
|
||||
Loading…
Add table
Add a link
Reference in a new issue