mirror of
https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation.git
synced 2026-08-09 15:19:11 +02:00
Merge pull request #1297 from iamanaws/feature/cli
feat: add CLI and restructure into multi-package workspace
This commit is contained in:
commit
4773be1f00
42 changed files with 3074 additions and 639 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-
|
restore-keys: ${{ runner.OS }}-release-
|
||||||
if: matrix.os == 'ubuntu-22.04'
|
if: matrix.os == 'ubuntu-22.04'
|
||||||
- name: Building
|
- 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]
|
- name: Renaming binaries [Windows]
|
||||||
if: matrix.os == 'windows-2022'
|
if: matrix.os == 'windows-2022'
|
||||||
run: mv target/release/uad-ng.exe uad-ng-${{ matrix.build_target }}.exe
|
run: mv target/release/uad-ng.exe uad-ng-${{ matrix.build_target }}.exe
|
||||||
|
|
@ -68,3 +68,59 @@ jobs:
|
||||||
with:
|
with:
|
||||||
name: uad-ng${{ matrix.update_name }}-${{ matrix.build_target }}
|
name: uad-ng${{ matrix.update_name }}-${{ matrix.build_target }}
|
||||||
path: uad-ng-*
|
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
|
- name: Create release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: uad-ng-*/*
|
files: |
|
||||||
|
uad-ng-*/*
|
||||||
|
uad-cli-*/*
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
body: |
|
body: |
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
|
|
|
||||||
805
Cargo.lock
generated
805
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
71
Cargo.toml
71
Cargo.toml
|
|
@ -1,6 +1,12 @@
|
||||||
[package]
|
[workspace]
|
||||||
name = "uad-ng"
|
members = [
|
||||||
description = "A cross-platform GUI debloater for android devices"
|
"crates/uad-core",
|
||||||
|
"crates/uad-gui",
|
||||||
|
"crates/uad-cli"
|
||||||
|
]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
authors = ["Universal-Debloater-Alliance"]
|
authors = ["Universal-Debloater-Alliance"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
|
|
@ -11,59 +17,38 @@ keywords = ["debloater", "android", "adb", "privacy", "bloatware"]
|
||||||
categories = ["gui"]
|
categories = ["gui"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[workspace.dependencies]
|
||||||
default = ["wgpu", "self-update", "img"]
|
# Core dependencies
|
||||||
wgpu = [] # Iced/wgpu is default
|
|
||||||
self-update = ["flate2", "tar"]
|
|
||||||
no-self-update = []
|
|
||||||
img = ["image", "iced/image"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
dark-light = "2"
|
|
||||||
serde = { version = "^1.0", features = ["derive"] }
|
serde = { version = "^1.0", features = ["derive"] }
|
||||||
serde_json = "^1.0"
|
serde_json = "^1.0"
|
||||||
fern = { version = "^0", features = ["colored"] }
|
|
||||||
chrono = { version = "^0.4", default-features = false, features = [
|
|
||||||
"std",
|
|
||||||
"clock",
|
|
||||||
] }
|
|
||||||
log = "^0.4"
|
log = "^0.4"
|
||||||
|
chrono = { version = "^0.4", default-features = false, features = ["std", "clock"] }
|
||||||
toml = "^0"
|
toml = "^0"
|
||||||
dirs = "^6"
|
dirs = "^6"
|
||||||
ureq = { version = "3", features = ["json"] }
|
ureq = { version = "3", features = ["json"] }
|
||||||
retry = "^2.0.0"
|
retry = "^2.0.0"
|
||||||
iced = { version = "=0.14.0", features = ["advanced"] }
|
|
||||||
image = { version = "0.25", optional = true }
|
|
||||||
rfd = "^0.17.1"
|
|
||||||
csv = "^1.3"
|
csv = "^1.3"
|
||||||
|
|
||||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
# GUI dependencies
|
||||||
flate2 = { version = "^1", optional = true }
|
iced = { version = "=0.14.0", features = ["advanced"] }
|
||||||
tar = { version = "^0.4", optional = true }
|
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"
|
win32console = "^0.1.5"
|
||||||
|
|
||||||
[profile.release]
|
[workspace.lints.rust]
|
||||||
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]
|
|
||||||
deprecated_safe = "warn"
|
deprecated_safe = "warn"
|
||||||
|
|
||||||
[lints.clippy]
|
[workspace.lints.clippy]
|
||||||
undocumented_unsafe_blocks = "forbid"
|
undocumented_unsafe_blocks = "forbid"
|
||||||
panic_in_result_fn = "warn"
|
panic_in_result_fn = "warn"
|
||||||
infinite_loop = "warn"
|
infinite_loop = "warn"
|
||||||
|
|
@ -75,4 +60,4 @@ struct_field_names = "allow" # annoying
|
||||||
disallowed_types = "deny"
|
disallowed_types = "deny"
|
||||||
disallowed_methods = "deny"
|
disallowed_methods = "deny"
|
||||||
|
|
||||||
pedantic = { level = "warn", priority = -1 }
|
pedantic = { level = "warn", priority = -1 }
|
||||||
52
crates/uad-cli/Cargo.toml
Normal file
52
crates/uad-cli/Cargo.toml
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
[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
|
||||||
|
|
||||||
|
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 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"
|
||||||
|
|
||||||
476
crates/uad-cli/src/commands.rs
Normal file
476
crates/uad-cli/src/commands.rs
Normal file
|
|
@ -0,0 +1,476 @@
|
||||||
|
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_or_exit!("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_or_exit!("\nFound {} device(s):\n", devices.len());
|
||||||
|
for device in &devices {
|
||||||
|
println_or_exit!(" Model: {}", device.model);
|
||||||
|
println_or_exit!(" Serial: {}", device.adb_id);
|
||||||
|
println_or_exit!(" Android SDK: {}", device.android_sdk);
|
||||||
|
|
||||||
|
if !device.user_list.is_empty() {
|
||||||
|
println_or_exit!(" Users: {} user(s)", device.user_list.len());
|
||||||
|
for user in &device.user_list {
|
||||||
|
let protected = if user.protected { " (protected)" } else { "" };
|
||||||
|
println_or_exit!(" - User ID: {}{}", user.id, protected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println_or_exit!();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = match get_package_state(&device.adb_id, package, Some(user.id)) {
|
||||||
|
Some(state) => state,
|
||||||
|
None if target_state == PackageState::Uninstalled => PackageState::Uninstalled,
|
||||||
|
None => {
|
||||||
|
return Err(format!(
|
||||||
|
"Package '{package}' was not found on device for user {}",
|
||||||
|
user.id
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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>,
|
||||||
|
user_id: Option<u16>,
|
||||||
|
) -> 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))?;
|
||||||
|
let user = get_user(&target_device, user_id)?;
|
||||||
|
println!("Device: {} ({})", target_device.model, target_device.adb_id);
|
||||||
|
println!("User: {}", user.id);
|
||||||
|
|
||||||
|
let state = get_package_state(&target_device.adb_id, package, Some(user.id))
|
||||||
|
.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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
240
crates/uad-cli/src/main.rs
Normal file
240
crates/uad-cli/src/main.rs
Normal file
|
|
@ -0,0 +1,240 @@
|
||||||
|
#![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>,
|
||||||
|
|
||||||
|
/// User ID (defaults to 0)
|
||||||
|
#[arg(short, long)]
|
||||||
|
user: Option<u16>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
user,
|
||||||
|
} => {
|
||||||
|
commands::show_package_info(&package, device, user)?;
|
||||||
|
}
|
||||||
|
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)*);
|
||||||
|
};
|
||||||
|
}
|
||||||
374
crates/uad-cli/src/repl.rs
Normal file
374
crates/uad-cli/src/repl.rs
Normal file
|
|
@ -0,0 +1,374 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
enum ReplAction {
|
||||||
|
Continue,
|
||||||
|
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) => {
|
||||||
|
match handle_repl_line(&line, &mut rl, &target_device, user, user_id, &uad_lists) {
|
||||||
|
Ok(ReplAction::Continue) => {}
|
||||||
|
Ok(ReplAction::Exit) => break,
|
||||||
|
Err(e) => {
|
||||||
|
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<ReplAction, Box<dyn std::error::Error>> {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.is_empty() {
|
||||||
|
return Ok(ReplAction::Continue);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = rl.add_history_entry(line);
|
||||||
|
|
||||||
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
|
if parts.is_empty() {
|
||||||
|
return Ok(ReplAction::Continue);
|
||||||
|
}
|
||||||
|
|
||||||
|
match parts[0] {
|
||||||
|
"help" => print_repl_help(),
|
||||||
|
"exit" | "quit" => {
|
||||||
|
println!("Goodbye!");
|
||||||
|
return Ok(ReplAction::Exit);
|
||||||
|
}
|
||||||
|
"list" | "ls" => {
|
||||||
|
handle_list_command(&parts[1..], device, user_id, uad_lists)?;
|
||||||
|
}
|
||||||
|
"info" => {
|
||||||
|
handle_info_command(&parts[1..], device, user.id, 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(ReplAction::Continue)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
user_id: u16,
|
||||||
|
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, Some(user_id))
|
||||||
|
.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");
|
||||||
|
}
|
||||||
52
crates/uad-core/Cargo.toml
Normal file
52
crates/uad-core/Cargo.toml
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
[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 = []
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
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")]
|
#[cfg(target_os = "windows")]
|
||||||
use std::os::windows::process::CommandExt;
|
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.
|
/// Convert ADB output bytes to a trimmed UTF-8 string.
|
||||||
/// Uses lossy conversion to prevent panics on non-UTF8 output from certain OEMs.
|
/// Uses lossy conversion to prevent panics on non-UTF8 output from certain OEMs.
|
||||||
|
#[must_use]
|
||||||
pub fn to_trimmed_utf8(v: &[u8]) -> String {
|
pub fn to_trimmed_utf8(v: &[u8]) -> String {
|
||||||
String::from_utf8_lossy(v).trim_end().to_string()
|
String::from_utf8_lossy(v).trim_end().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
|
#[must_use]
|
||||||
fn is_version_triple(s: &str) -> bool {
|
fn is_version_triple(s: &str) -> bool {
|
||||||
let mut components = s.split('.');
|
let mut components = s.split('.');
|
||||||
for _ in 0..3 {
|
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.
|
/// Builder object for a command that runs on the device's default `sh` implementation.
|
||||||
/// Typically MKSH, but could be Ash.
|
/// Typically MKSH, but could be Ash.
|
||||||
///
|
///
|
||||||
|
|
@ -273,6 +281,7 @@ impl PackageId {
|
||||||
/// Creates a package-ID if it's valid according to:
|
/// 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/guide/topics/manifest/manifest-element.html#package>
|
||||||
/// - <https://developer.android.com/build/configure-app-module#set-application-id>
|
/// - <https://developer.android.com/build/configure-app-module#set-application-id>
|
||||||
|
#[must_use]
|
||||||
pub fn new(p_id: Box<str>) -> Option<Self> {
|
pub fn new(p_id: Box<str>) -> Option<Self> {
|
||||||
let mut components = p_id.split('.');
|
let mut components = p_id.split('.');
|
||||||
for _ in 0..2 {
|
for _ in 0..2 {
|
||||||
|
|
@ -325,6 +334,7 @@ pub const PM_CLEAR_PACK: &str = "pm clear";
|
||||||
/// Builder object for an Android Package Manager command.
|
/// Builder object for an Android Package Manager command.
|
||||||
/// <https://developer.android.com/tools/adb#pm>
|
/// <https://developer.android.com/tools/adb#pm>
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[must_use]
|
||||||
pub struct PmCommand(ShellCommand);
|
pub struct PmCommand(ShellCommand);
|
||||||
impl PmCommand {
|
impl PmCommand {
|
||||||
/// `list packages -s` sub-command, [`PACK_PREFIX`] stripped.
|
/// `list packages -s` sub-command, [`PACK_PREFIX`] stripped.
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
use crate::CACHE_DIR;
|
use crate::CACHE_DIR;
|
||||||
use crate::CONFIG_DIR;
|
use crate::CONFIG_DIR;
|
||||||
use crate::core::utils::DisplayablePath;
|
use crate::sync::User;
|
||||||
use crate::core::{sync::User, theme::Theme};
|
use crate::utils::DisplayablePath;
|
||||||
use crate::gui::views::settings::Settings;
|
use log::error;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
const DEFAULT_THEME: &str = "Auto (follow system theme)";
|
||||||
|
|
||||||
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
|
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub general: GeneralSettings,
|
pub general: GeneralSettings,
|
||||||
|
|
@ -44,7 +46,7 @@ pub struct DeviceSettings {
|
||||||
impl Default for GeneralSettings {
|
impl Default for GeneralSettings {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
theme: Theme::default().to_string(),
|
theme: DEFAULT_THEME.to_string(),
|
||||||
expert_mode: false,
|
expert_mode: false,
|
||||||
backup_folder: CACHE_DIR.join("backups"),
|
backup_folder: CACHE_DIR.join("backups"),
|
||||||
}
|
}
|
||||||
|
|
@ -54,23 +56,26 @@ impl Default for GeneralSettings {
|
||||||
static CONFIG_FILE: LazyLock<PathBuf> = LazyLock::new(|| CONFIG_DIR.join("config.toml"));
|
static CONFIG_FILE: LazyLock<PathBuf> = LazyLock::new(|| CONFIG_DIR.join("config.toml"));
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn save_changes(settings: &Settings, device_id: &String) {
|
pub fn save_device_settings(
|
||||||
let mut config = Self::load_configuration_file();
|
&mut self,
|
||||||
if let Some(device) = config
|
device_settings: DeviceSettings,
|
||||||
|
general: GeneralSettings,
|
||||||
|
) {
|
||||||
|
if let Some(device) = self
|
||||||
.devices
|
.devices
|
||||||
.iter_mut()
|
.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 {
|
} else {
|
||||||
debug!("config: New device settings saved");
|
self.devices.push(device_settings);
|
||||||
config.devices.push(settings.device.clone());
|
|
||||||
}
|
}
|
||||||
config.general.clone_from(&settings.general);
|
self.general = general;
|
||||||
let toml = toml::to_string(&config).unwrap();
|
let toml = toml::to_string(&self).unwrap();
|
||||||
fs::write(&*CONFIG_FILE, toml).expect("Could not write config file to disk!");
|
fs::write(&*CONFIG_FILE, toml).expect("Could not write config file to disk!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn load_configuration_file() -> Self {
|
pub fn load_configuration_file() -> Self {
|
||||||
match fs::read_to_string(&*CONFIG_FILE) {
|
match fs::read_to_string(&*CONFIG_FILE) {
|
||||||
Ok(s) => match toml::from_str(&s) {
|
Ok(s) => match toml::from_str(&s) {
|
||||||
|
|
@ -111,7 +116,7 @@ mod tests {
|
||||||
let config = Config::load_configuration_file();
|
let config = Config::load_configuration_file();
|
||||||
// non-deterministic
|
// non-deterministic
|
||||||
//assert_eq!(config.devices.len(), 0);
|
//assert_eq!(config.devices.len(), 0);
|
||||||
assert_eq!(config.general.theme, Theme::default().to_string());
|
assert_eq!(config.general.theme, DEFAULT_THEME);
|
||||||
assert!(!config.general.expert_mode);
|
assert!(!config.general.expert_mode);
|
||||||
assert_eq!(config.general.backup_folder, CACHE_DIR.join("backups"));
|
assert_eq!(config.general.backup_folder, CACHE_DIR.join("backups"));
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +138,7 @@ mod tests {
|
||||||
fn test_default_config() {
|
fn test_default_config() {
|
||||||
let config = Config::default();
|
let config = Config::default();
|
||||||
assert_eq!(config.devices.len(), 0);
|
assert_eq!(config.devices.len(), 0);
|
||||||
assert_eq!(config.general.theme, Theme::default().to_string());
|
assert_eq!(config.general.theme, DEFAULT_THEME);
|
||||||
assert!(!config.general.expert_mode);
|
assert!(!config.general.expert_mode);
|
||||||
assert_eq!(config.general.backup_folder, CACHE_DIR.join("backups"));
|
assert_eq!(config.general.backup_folder, CACHE_DIR.join("backups"));
|
||||||
}
|
}
|
||||||
23
crates/uad-core/src/lib.rs
Normal file
23
crates/uad-core/src/lib.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
#![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 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::config::{Config, DeviceSettings};
|
||||||
use crate::core::sync::{CorePackage, Phone, User, apply_pkg_state_commands};
|
use crate::sync::{CorePackage, Phone, User, apply_pkg_state_commands};
|
||||||
use crate::core::utils::DisplayablePath;
|
use crate::utils::DisplayablePath;
|
||||||
use crate::gui::widgets::package_row::PackageRow;
|
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
|
|
@ -22,10 +21,10 @@ pub struct UserBackup {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Backup all `Uninstalled` and `Disabled` packages
|
/// Backup all `Uninstalled` and `Disabled` packages
|
||||||
pub async fn backup_phone(
|
pub fn backup_phone(
|
||||||
users: Vec<User>,
|
users: Vec<User>,
|
||||||
device_id: String,
|
device_id: String,
|
||||||
phone_packages: Vec<Vec<PackageRow>>,
|
phone_packages: &[Vec<CorePackage>],
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
let mut backup = PhoneBackup {
|
let mut backup = PhoneBackup {
|
||||||
device_id: device_id.clone(),
|
device_id: device_id.clone(),
|
||||||
|
|
@ -38,11 +37,8 @@ pub async fn backup_phone(
|
||||||
..UserBackup::default()
|
..UserBackup::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
for p in phone_packages[u.index].clone() {
|
for p in phone_packages[u.index].iter().cloned() {
|
||||||
user_backup.packages.push(CorePackage {
|
user_backup.packages.push(p);
|
||||||
name: p.name.clone(),
|
|
||||||
state: p.state,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
backup.users.push(user_backup);
|
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> {
|
pub fn list_available_backup_user(backup: DisplayablePath) -> Vec<User> {
|
||||||
match fs::read_to_string(backup.path) {
|
match fs::read_to_string(backup.path) {
|
||||||
Ok(data) => serde_json::from_str::<PhoneBackup>(&data)
|
Ok(data) => serde_json::from_str::<PhoneBackup>(&data)
|
||||||
|
|
@ -113,7 +110,7 @@ pub struct RestoreResult {
|
||||||
|
|
||||||
pub fn restore_backup(
|
pub fn restore_backup(
|
||||||
selected_device: &Phone,
|
selected_device: &Phone,
|
||||||
packages: &[Vec<PackageRow>],
|
packages: &[Vec<CorePackage>],
|
||||||
settings: &DeviceSettings,
|
settings: &DeviceSettings,
|
||||||
) -> Result<RestoreResult, String> {
|
) -> Result<RestoreResult, String> {
|
||||||
match fs::read_to_string(
|
match fs::read_to_string(
|
||||||
|
|
@ -142,7 +139,7 @@ pub fn restore_backup(
|
||||||
.iter()
|
.iter()
|
||||||
.find(|x| x.name == backup_package.name)
|
.find(|x| x.name == backup_package.name)
|
||||||
{
|
{
|
||||||
p.into()
|
p.clone()
|
||||||
} else {
|
} else {
|
||||||
skipped_packages += 1;
|
skipped_packages += 1;
|
||||||
warn!(
|
warn!(
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::core::{
|
use crate::{
|
||||||
adb::{ACommand as AdbCommand, PM_CLEAR_PACK},
|
adb::{ACommand as AdbCommand, PM_CLEAR_PACK},
|
||||||
uad_lists::PackageState,
|
uad_lists::PackageState,
|
||||||
};
|
};
|
||||||
use crate::gui::{views::list::PackageInfo, widgets::package_row::PackageRow};
|
use log::{error, info};
|
||||||
use retry::{OperationResult, delay::Fixed, retry};
|
use retry::{OperationResult, delay::Fixed, retry};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
@ -61,27 +61,25 @@ pub enum AdbError {
|
||||||
/// This replaces the deprecated `adb_shell_command`.
|
/// This replaces the deprecated `adb_shell_command`.
|
||||||
///
|
///
|
||||||
/// If `serial` is empty, it lets ADB choose the default device.
|
/// 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,
|
device_serial: S,
|
||||||
action: String,
|
action: &str,
|
||||||
p: PackageInfo,
|
) -> Result<String, AdbError> {
|
||||||
) -> Result<PackageInfo, AdbError> {
|
|
||||||
let serial = device_serial.as_ref();
|
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) => {
|
Ok(o) => {
|
||||||
if ["Error", "Failure"].iter().any(|&e| o.contains(e)) {
|
if ["Error", "Failure"].iter().any(|&e| o.contains(e)) {
|
||||||
let friendly_msg = make_friendly_error_message(&o, &action);
|
let friendly_msg = make_friendly_error_message(&o, action);
|
||||||
return Err(AdbError::Generic(format!("[{label}] {friendly_msg}")));
|
return Err(AdbError::Generic(friendly_msg));
|
||||||
}
|
}
|
||||||
info!("[{label}] {action} -> {o}");
|
info!("{action} -> {o}");
|
||||||
Ok(p)
|
Ok(o)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if !err.contains("[not installed for") {
|
if !err.contains("[not installed for") {
|
||||||
let friendly_msg = make_friendly_error_message(&err, &action);
|
let friendly_msg = make_friendly_error_message(&err, action);
|
||||||
return Err(AdbError::Generic(format!("[{label}] {friendly_msg}")));
|
return Err(AdbError::Generic(friendly_msg));
|
||||||
}
|
}
|
||||||
Err(AdbError::Generic(err))
|
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"
|
/// If `None`, returns an empty String, not " --user 0"
|
||||||
|
#[must_use]
|
||||||
pub fn user_flag(user_id: Option<User>) -> String {
|
pub fn user_flag(user_id: Option<User>) -> String {
|
||||||
user_id
|
user_id
|
||||||
.map(|user| format!(" --user {}", 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)]
|
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||||
pub struct CorePackage {
|
pub struct CorePackage {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub removal: crate::uad_lists::Removal,
|
||||||
|
#[serde(default)]
|
||||||
pub state: PackageState,
|
pub state: PackageState,
|
||||||
|
#[serde(default)]
|
||||||
|
pub list: crate::uad_lists::UadList,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&mut PackageRow> for CorePackage {
|
#[must_use]
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apply_pkg_state_commands(
|
pub fn apply_pkg_state_commands(
|
||||||
package: &CorePackage,
|
package: &CorePackage,
|
||||||
wanted_state: PackageState,
|
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.
|
/// Build a command request to be sent via ADB to a device.
|
||||||
/// `commands` accepts one or more ADB shell commands
|
/// `commands` accepts one or more ADB shell commands
|
||||||
/// which act on a common `package` and `user`.
|
/// which act on a common `package` and `user`.
|
||||||
|
#[must_use]
|
||||||
pub fn request_builder(commands: &[&str], package: &str, user: Option<User>) -> Vec<String> {
|
pub fn request_builder(commands: &[&str], package: &str, user: Option<User>) -> Vec<String> {
|
||||||
let maybe_user_flag = user_flag(user);
|
let maybe_user_flag = user_flag(user);
|
||||||
commands
|
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.
|
/// Get the model by querying the `ro.product.model` property.
|
||||||
///
|
///
|
||||||
/// If `serial` is empty, it lets ADB choose the default device.
|
/// If `serial` is empty, it lets ADB choose the default device.
|
||||||
|
#[must_use]
|
||||||
pub fn get_device_model(serial: &str) -> String {
|
pub fn get_device_model(serial: &str) -> String {
|
||||||
AdbCommand::new()
|
AdbCommand::new()
|
||||||
.shell(serial)
|
.shell(serial)
|
||||||
|
|
@ -253,6 +235,7 @@ pub fn get_device_model(serial: &str) -> String {
|
||||||
/// Get the brand by querying the `ro.product.brand` property.
|
/// Get the brand by querying the `ro.product.brand` property.
|
||||||
///
|
///
|
||||||
/// If `serial` is empty, it lets ADB choose the default device.
|
/// If `serial` is empty, it lets ADB choose the default device.
|
||||||
|
#[must_use]
|
||||||
pub fn get_device_brand(serial: &str) -> String {
|
pub fn get_device_brand(serial: &str) -> String {
|
||||||
AdbCommand::new()
|
AdbCommand::new()
|
||||||
.shell(serial)
|
.shell(serial)
|
||||||
|
|
@ -266,6 +249,7 @@ pub fn get_device_brand(serial: &str) -> String {
|
||||||
// `ro.build.version.sdk` property or defaulting to 0.
|
// `ro.build.version.sdk` property or defaulting to 0.
|
||||||
///
|
///
|
||||||
/// If `device_serial` is empty, it lets ADB choose the default device.
|
/// If `device_serial` is empty, it lets ADB choose the default device.
|
||||||
|
#[must_use]
|
||||||
pub fn get_android_sdk(device_serial: &str) -> u8 {
|
pub fn get_android_sdk(device_serial: &str) -> u8 {
|
||||||
AdbCommand::new()
|
AdbCommand::new()
|
||||||
.shell(device_serial)
|
.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
|
/// Only includes users where the package exists (Some state). Users where the package
|
||||||
/// doesn't exist (None) are not tracked.
|
/// doesn't exist (None) are not tracked.
|
||||||
|
#[must_use]
|
||||||
pub fn capture_cross_user_states(
|
pub fn capture_cross_user_states(
|
||||||
package_name: &str,
|
package_name: &str,
|
||||||
device_serial: &str,
|
device_serial: &str,
|
||||||
|
|
@ -301,6 +286,7 @@ pub fn capture_cross_user_states(
|
||||||
/// - Case A: Uninstall → Restore (package appears on other users)
|
/// - Case A: Uninstall → Restore (package appears on other users)
|
||||||
/// - Case B: Uninstall → Uninstall (package disappears from other users that previously had it)
|
/// - Case B: Uninstall → Uninstall (package disappears from other users that previously had it)
|
||||||
/// - Case C: Restore → Restore (package appears on other users)
|
/// - Case C: Restore → Restore (package appears on other users)
|
||||||
|
#[must_use]
|
||||||
pub fn detect_cross_user_behavior(
|
pub fn detect_cross_user_behavior(
|
||||||
package_name: &str,
|
package_name: &str,
|
||||||
device_serial: &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()
|
.is_err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn list_users_idx_prot(device_serial: &str) -> Vec<User> {
|
pub fn list_users_idx_prot(device_serial: &str) -> Vec<User> {
|
||||||
AdbCommand::new()
|
AdbCommand::new()
|
||||||
.shell(device_serial)
|
.shell(device_serial)
|
||||||
|
|
@ -456,9 +443,10 @@ pub fn list_users_idx_prot(device_serial: &str) -> Vec<User> {
|
||||||
|
|
||||||
/// This matches serials (`getprop ro.serialno`)
|
/// This matches serials (`getprop ro.serialno`)
|
||||||
/// that are authorized by the user.
|
/// that are authorized by the user.
|
||||||
pub async fn get_devices_list() -> Vec<Phone> {
|
#[must_use]
|
||||||
|
pub fn get_devices_list() -> Vec<Phone> {
|
||||||
retry(
|
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() {
|
|| match AdbCommand::new().devices() {
|
||||||
Ok(devices) => {
|
Ok(devices) => {
|
||||||
let mut device_list: Vec<Phone> = vec![];
|
let mut device_list: Vec<Phone> = vec![];
|
||||||
|
|
@ -486,20 +474,22 @@ pub async fn get_devices_list() -> Vec<Phone> {
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn initial_load() -> bool {
|
#[must_use]
|
||||||
|
pub fn initial_load() -> bool {
|
||||||
match AdbCommand::new().devices() {
|
match AdbCommand::new().devices() {
|
||||||
Ok(_devices) => true,
|
Ok(_devices) => true,
|
||||||
Err(_err) => false,
|
Err(_err) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify the actual state of a package on the device
|
/// Get the current state of a package on a device
|
||||||
pub fn verify_package_state(
|
#[must_use]
|
||||||
package_name: &str,
|
pub fn get_package_state(
|
||||||
device_serial: &str,
|
device_serial: &str,
|
||||||
|
package_name: &str,
|
||||||
user_id: Option<u16>,
|
user_id: Option<u16>,
|
||||||
) -> Option<PackageState> {
|
) -> Option<PackageState> {
|
||||||
use crate::core::adb::{ACommand as AdbCommand, PmListPacksFlag};
|
use crate::adb::{ACommand as AdbCommand, PmListPacksFlag};
|
||||||
|
|
||||||
// Check if package is enabled
|
// Check if package is enabled
|
||||||
if let Ok(enabled_packages) = AdbCommand::new()
|
if let Ok(enabled_packages) = AdbCommand::new()
|
||||||
|
|
@ -535,11 +525,23 @@ pub fn verify_package_state(
|
||||||
None
|
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.
|
/// Check if a package exists on any other users besides the target user.
|
||||||
/// This helps detect OEM-specific cross-user restoration behavior.
|
/// This helps detect OEM-specific cross-user restoration behavior.
|
||||||
///
|
///
|
||||||
/// Only includes users where the package exists in a non-uninstalled state
|
/// Only includes users where the package exists in a non-uninstalled state
|
||||||
/// (i.e., Enabled or Disabled).
|
/// (i.e., Enabled or Disabled).
|
||||||
|
#[must_use]
|
||||||
pub fn check_cross_user_package_existence(
|
pub fn check_cross_user_package_existence(
|
||||||
package_name: &str,
|
package_name: &str,
|
||||||
device_serial: &str,
|
device_serial: &str,
|
||||||
|
|
@ -561,9 +563,29 @@ pub fn check_cross_user_package_existence(
|
||||||
other_user_states
|
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
|
/// Attempt fallback action when package state verification fails
|
||||||
pub fn attempt_fallback(
|
pub fn attempt_fallback(
|
||||||
package: &crate::gui::widgets::package_row::PackageRow,
|
package: &CorePackage,
|
||||||
wanted_state: PackageState,
|
wanted_state: PackageState,
|
||||||
actual_state: PackageState,
|
actual_state: PackageState,
|
||||||
user: User,
|
user: User,
|
||||||
|
|
@ -572,101 +594,49 @@ pub fn attempt_fallback(
|
||||||
match (wanted_state, actual_state) {
|
match (wanted_state, actual_state) {
|
||||||
// Case 1: Tried to uninstall but package was reinstalled -> disable it
|
// Case 1: Tried to uninstall but package was reinstalled -> disable it
|
||||||
(PackageState::Uninstalled, PackageState::Enabled) => {
|
(PackageState::Uninstalled, PackageState::Enabled) => {
|
||||||
let core_package = CorePackage {
|
let pkg = package_with_state(package, PackageState::Enabled);
|
||||||
name: package.name.clone(),
|
let commands = apply_pkg_state_commands(&pkg, PackageState::Disabled, user, phone);
|
||||||
state: PackageState::Enabled,
|
execute_first_command(&commands, phone)
|
||||||
};
|
.map(|()| "disabled package instead of uninstalling".to_string())
|
||||||
let commands =
|
.map_err(|e| format!("Failed to disable package: {e}"))
|
||||||
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}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case 2: Tried to disable but package re-enabled itself -> try uninstall
|
// Case 2: Tried to disable but package re-enabled itself -> try uninstall
|
||||||
(PackageState::Disabled, PackageState::Enabled) => {
|
(PackageState::Disabled, PackageState::Enabled) => {
|
||||||
let core_package = CorePackage {
|
let pkg = package_with_state(package, PackageState::Enabled);
|
||||||
name: package.name.clone(),
|
let commands = apply_pkg_state_commands(&pkg, PackageState::Uninstalled, user, phone);
|
||||||
state: PackageState::Enabled,
|
execute_first_command(&commands, phone)
|
||||||
};
|
.map_err(|e| format!("Failed to uninstall: {e}"))?;
|
||||||
let commands =
|
|
||||||
apply_pkg_state_commands(&core_package, PackageState::Uninstalled, user, phone);
|
|
||||||
|
|
||||||
if commands.is_empty() {
|
// Verify the package was actually uninstalled
|
||||||
Err("No uninstall command available for this Android version".to_string())
|
match verify_package_state(&package.name, &phone.adb_id, Some(user.id)) {
|
||||||
} else {
|
Some(PackageState::Uninstalled) | None => {
|
||||||
// Execute the uninstall command
|
Ok("uninstalled package instead of disabling".to_string())
|
||||||
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}")),
|
|
||||||
}
|
}
|
||||||
|
_ => Err("Package still exists after uninstall attempt".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Case 3: Tried to enable but package was disabled -> try uninstall then reinstall
|
// Case 3: Tried to enable but package was disabled -> try uninstall then reinstall
|
||||||
(PackageState::Enabled, PackageState::Disabled) => {
|
(PackageState::Enabled, PackageState::Disabled) => {
|
||||||
// First try to uninstall
|
let pkg = package_with_state(package, PackageState::Disabled);
|
||||||
let core_package = CorePackage {
|
let uninstall_cmds =
|
||||||
name: package.name.clone(),
|
apply_pkg_state_commands(&pkg, PackageState::Uninstalled, user, phone);
|
||||||
state: PackageState::Disabled,
|
execute_first_command(&uninstall_cmds, phone)
|
||||||
};
|
.map_err(|e| format!("Failed to uninstall for reinstall: {e}"))?;
|
||||||
let uninstall_commands =
|
|
||||||
apply_pkg_state_commands(&core_package, PackageState::Uninstalled, user, phone);
|
|
||||||
|
|
||||||
if uninstall_commands.is_empty() {
|
// Now try to reinstall/enable
|
||||||
Err("No uninstall command available for reinstall attempt".to_string())
|
let pkg_uninstalled = package_with_state(package, PackageState::Uninstalled);
|
||||||
} else {
|
let enable_cmds =
|
||||||
let uninstall_action = uninstall_commands[0].clone();
|
apply_pkg_state_commands(&pkg_uninstalled, PackageState::Enabled, user, phone);
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
if enable_commands.is_empty() {
|
if enable_cmds.is_empty() {
|
||||||
Ok("uninstalled package but couldn't reinstall".to_string())
|
return 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}")),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Other cases - no fallback available
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::CACHE_DIR;
|
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 retry::{OperationResult, delay::Fixed, retry};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json;
|
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"
|
reason = "https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/discussions/608"
|
||||||
)]
|
)]
|
||||||
// not `const`, because it's too big
|
// 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)]
|
#[derive(Deserialize, Debug, Clone, PartialEq, Hash, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|
@ -28,7 +29,7 @@ pub struct Package {
|
||||||
pub removal: Removal,
|
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 {
|
pub enum UadList {
|
||||||
#[default]
|
#[default]
|
||||||
All,
|
All,
|
||||||
|
|
@ -73,6 +74,7 @@ impl UadList {
|
||||||
Self::Unlisted,
|
Self::Unlisted,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub const fn as_str(self) -> &'static str {
|
pub const fn as_str(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::All => "All lists",
|
Self::All => "All lists",
|
||||||
|
|
@ -148,7 +150,7 @@ impl Opposite for PackageState {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bad names. To be changed!
|
// 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 {
|
pub enum Removal {
|
||||||
#[default]
|
#[default]
|
||||||
Recommended,
|
Recommended,
|
||||||
|
|
@ -176,6 +178,7 @@ impl Removal {
|
||||||
Self::Unlisted,
|
Self::Unlisted,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub const fn as_str(self) -> &'static str {
|
pub const fn as_str(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::All => "All removals",
|
Self::All => "All removals",
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use crate::core::utils::NAME;
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[cfg(feature = "self-update")]
|
#[cfg(feature = "self-update")]
|
||||||
use {
|
use {
|
||||||
|
crate::utils::NAME,
|
||||||
|
log::{debug, error},
|
||||||
retry::{OperationResult, delay::Fibonacci, retry},
|
retry::{OperationResult, delay::Fibonacci, retry},
|
||||||
std::fs,
|
std::fs,
|
||||||
std::io,
|
std::io,
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
#![warn(clippy::unwrap_used)]
|
#![warn(clippy::unwrap_used)]
|
||||||
|
|
||||||
use crate::core::{
|
use crate::{
|
||||||
adb::{ACommand as AdbCommand, PmListPacksFlag},
|
adb::{ACommand as AdbCommand, PmListPacksFlag},
|
||||||
sync::User,
|
sync::{CorePackage, User},
|
||||||
theme::Theme,
|
|
||||||
uad_lists::{PackageHashMap, PackageState, Removal, UadList},
|
uad_lists::{PackageHashMap, PackageState, Removal, UadList},
|
||||||
};
|
};
|
||||||
use crate::gui::widgets::package_row::PackageRow;
|
|
||||||
use chrono::{DateTime, offset::Utc};
|
use chrono::{DateTime, offset::Utc};
|
||||||
use csv::Writer;
|
use csv::Writer;
|
||||||
|
use log::error;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
fmt, fs,
|
fmt, fs,
|
||||||
|
|
@ -66,11 +65,12 @@ pub enum Error {
|
||||||
DialogClosed,
|
DialogClosed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn fetch_packages(
|
pub fn fetch_packages(
|
||||||
uad_lists: &PackageHashMap,
|
uad_lists: &PackageHashMap,
|
||||||
device_serial: &str,
|
device_serial: &str,
|
||||||
user_id: Option<u16>,
|
user_id: Option<u16>,
|
||||||
) -> Vec<PackageRow> {
|
) -> Vec<CorePackage> {
|
||||||
let all_sys_packs = AdbCommand::new()
|
let all_sys_packs = AdbCommand::new()
|
||||||
.shell(device_serial)
|
.shell(device_serial)
|
||||||
.pm()
|
.pm()
|
||||||
|
|
@ -92,24 +92,24 @@ pub fn fetch_packages(
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut description;
|
let mut description;
|
||||||
let mut uad_list;
|
|
||||||
let mut state;
|
|
||||||
let mut removal;
|
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 {
|
for pack_name in all_sys_packs {
|
||||||
let p_name = &pack_name;
|
let p_name = &pack_name;
|
||||||
state = PackageState::Uninstalled;
|
description = String::from("[No description]: CONTRIBUTION WELCOMED");
|
||||||
description = "[No description]: CONTRIBUTION WELCOMED";
|
|
||||||
uad_list = UadList::Unlisted;
|
|
||||||
removal = Removal::Unlisted;
|
removal = Removal::Unlisted;
|
||||||
|
state = PackageState::Uninstalled;
|
||||||
|
list = UadList::Unlisted;
|
||||||
|
|
||||||
if let Some(package) = uad_lists.get(p_name) {
|
if let Some(package) = uad_lists.get(p_name) {
|
||||||
if !package.description.is_empty() {
|
if !package.description.is_empty() {
|
||||||
description = &package.description;
|
description = package.description.clone();
|
||||||
}
|
}
|
||||||
uad_list = package.list;
|
|
||||||
removal = package.removal;
|
removal = package.removal;
|
||||||
|
list = package.list;
|
||||||
}
|
}
|
||||||
|
|
||||||
if enabled_sys_packs.contains(p_name) {
|
if enabled_sys_packs.contains(p_name) {
|
||||||
|
|
@ -118,25 +118,20 @@ pub fn fetch_packages(
|
||||||
state = PackageState::Disabled;
|
state = PackageState::Disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
let package_row =
|
let package = CorePackage {
|
||||||
PackageRow::new(p_name, state, description, uad_list, removal, false, false);
|
name: p_name.clone(),
|
||||||
user_package.push(package_row);
|
description,
|
||||||
|
removal,
|
||||||
|
state,
|
||||||
|
list,
|
||||||
|
};
|
||||||
|
user_package.push(package);
|
||||||
}
|
}
|
||||||
user_package.sort_by_key(|a| a.name.to_lowercase());
|
user_package.sort_by_key(|package| package.name.to_lowercase());
|
||||||
user_package
|
user_package
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn string_to_theme(theme: &str) -> Theme {
|
#[must_use]
|
||||||
match theme {
|
|
||||||
"Dark" => Theme::Dark,
|
|
||||||
"Light" => Theme::Light,
|
|
||||||
"Lupin" => Theme::Lupin,
|
|
||||||
// Auto uses `Display`, so it doesn't have a canonical repr
|
|
||||||
t if t.starts_with("Auto") => Theme::Auto,
|
|
||||||
_ => Theme::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn setup_uad_dir(dir: &Path) -> PathBuf {
|
pub fn setup_uad_dir(dir: &Path) -> PathBuf {
|
||||||
let dir = dir.join("uad");
|
let dir = dir.join("uad");
|
||||||
if let Err(e) = fs::create_dir_all(&dir) {
|
if let Err(e) = fs::create_dir_all(&dir) {
|
||||||
|
|
@ -173,6 +168,7 @@ pub fn open_url(dir: PathBuf) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
|
#[must_use]
|
||||||
pub fn last_modified_date(file: PathBuf) -> DateTime<Utc> {
|
pub fn last_modified_date(file: PathBuf) -> DateTime<Utc> {
|
||||||
fs::metadata(file).map_or_else(|_| Utc::now(), |metadata| match metadata.modified() {
|
fs::metadata(file).map_or_else(|_| Utc::now(), |metadata| match metadata.modified() {
|
||||||
Ok(time) => time.into(),
|
Ok(time) => time.into(),
|
||||||
|
|
@ -180,6 +176,7 @@ pub fn last_modified_date(file: PathBuf) -> DateTime<Utc> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn format_diff_time_from_now(date: DateTime<Utc>) -> String {
|
pub fn format_diff_time_from_now(date: DateTime<Utc>) -> String {
|
||||||
let now: DateTime<Utc> = Utc::now();
|
let now: DateTime<Utc> = Utc::now();
|
||||||
let last_update = now - date;
|
let last_update = now - date;
|
||||||
|
|
@ -194,15 +191,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.
|
/// File will be saved in same directory where UAD-ng is located.
|
||||||
pub async fn export_selection(packages: Vec<PackageRow>) -> Result<bool, String> {
|
pub fn export_selection(package_names: &[String]) -> Result<bool, String> {
|
||||||
let selected = packages
|
let selected = package_names.join("\n");
|
||||||
.iter()
|
|
||||||
.filter(|p| p.selected)
|
|
||||||
.map(|p| p.name.clone())
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
match fs::write(EXPORT_FILE_NAME, selected) {
|
match fs::write(EXPORT_FILE_NAME, selected) {
|
||||||
Ok(()) => Ok(true),
|
Ok(()) => Ok(true),
|
||||||
|
|
@ -236,22 +228,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.
|
/// Export uninstalled packages in a csv file.
|
||||||
/// Exported information will contain package name and description.
|
/// Exported information will contain package name and description.
|
||||||
pub async fn export_packages(
|
pub fn export_packages(user: User, phone_packages: &[Vec<CorePackage>]) -> Result<bool, String> {
|
||||||
user: User,
|
|
||||||
phone_packages: Vec<Vec<PackageRow>>,
|
|
||||||
) -> Result<bool, String> {
|
|
||||||
let backup_file = generate_backup_name(chrono::Local::now());
|
let backup_file = generate_backup_name(chrono::Local::now());
|
||||||
|
|
||||||
let file = fs::File::create(backup_file).map_err(|err| err.to_string())?;
|
let file = fs::File::create(backup_file).map_err(|err| err.to_string())?;
|
||||||
|
|
@ -260,7 +239,7 @@ pub async fn export_packages(
|
||||||
wtr.write_record(["Package Name", "Description"])
|
wtr.write_record(["Package Name", "Description"])
|
||||||
.map_err(|err| err.to_string())?;
|
.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()
|
.iter()
|
||||||
.filter(|p| p.state == PackageState::Uninstalled)
|
.filter(|p| p.state == PackageState::Uninstalled)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -275,6 +254,36 @@ pub async fn export_packages(
|
||||||
Ok(true)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
62
crates/uad-gui/Cargo.toml
Normal file
62
crates/uad-gui/Cargo.toml
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
[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 = ["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
|
||||||
|
|
||||||
|
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};
|
|
||||||
#[cfg(feature = "img")]
|
#[cfg(feature = "img")]
|
||||||
use crate::core::theme::OS_COLOR_SCHEME;
|
use crate::theme::OS_COLOR_SCHEME;
|
||||||
use crate::core::theme::Theme;
|
use crate::theme::Theme;
|
||||||
use crate::core::uad_lists::UadListState;
|
use crate::theme::string_to_theme;
|
||||||
use crate::core::update::{Release, SelfUpdateState, SelfUpdateStatus, get_latest_release};
|
use crate::views::about::{About as AboutView, Message as AboutMessage};
|
||||||
use crate::core::utils::{FULL_NAME, NAME, string_to_theme};
|
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;
|
use iced::font;
|
||||||
|
use iced::widget::column;
|
||||||
#[cfg(feature = "img")]
|
#[cfg(feature = "img")]
|
||||||
use iced::window::icon;
|
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::{Alignment, Element, Length, Settings, Task, window::Settings as Window};
|
||||||
use iced::{Subscription, event, keyboard};
|
use iced::{Subscription, event, keyboard};
|
||||||
|
#[cfg(feature = "img")]
|
||||||
|
use image::ImageFormat;
|
||||||
|
use log::{debug, error, info};
|
||||||
#[cfg(feature = "self-update")]
|
#[cfg(feature = "self-update")]
|
||||||
use std::path::PathBuf;
|
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")]
|
#[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)]
|
#[derive(Default, Debug, Clone)]
|
||||||
enum View {
|
enum View {
|
||||||
|
|
@ -41,8 +38,8 @@ enum View {
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub struct UpdateState {
|
pub struct UpdateState {
|
||||||
self_update: SelfUpdateState,
|
pub self_update: SelfUpdateState,
|
||||||
uad_list: UadListState,
|
pub uad_list: UadListState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -86,18 +83,21 @@ impl UadGui {
|
||||||
reason = "required by iced's Application trait interface"
|
reason = "required by iced's Application trait interface"
|
||||||
)]
|
)]
|
||||||
fn title(&self) -> String {
|
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>) {
|
fn new() -> (Self, Task<Message>) {
|
||||||
(
|
(
|
||||||
Self::default(),
|
Self::default(),
|
||||||
Task::batch([
|
Task::batch([
|
||||||
// Used in crate::gui::widgets::navigation_menu::ICONS. Name is `icomoon`.
|
// Used in crate::widgets::navigation_menu::ICONS. Name is `icomoon`.
|
||||||
font::load(include_bytes!("../../resources/assets/icons.ttf").as_slice())
|
font::load(include_bytes!("../../../resources/assets/icons.ttf").as_slice())
|
||||||
.map(Message::FontLoaded),
|
.map(Message::FontLoaded),
|
||||||
Task::perform(initial_load(), Message::ADBSatisfied),
|
Task::perform(async { initial_load() }, Message::ADBSatisfied),
|
||||||
Task::perform(get_devices_list(), Message::LoadDevices),
|
Task::perform(async { get_devices_list() }, Message::LoadDevices),
|
||||||
Task::perform(
|
Task::perform(
|
||||||
async move { get_latest_release() },
|
async move { get_latest_release() },
|
||||||
Message::GetLatestRelease,
|
Message::GetLatestRelease,
|
||||||
|
|
@ -111,21 +111,45 @@ impl UadGui {
|
||||||
reason = "required by iced's Application trait interface"
|
reason = "required by iced's Application trait interface"
|
||||||
)]
|
)]
|
||||||
fn subscription(&self) -> Subscription<Message> {
|
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 {
|
iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
||||||
key: keyboard::Key::Character(c),
|
key: keyboard::Key::Character(c),
|
||||||
modifiers,
|
modifiers,
|
||||||
..
|
..
|
||||||
}) if modifiers.control() && modifiers.shift() => match c.as_str() {
|
}) if modifiers.control() && modifiers.shift() => match c.as_str() {
|
||||||
"r" => Some(Message::RebootButtonPressed),
|
"r" if can_control_device => Some(Message::RebootButtonPressed),
|
||||||
"5" => Some(Message::RefreshButtonPressed),
|
"5" if can_control_device => Some(Message::RefreshButtonPressed),
|
||||||
"a" => Some(Message::AppsPress),
|
"a" => Some(Message::AppsPress),
|
||||||
"i" => Some(Message::AboutPressed),
|
"i" => Some(Message::AboutPressed),
|
||||||
"s" => Some(Message::SettingsPressed),
|
"s" => Some(Message::SettingsPressed),
|
||||||
_ => None,
|
_ => None,
|
||||||
},
|
},
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(
|
#[allow(
|
||||||
|
|
@ -178,7 +202,7 @@ impl UadGui {
|
||||||
self.adb_satisfied,
|
self.adb_satisfied,
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
Task::perform(get_devices_list(), Message::LoadDevices)
|
Task::perform(async { get_devices_list() }, Message::LoadDevices)
|
||||||
}
|
}
|
||||||
Message::RebootButtonPressed => {
|
Message::RebootButtonPressed => {
|
||||||
self.apps_view = AppsView::default();
|
self.apps_view = AppsView::default();
|
||||||
|
|
@ -410,12 +434,12 @@ impl UadGui {
|
||||||
pub fn start() -> iced::Result {
|
pub fn start() -> iced::Result {
|
||||||
#[cfg(feature = "img")]
|
#[cfg(feature = "img")]
|
||||||
let logo: &[u8] = match *OS_COLOR_SCHEME {
|
let logo: &[u8] = match *OS_COLOR_SCHEME {
|
||||||
// remember to keep `Unspecified` in sync with `src/core/theme`
|
// remember to keep `Unspecified` in sync with `src/theme`
|
||||||
dark_light::Mode::Dark | dark_light::Mode::Unspecified => {
|
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 => {
|
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"]
|
#![windows_subsystem = "windows"]
|
||||||
#[macro_use]
|
|
||||||
extern crate log;
|
|
||||||
|
|
||||||
use crate::core::utils::setup_uad_dir;
|
|
||||||
use fern::{
|
use fern::{
|
||||||
FormatCallback,
|
FormatCallback,
|
||||||
colors::{Color, ColoredLevelConfig},
|
colors::{Color, ColoredLevelConfig},
|
||||||
|
|
@ -10,12 +7,10 @@ use fern::{
|
||||||
use log::Record;
|
use log::Record;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use std::{fmt::Arguments, fs::OpenOptions, path::PathBuf};
|
use std::{fmt::Arguments, fs::OpenOptions, path::PathBuf};
|
||||||
|
use uad_core::utils::setup_uad_dir;
|
||||||
|
|
||||||
mod core;
|
use uad_gui::gui::UadGui;
|
||||||
mod gui;
|
|
||||||
|
|
||||||
static CONFIG_DIR: LazyLock<PathBuf> =
|
|
||||||
LazyLock::new(|| setup_uad_dir(&dirs::config_dir().expect("Can't detect config dir")));
|
|
||||||
static CACHE_DIR: LazyLock<PathBuf> =
|
static CACHE_DIR: LazyLock<PathBuf> =
|
||||||
LazyLock::new(|| setup_uad_dir(&dirs::cache_dir().expect("Can't detect cache dir")));
|
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");
|
setup_logger().expect("setup logging");
|
||||||
gui::UadGui::start()
|
UadGui::start()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets up logging to a new file in `CACHE_DIR"/uadng.log"`
|
/// 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))
|
.format(make_formatter(false))
|
||||||
.level(default_log_level)
|
.level(default_log_level)
|
||||||
// Rust compiler makes module names use _ instead of -
|
// 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);
|
.chain(log_file);
|
||||||
|
|
||||||
let stdout_dispatcher = fern::Dispatch::new()
|
let stdout_dispatcher = fern::Dispatch::new()
|
||||||
.format(make_formatter(true))
|
.format(make_formatter(true))
|
||||||
.level(default_log_level)
|
.level(default_log_level)
|
||||||
// Rust compiler makes module names use _ instead of -
|
// 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());
|
.chain(std::io::stdout());
|
||||||
|
|
||||||
fern::Dispatch::new()
|
fern::Dispatch::new()
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
clippy::wildcard_imports,
|
clippy::wildcard_imports,
|
||||||
reason = "Iced style modules use PascalCase and &Theme; wildcard for local convenience"
|
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::{
|
use iced::widget::{
|
||||||
button, checkbox, container, overlay, pick_list, radio, scrollable, text, text_editor,
|
button, checkbox, container, overlay, pick_list, radio, scrollable, text, text_editor,
|
||||||
text_input,
|
text_input,
|
||||||
|
|
@ -321,10 +321,12 @@ pub mod Container {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[allow(dead_code, reason = "Used by other themes or future styles")]
|
#[allow(dead_code, reason = "Used by other themes or future styles")]
|
||||||
|
#[must_use]
|
||||||
pub fn Invisible(_: &Theme) -> container::Style {
|
pub fn Invisible(_: &Theme) -> container::Style {
|
||||||
container::Style::default()
|
container::Style::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn Frame(theme: &Theme) -> container::Style {
|
pub fn Frame(theme: &Theme) -> container::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
container::Style {
|
container::Style {
|
||||||
|
|
@ -340,6 +342,7 @@ pub mod Container {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn BorderedFrame(theme: &Theme) -> container::Style {
|
pub fn BorderedFrame(theme: &Theme) -> container::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
container::Style {
|
container::Style {
|
||||||
|
|
@ -356,6 +359,7 @@ pub mod Container {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "Currently unused in some views")]
|
#[allow(dead_code, reason = "Currently unused in some views")]
|
||||||
|
#[must_use]
|
||||||
pub fn Tooltip(theme: &Theme) -> container::Style {
|
pub fn Tooltip(theme: &Theme) -> container::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
container::Style {
|
container::Style {
|
||||||
|
|
@ -371,6 +375,7 @@ pub mod Container {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn Background(theme: &Theme) -> container::Style {
|
pub fn Background(theme: &Theme) -> container::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
container::Style {
|
container::Style {
|
||||||
|
|
@ -408,6 +413,7 @@ pub mod Button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn Primary(theme: &Theme, status: button::Status) -> button::Style {
|
pub fn Primary(theme: &Theme, status: button::Status) -> button::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
let mut style = style_active_hover_disabled(p.bright.primary, p.bright.primary, status);
|
let mut style = style_active_hover_disabled(p.bright.primary, p.bright.primary, status);
|
||||||
|
|
@ -421,10 +427,12 @@ pub mod Button {
|
||||||
dead_code,
|
dead_code,
|
||||||
reason = "Alias kept for semantic clarity in some call-sites"
|
reason = "Alias kept for semantic clarity in some call-sites"
|
||||||
)]
|
)]
|
||||||
|
#[must_use]
|
||||||
pub fn SelfUpdate(theme: &Theme, status: button::Status) -> button::Style {
|
pub fn SelfUpdate(theme: &Theme, status: button::Status) -> button::Style {
|
||||||
Primary(theme, status)
|
Primary(theme, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn RestorePackage(theme: &Theme, status: button::Status) -> button::Style {
|
pub fn RestorePackage(theme: &Theme, status: button::Status) -> button::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
let mut style = style_active_hover_disabled(p.bright.secondary, p.bright.secondary, status);
|
let mut style = style_active_hover_disabled(p.bright.secondary, p.bright.secondary, status);
|
||||||
|
|
@ -441,6 +449,7 @@ pub mod Button {
|
||||||
style
|
style
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn UninstallPackage(theme: &Theme, status: button::Status) -> button::Style {
|
pub fn UninstallPackage(theme: &Theme, status: button::Status) -> button::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
let mut style = style_active_hover_disabled(p.bright.error, p.bright.error, status);
|
let mut style = style_active_hover_disabled(p.bright.error, p.bright.error, status);
|
||||||
|
|
@ -454,10 +463,12 @@ pub mod Button {
|
||||||
dead_code,
|
dead_code,
|
||||||
reason = "Style exposed for disabled state buttons in some contexts"
|
reason = "Style exposed for disabled state buttons in some contexts"
|
||||||
)]
|
)]
|
||||||
|
#[must_use]
|
||||||
pub fn Unavailable(theme: &Theme, status: button::Status) -> button::Style {
|
pub fn Unavailable(theme: &Theme, status: button::Status) -> button::Style {
|
||||||
UninstallPackage(theme, status)
|
UninstallPackage(theme, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn NormalPackage(theme: &Theme, status: button::Status) -> button::Style {
|
pub fn NormalPackage(theme: &Theme, status: button::Status) -> button::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
match status {
|
match status {
|
||||||
|
|
@ -489,6 +500,7 @@ pub mod Button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn SelectedPackage(theme: &Theme, _status: button::Status) -> button::Style {
|
pub fn SelectedPackage(theme: &Theme, _status: button::Status) -> button::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
button::Style {
|
button::Style {
|
||||||
|
|
@ -508,6 +520,7 @@ pub mod Button {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code, reason = "Used in views where buttons must be invisible")]
|
#[allow(dead_code, reason = "Used in views where buttons must be invisible")]
|
||||||
|
#[must_use]
|
||||||
pub fn Hidden(_: &Theme, _: button::Status) -> button::Style {
|
pub fn Hidden(_: &Theme, _: button::Status) -> button::Style {
|
||||||
button::Style {
|
button::Style {
|
||||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
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 {
|
pub fn Description(theme: &Theme, _status: scrollable::Status) -> scrollable::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
let (v, h) = rails(p.normal.surface);
|
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 {
|
pub fn Packages(theme: &Theme, _status: scrollable::Status) -> scrollable::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
let (v, h) = rails(p.base.foreground);
|
let (v, h) = rails(p.base.foreground);
|
||||||
|
|
@ -633,6 +648,7 @@ pub mod Scrollable {
|
||||||
pub mod CheckBox {
|
pub mod CheckBox {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn PackageEnabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
pub fn PackageEnabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
checkbox::Style {
|
checkbox::Style {
|
||||||
|
|
@ -647,6 +663,7 @@ pub mod CheckBox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn PackageDisabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
pub fn PackageDisabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
checkbox::Style {
|
checkbox::Style {
|
||||||
|
|
@ -664,6 +681,7 @@ pub mod CheckBox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn SettingsEnabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
pub fn SettingsEnabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
checkbox::Style {
|
checkbox::Style {
|
||||||
|
|
@ -678,6 +696,7 @@ pub mod CheckBox {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn SettingsDisabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
pub fn SettingsDisabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
checkbox::Style {
|
checkbox::Style {
|
||||||
|
|
@ -696,11 +715,13 @@ pub mod CheckBox {
|
||||||
pub mod Text {
|
pub mod Text {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn Default(theme: &Theme) -> text::Style {
|
pub fn Default(theme: &Theme) -> text::Style {
|
||||||
let _ = theme;
|
let _ = theme;
|
||||||
text::Style::default()
|
text::Style::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn Ok(theme: &Theme) -> text::Style {
|
pub fn Ok(theme: &Theme) -> text::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
text::Style {
|
text::Style {
|
||||||
|
|
@ -708,6 +729,7 @@ pub mod Text {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn Danger(theme: &Theme) -> text::Style {
|
pub fn Danger(theme: &Theme) -> text::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
text::Style {
|
text::Style {
|
||||||
|
|
@ -715,6 +737,7 @@ pub mod Text {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn Commentary(theme: &Theme) -> text::Style {
|
pub fn Commentary(theme: &Theme) -> text::Style {
|
||||||
let p = theme.palette();
|
let p = theme.palette();
|
||||||
text::Style {
|
text::Style {
|
||||||
|
|
@ -738,7 +761,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_palette() {
|
fn test_palette() {
|
||||||
let palette = Theme::default().palette();
|
let palette = Theme::Dark.palette();
|
||||||
|
|
||||||
assert_ne!(palette.base.background, palette.base.foreground);
|
assert_ne!(palette.base.background, palette.base.foreground);
|
||||||
assert_ne!(palette.normal.primary, Color::BLACK);
|
assert_ne!(palette.normal.primary, Color::BLACK);
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use dark_light;
|
|
||||||
use iced::theme::{self, Mode, Palette, Style};
|
use iced::theme::{self, Mode, Palette, Style};
|
||||||
use iced::{Color, color};
|
use iced::{Color, color};
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
@ -151,6 +150,17 @@ 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl theme::Base for Theme {
|
impl theme::Base for Theme {
|
||||||
fn default(preference: Mode) -> Self {
|
fn default(preference: Mode) -> Self {
|
||||||
match preference {
|
match preference {
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
use crate::CACHE_DIR;
|
use crate::helpers::button_primary;
|
||||||
use crate::core::adb;
|
use crate::theme::Theme;
|
||||||
use crate::core::helpers::button_primary;
|
use crate::{gui::UpdateState, style, widgets::text};
|
||||||
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 iced::widget::{Space, column, container, row};
|
use iced::widget::{Space, column, container, row};
|
||||||
use iced::{Alignment, Element, Length, Renderer};
|
use iced::{Alignment, Element, Length, Renderer};
|
||||||
|
use log::error;
|
||||||
use std::path::PathBuf;
|
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")]
|
#[cfg(feature = "self-update")]
|
||||||
use crate::core::update::SelfUpdateStatus;
|
use uad_core::update::SelfUpdateStatus;
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
pub struct About {}
|
pub struct About {}
|
||||||
|
|
@ -1,19 +1,22 @@
|
||||||
use crate::core::config::DeviceSettings;
|
use crate::helpers::button_primary;
|
||||||
use crate::core::helpers::button_primary;
|
use crate::style;
|
||||||
use crate::core::sync::{AdbError, Phone, User, apply_pkg_state_commands, run_adb_action};
|
use crate::theme::Theme;
|
||||||
use crate::core::theme::Theme;
|
use crate::widgets::navigation_menu::ICONS;
|
||||||
use crate::core::uad_lists::{
|
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,
|
Opposite, PackageHashMap, PackageState, Removal, UadList, UadListState, load_debloat_lists,
|
||||||
};
|
};
|
||||||
use crate::core::utils::{EXPORT_FILE_NAME, NAME, export_selection, fetch_packages, open_url};
|
use uad_core::utils::{
|
||||||
use crate::gui::style;
|
EXPORT_FILE_NAME, NAME, export_selection, fetch_packages, matches_search, open_url,
|
||||||
use crate::gui::widgets::navigation_menu::ICONS;
|
};
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use crate::gui::views::settings::Settings;
|
use crate::views::settings::Settings;
|
||||||
use crate::gui::widgets::modal::Modal;
|
use crate::widgets::modal::Modal;
|
||||||
use crate::gui::widgets::package_row::{Message as RowMessage, PackageRow};
|
use crate::widgets::package_row::{Message as RowMessage, PackageRow};
|
||||||
use crate::gui::widgets::text;
|
use crate::widgets::text;
|
||||||
use iced::widget::scrollable::{Direction, Scrollbar};
|
use iced::widget::scrollable::{Direction, Scrollbar};
|
||||||
use iced::widget::{
|
use iced::widget::{
|
||||||
Column, Space, button, checkbox, column, container, pick_list, radio, row, rule, scrollable,
|
Column, Space, button, checkbox, column, container, pick_list, radio, row, rule, scrollable,
|
||||||
|
|
@ -52,8 +55,8 @@ pub struct List {
|
||||||
filtered_packages: Vec<usize>,
|
filtered_packages: Vec<usize>,
|
||||||
/// Vec of `(user_index, pkg_index)`
|
/// Vec of `(user_index, pkg_index)`
|
||||||
selected_packages: Vec<(usize, usize)>,
|
selected_packages: Vec<(usize, usize)>,
|
||||||
selected_package_state: Option<PackageState>,
|
|
||||||
selected_removal: Option<Removal>,
|
selected_removal: Option<Removal>,
|
||||||
|
selected_package_state: Option<PackageState>,
|
||||||
selected_list: Option<UadList>,
|
selected_list: Option<UadList>,
|
||||||
pub selected_user: Option<User>,
|
pub selected_user: Option<User>,
|
||||||
all_selected: bool,
|
all_selected: bool,
|
||||||
|
|
@ -81,8 +84,8 @@ pub enum Message {
|
||||||
ToggleAllSelected(bool),
|
ToggleAllSelected(bool),
|
||||||
ListSelected(UadList),
|
ListSelected(UadList),
|
||||||
UserSelected(User),
|
UserSelected(User),
|
||||||
PackageStateSelected(PackageState),
|
|
||||||
RemovalSelected(Removal),
|
RemovalSelected(Removal),
|
||||||
|
PackageStateSelected(PackageState),
|
||||||
ApplyActionOnSelection,
|
ApplyActionOnSelection,
|
||||||
List(usize, RowMessage),
|
List(usize, RowMessage),
|
||||||
VerifyAndFallback(Result<PackageInfo, AdbError>),
|
VerifyAndFallback(Result<PackageInfo, AdbError>),
|
||||||
|
|
@ -149,8 +152,8 @@ impl List {
|
||||||
}
|
}
|
||||||
Message::SearchInputChanged(letter) => self.on_search_input_changed(letter),
|
Message::SearchInputChanged(letter) => self.on_search_input_changed(letter),
|
||||||
Message::ListSelected(list) => self.on_list_selected(list),
|
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::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::List(i, row_msg) => self.on_list_row(i, &row_msg, settings, selected_device),
|
||||||
Message::ApplyActionOnSelection => self.on_apply_action_on_selection(),
|
Message::ApplyActionOnSelection => self.on_apply_action_on_selection(),
|
||||||
Message::UserSelected(user) => self.on_user_selected(user),
|
Message::UserSelected(user) => self.on_user_selected(user),
|
||||||
|
|
@ -635,7 +638,7 @@ impl List {
|
||||||
.width(120),
|
.width(120),
|
||||||
row![text(
|
row![text(
|
||||||
self.phone_packages[selection.0][selection.1]
|
self.phone_packages[selection.0][selection.1]
|
||||||
.uad_list
|
.list
|
||||||
.to_string()
|
.to_string()
|
||||||
)]
|
)]
|
||||||
.width(55),
|
.width(55),
|
||||||
|
|
@ -752,12 +755,11 @@ impl List {
|
||||||
// that's why `enumerate` is before `filter`.
|
// that's why `enumerate` is before `filter`.
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(_, p)| {
|
.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)
|
&& (package_filter == PackageState::All || p.state == package_filter)
|
||||||
&& (removal_filter == Removal::All || p.removal == removal_filter)
|
&& (removal_filter == Removal::All || p.removal == removal_filter)
|
||||||
&& (self.input_value.is_empty()
|
&& (self.input_value.is_empty()
|
||||||
|| p.name.contains(&self.input_value)
|
|| matches_search(&p.name, &self.input_value, Some(&p.description)))
|
||||||
|| p.description.contains(&self.input_value))
|
|
||||||
})
|
})
|
||||||
.map(|(i, _)| i)
|
.map(|(i, _)| i)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -771,11 +773,21 @@ impl List {
|
||||||
) -> Vec<Vec<PackageRow>> {
|
) -> Vec<Vec<PackageRow>> {
|
||||||
let serial = device_serial.as_ref();
|
let serial = device_serial.as_ref();
|
||||||
if user_list.len() <= 1 {
|
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 {
|
} else {
|
||||||
user_list
|
user_list
|
||||||
.iter()
|
.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()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -877,8 +889,8 @@ impl List {
|
||||||
let i_user = self.selected_user.unwrap_or_default().index;
|
let i_user = self.selected_user.unwrap_or_default().index;
|
||||||
self.phone_packages = packages;
|
self.phone_packages = packages;
|
||||||
self.filtered_packages = (0..self.phone_packages[i_user].len()).collect();
|
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_removal = Some(Removal::Recommended);
|
||||||
|
self.selected_package_state = Some(PackageState::Enabled);
|
||||||
self.selected_list = Some(UadList::All);
|
self.selected_list = Some(UadList::All);
|
||||||
self.selected_user = Some(User::default());
|
self.selected_user = Some(User::default());
|
||||||
self.fallback_notifications.clear();
|
self.fallback_notifications.clear();
|
||||||
|
|
@ -1053,7 +1065,7 @@ impl List {
|
||||||
Task::perform(
|
Task::perform(
|
||||||
async move {
|
async move {
|
||||||
// Blocking ADB calls happen here (off UI thread)
|
// 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,
|
&pkg_name,
|
||||||
device.adb_id.as_str(),
|
device.adb_id.as_str(),
|
||||||
Some(user_id),
|
Some(user_id),
|
||||||
|
|
@ -1062,7 +1074,7 @@ impl List {
|
||||||
match actual_state_opt {
|
match actual_state_opt {
|
||||||
Some(actual_state) if actual_state == wanted_state => {
|
Some(actual_state) if actual_state == wanted_state => {
|
||||||
// Check cross-user behavior
|
// 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,
|
&pkg_name,
|
||||||
device.adb_id.as_str(),
|
device.adb_id.as_str(),
|
||||||
user_id,
|
user_id,
|
||||||
|
|
@ -1091,16 +1103,16 @@ impl List {
|
||||||
// Package doesn't exist (None) or has wrong state - try fallback
|
// Package doesn't exist (None) or has wrong state - try fallback
|
||||||
let actual_state =
|
let actual_state =
|
||||||
actual_state_opt.unwrap_or(PackageState::Uninstalled);
|
actual_state_opt.unwrap_or(PackageState::Uninstalled);
|
||||||
let fallback_result = crate::core::sync::attempt_fallback(
|
// Create CorePackage for fallback
|
||||||
&crate::gui::widgets::package_row::PackageRow::new(
|
let core_package = uad_core::sync::CorePackage {
|
||||||
&pkg_name,
|
name: pkg_name.clone(),
|
||||||
current_state,
|
description: String::new(),
|
||||||
"",
|
removal: Removal::All,
|
||||||
UadList::All,
|
state: current_state,
|
||||||
Removal::All,
|
list: UadList::Unlisted,
|
||||||
false,
|
};
|
||||||
false,
|
let fallback_result = uad_core::sync::attempt_fallback(
|
||||||
),
|
&core_package,
|
||||||
wanted_state,
|
wanted_state,
|
||||||
actual_state,
|
actual_state,
|
||||||
device.user_list[i_user],
|
device.user_list[i_user],
|
||||||
|
|
@ -1195,14 +1207,17 @@ impl List {
|
||||||
// fallback task
|
// fallback task
|
||||||
return Task::perform(
|
return Task::perform(
|
||||||
async move {
|
async move {
|
||||||
let temp_pkg = crate::core::sync::CorePackage {
|
let temp_pkg = uad_core::sync::CorePackage {
|
||||||
name: pkg_name.clone(),
|
name: pkg_name.clone(),
|
||||||
|
description: String::new(),
|
||||||
|
removal: Removal::All,
|
||||||
state: PackageState::Disabled,
|
state: PackageState::Disabled,
|
||||||
|
list: UadList::Unlisted,
|
||||||
};
|
};
|
||||||
|
|
||||||
// get uninstall command
|
// get uninstall command
|
||||||
let uninstall_cmds =
|
let uninstall_cmds =
|
||||||
crate::core::sync::apply_pkg_state_commands(
|
uad_core::sync::apply_pkg_state_commands(
|
||||||
&temp_pkg,
|
&temp_pkg,
|
||||||
PackageState::Uninstalled,
|
PackageState::Uninstalled,
|
||||||
user,
|
user,
|
||||||
|
|
@ -1210,24 +1225,24 @@ impl List {
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(cmd) = uninstall_cmds.first() {
|
if let Some(cmd) = uninstall_cmds.first() {
|
||||||
let dummy_info = PackageInfo::default();
|
let _ = uad_core::sync::run_adb_shell_action(
|
||||||
let _ = crate::core::sync::run_adb_action(
|
&device.adb_id,
|
||||||
device.adb_id.clone(),
|
cmd,
|
||||||
cmd.clone(),
|
);
|
||||||
dummy_info,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// reinstall/enable
|
// reinstall/enable
|
||||||
let temp_pkg_uninstalled =
|
let temp_pkg_uninstalled =
|
||||||
crate::core::sync::CorePackage {
|
uad_core::sync::CorePackage {
|
||||||
name: pkg_name.clone(),
|
name: pkg_name.clone(),
|
||||||
|
description: String::new(),
|
||||||
|
removal: Removal::All,
|
||||||
state: PackageState::Uninstalled,
|
state: PackageState::Uninstalled,
|
||||||
|
list: UadList::Unlisted,
|
||||||
};
|
};
|
||||||
|
|
||||||
let enable_cmds =
|
let enable_cmds =
|
||||||
crate::core::sync::apply_pkg_state_commands(
|
uad_core::sync::apply_pkg_state_commands(
|
||||||
&temp_pkg_uninstalled,
|
&temp_pkg_uninstalled,
|
||||||
PackageState::Enabled,
|
PackageState::Enabled,
|
||||||
user,
|
user,
|
||||||
|
|
@ -1236,18 +1251,15 @@ impl List {
|
||||||
|
|
||||||
// lets enable the new package
|
// lets enable the new package
|
||||||
if let Some(cmd) = enable_cmds.first() {
|
if let Some(cmd) = enable_cmds.first() {
|
||||||
let dummy_info = PackageInfo::default();
|
let _ = uad_core::sync::run_adb_shell_action(
|
||||||
let _ = crate::core::sync::run_adb_action(
|
&device.adb_id,
|
||||||
device.adb_id.clone(),
|
cmd,
|
||||||
cmd.clone(),
|
);
|
||||||
dummy_info,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//verifies the final state for thread confirmation
|
//verifies the final state for thread confirmation
|
||||||
let actual_state =
|
let actual_state =
|
||||||
crate::core::sync::verify_package_state(
|
uad_core::sync::verify_package_state(
|
||||||
&pkg_name,
|
&pkg_name,
|
||||||
&device.adb_id,
|
&device.adb_id,
|
||||||
Some(user.id),
|
Some(user.id),
|
||||||
|
|
@ -1316,8 +1328,13 @@ impl List {
|
||||||
|
|
||||||
fn on_export_selection(&mut self) -> Task<Message> {
|
fn on_export_selection(&mut self) -> Task<Message> {
|
||||||
let i_user = self.selected_user.unwrap_or_default().index;
|
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(
|
Task::perform(
|
||||||
export_selection(self.phone_packages[i_user].clone()),
|
async move { export_selection(&package_names) },
|
||||||
Message::SelectionExported,
|
Message::SelectionExported,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1483,17 +1500,24 @@ fn build_action_pkg_commands(
|
||||||
// Will be filled asynchronously before running the adb action
|
// Will be filled asynchronously before running the adb action
|
||||||
before_cross_user_states: vec![],
|
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
|
// In the end there is only one package state change
|
||||||
// even if we run multiple adb commands
|
// even if we run multiple adb commands
|
||||||
commands.push(Task::perform(
|
commands.push(Task::perform(
|
||||||
run_adb_action_with_before_states(
|
async move {
|
||||||
device.adb_id.clone(),
|
run_adb_action_with_before_states(
|
||||||
action,
|
&device_serial,
|
||||||
p_info,
|
&action,
|
||||||
u_pkg.name.clone(),
|
p_info,
|
||||||
u.id,
|
&package_name,
|
||||||
device.clone(),
|
user_id,
|
||||||
),
|
&phone,
|
||||||
|
)
|
||||||
|
},
|
||||||
if j == 0 {
|
if j == 0 {
|
||||||
Message::VerifyAndFallback
|
Message::VerifyAndFallback
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1505,23 +1529,25 @@ fn build_action_pkg_commands(
|
||||||
commands
|
commands
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_adb_action_with_before_states(
|
fn run_adb_action_with_before_states(
|
||||||
device_serial: String,
|
device_serial: &str,
|
||||||
action: String,
|
action: &str,
|
||||||
mut p_info: PackageInfo,
|
mut p_info: PackageInfo,
|
||||||
package_name: String,
|
package_name: &str,
|
||||||
target_user_id: u16,
|
target_user_id: u16,
|
||||||
phone: Phone,
|
phone: &Phone,
|
||||||
) -> Result<PackageInfo, AdbError> {
|
) -> Result<PackageInfo, AdbError> {
|
||||||
// Capture before-state in background to avoid blocking UI thread
|
// Capture before-state in background to avoid blocking UI thread
|
||||||
let before_states = crate::core::sync::capture_cross_user_states(
|
let before_states = uad_core::sync::capture_cross_user_states(
|
||||||
&package_name,
|
package_name,
|
||||||
&device_serial,
|
device_serial,
|
||||||
target_user_id,
|
target_user_id,
|
||||||
&phone,
|
phone,
|
||||||
);
|
);
|
||||||
p_info.before_cross_user_states = before_states;
|
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> {
|
fn recap<'a>(settings: &Settings, recap: &SummaryEntry) -> Element<'a, Message, Theme, Renderer> {
|
||||||
|
|
@ -1,15 +1,7 @@
|
||||||
use crate::core::{
|
use crate::helpers::{button_primary, open_folder};
|
||||||
config::{BackupSettings, Config, DeviceSettings, GeneralSettings},
|
use crate::theme::Theme;
|
||||||
helpers::button_primary,
|
use crate::theme::string_to_theme;
|
||||||
save::{backup_phone, list_available_backup_user, list_available_backups, restore_backup},
|
use crate::{
|
||||||
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::{
|
|
||||||
style,
|
style,
|
||||||
views::list::{List as AppsView, PackageInfo},
|
views::list::{List as AppsView, PackageInfo},
|
||||||
widgets::modal::Modal,
|
widgets::modal::Modal,
|
||||||
|
|
@ -17,9 +9,20 @@ use crate::gui::{
|
||||||
widgets::package_row::PackageRow,
|
widgets::package_row::PackageRow,
|
||||||
widgets::text,
|
widgets::text,
|
||||||
};
|
};
|
||||||
|
use chrono;
|
||||||
use iced::widget::{Space, button, checkbox, column, container, pick_list, radio, row, scrollable};
|
use iced::widget::{Space, button, checkbox, column, container, pick_list, radio, row, scrollable};
|
||||||
use iced::{Alignment, Element, Length, Renderer, Task, alignment};
|
use iced::{Alignment, Element, Length, Renderer, Task, alignment};
|
||||||
|
use log::{debug, error, info};
|
||||||
use std::path::PathBuf;
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum PopUpModal {
|
pub enum PopUpModal {
|
||||||
|
|
@ -101,10 +104,11 @@ impl Settings {
|
||||||
Task::none()
|
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;
|
self.general.expert_mode = toggled;
|
||||||
debug!("Config change: {self:?}");
|
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()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,22 +116,25 @@ impl Settings {
|
||||||
if phone.android_sdk >= 23 {
|
if phone.android_sdk >= 23 {
|
||||||
self.device.disable_mode = toggled;
|
self.device.disable_mode = toggled;
|
||||||
debug!("Config change: {self:?}");
|
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()
|
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;
|
self.device.multi_user_mode = toggled;
|
||||||
debug!("Config change: {self:?}");
|
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()
|
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();
|
self.general.theme = theme.to_string();
|
||||||
debug!("Config change: {self:?}");
|
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()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,12 +189,16 @@ impl Settings {
|
||||||
phone: &Phone,
|
phone: &Phone,
|
||||||
packages: &[Vec<PackageRow>],
|
packages: &[Vec<PackageRow>],
|
||||||
) -> Task<Message> {
|
) -> 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(
|
Task::perform(
|
||||||
backup_phone(
|
async move { backup_phone(user_list, device_id, &packages_owned) },
|
||||||
phone.user_list.clone(),
|
|
||||||
self.device.device_id.clone(),
|
|
||||||
packages.to_vec(),
|
|
||||||
),
|
|
||||||
Message::DeviceBackedUp,
|
Message::DeviceBackedUp,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -217,7 +228,11 @@ impl Settings {
|
||||||
packages: &[Vec<PackageRow>],
|
packages: &[Vec<PackageRow>],
|
||||||
nb_running_async_adb_commands: &mut u32,
|
nb_running_async_adb_commands: &mut u32,
|
||||||
) -> Task<Message> {
|
) -> 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) => {
|
Ok(restore_result) => {
|
||||||
let mut commands = vec![];
|
let mut commands = vec![];
|
||||||
*nb_running_async_adb_commands = 0;
|
*nb_running_async_adb_commands = 0;
|
||||||
|
|
@ -230,8 +245,13 @@ impl Settings {
|
||||||
};
|
};
|
||||||
for command in p.commands.clone() {
|
for command in p.commands.clone() {
|
||||||
*nb_running_async_adb_commands += 1;
|
*nb_running_async_adb_commands += 1;
|
||||||
|
let p_info_clone = p_info.clone();
|
||||||
|
let phone_id = phone.adb_id.clone();
|
||||||
commands.push(Task::perform(
|
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,
|
Message::RestoringDevice,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
@ -274,7 +294,8 @@ impl Settings {
|
||||||
|
|
||||||
if let Ok(path) = result {
|
if let Ok(path) = result {
|
||||||
self.general.backup_folder = path;
|
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);
|
self.load_device_settings(phone);
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
|
|
@ -293,8 +314,12 @@ impl Settings {
|
||||||
selected_user: Option<User>,
|
selected_user: Option<User>,
|
||||||
packages: &[Vec<PackageRow>],
|
packages: &[Vec<PackageRow>],
|
||||||
) -> Task<Message> {
|
) -> Task<Message> {
|
||||||
|
let core_packages: Vec<Vec<_>> = packages
|
||||||
|
.iter()
|
||||||
|
.map(|user_packages| user_packages.iter().map(CorePackage::from).collect())
|
||||||
|
.collect();
|
||||||
Task::perform(
|
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,
|
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
|
/// Sets the message that will be produces when the background
|
||||||
/// of the [`Modal`] is pressed
|
/// of the [`Modal`] is pressed
|
||||||
|
#[must_use]
|
||||||
pub fn on_blur(self, on_blur: Message) -> Self {
|
pub fn on_blur(self, on_blur: Message) -> Self {
|
||||||
Self {
|
Self {
|
||||||
on_blur: Some(on_blur),
|
on_blur: Some(on_blur),
|
||||||
|
|
@ -1,19 +1,22 @@
|
||||||
use crate::core::helpers::button_primary;
|
use crate::helpers::button_primary;
|
||||||
pub use crate::core::sync::Phone;
|
use crate::theme::Theme;
|
||||||
use crate::core::theme::Theme;
|
pub use crate::views::about::Message as AboutMessage;
|
||||||
use crate::core::update::{SelfUpdateState, SelfUpdateStatus};
|
pub use crate::views::list::{List as AppsView, LoadingState as ListLoadingState};
|
||||||
pub use crate::gui::views::about::Message as AboutMessage;
|
use crate::{gui::Message, style, widgets::text};
|
||||||
pub use crate::gui::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::widget::{Space, button, container, pick_list, row, tooltip};
|
||||||
use iced::{Alignment, Element, Font, Length, Renderer, alignment, font};
|
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`]
|
/// resources/assets/icons.ttf, loaded in [`crate::gui::UadGui`]
|
||||||
pub const ICONS: Font = Font {
|
pub const ICONS: Font = Font {
|
||||||
family: font::Family::Name("icomoon"),
|
family: font::Family::Name("icomoon"),
|
||||||
..Font::DEFAULT
|
..Font::DEFAULT
|
||||||
};
|
};
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(
|
||||||
|
clippy::too_many_lines,
|
||||||
|
reason = "Navigation layout composes many widgets inline"
|
||||||
|
)]
|
||||||
pub fn nav_menu<'a>(
|
pub fn nav_menu<'a>(
|
||||||
device_list: &'a [Phone],
|
device_list: &'a [Phone],
|
||||||
selected_device: Option<Phone>,
|
selected_device: Option<Phone>,
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
use crate::core::sync::Phone;
|
use crate::style;
|
||||||
use crate::core::theme::Theme;
|
use crate::theme::Theme;
|
||||||
use crate::core::uad_lists::{PackageState, Removal, UadList};
|
use crate::views::settings::Settings;
|
||||||
use crate::gui::style;
|
use crate::widgets::text;
|
||||||
use crate::gui::views::settings::Settings;
|
use log::warn;
|
||||||
use crate::gui::widgets::text;
|
use uad_core::sync::{CorePackage, Phone};
|
||||||
|
use uad_core::uad_lists::{PackageState, Removal, UadList};
|
||||||
|
|
||||||
use iced::widget::{Space, button, checkbox, row};
|
use iced::widget::{Space, button, checkbox, row};
|
||||||
use iced::{Alignment, Element, Length, Renderer, Task, alignment};
|
use iced::{Alignment, Element, Length, Renderer, Task, alignment};
|
||||||
|
|
@ -11,10 +12,10 @@ use iced::{Alignment, Element, Length, Renderer, Task, alignment};
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct PackageRow {
|
pub struct PackageRow {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub state: PackageState,
|
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub uad_list: UadList,
|
|
||||||
pub removal: Removal,
|
pub removal: Removal,
|
||||||
|
pub state: PackageState,
|
||||||
|
pub list: UadList,
|
||||||
pub selected: bool,
|
pub selected: bool,
|
||||||
pub current: bool,
|
pub current: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -27,21 +28,22 @@ pub enum Message {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PackageRow {
|
impl PackageRow {
|
||||||
|
#[must_use]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
name: &str,
|
name: &str,
|
||||||
state: PackageState,
|
|
||||||
description: &str,
|
description: &str,
|
||||||
uad_list: UadList,
|
|
||||||
removal: Removal,
|
removal: Removal,
|
||||||
|
state: PackageState,
|
||||||
|
list: UadList,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
current: bool,
|
current: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
state,
|
|
||||||
description: description.to_string(),
|
description: description.to_string(),
|
||||||
uad_list,
|
|
||||||
removal,
|
removal,
|
||||||
|
state,
|
||||||
|
list,
|
||||||
selected,
|
selected,
|
||||||
current,
|
current,
|
||||||
}
|
}
|
||||||
|
|
@ -143,3 +145,56 @@ impl PackageRow {
|
||||||
.into()
|
.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`]"
|
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::advanced::text::IntoFragment;
|
||||||
use iced::widget;
|
use iced::widget;
|
||||||
|
|
||||||
// Creates a new Text widget with advanced shaping.
|
// 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)
|
widget::Text::new(text).shaping(iced::widget::text::Shaping::Advanced)
|
||||||
}
|
}
|
||||||
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1765270179,
|
"lastModified": 1776949667,
|
||||||
"narHash": "sha256-g2a4MhRKu4ymR4xwo+I+auTknXt/+j37Lnf0Mvfl1rE=",
|
"narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "677fbe97984e7af3175b6c121f3c39ee5c8d62c9",
|
"rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,10 @@
|
||||||
with pkgs;
|
with pkgs;
|
||||||
[
|
[
|
||||||
libglvnd
|
libglvnd
|
||||||
xorg.libX11
|
libX11
|
||||||
xorg.libXcursor
|
libXcursor
|
||||||
xorg.libXi
|
libXi
|
||||||
xorg.libXrandr
|
libXrandr
|
||||||
libxkbcommon
|
libxkbcommon
|
||||||
wayland
|
wayland
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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