mirror of
https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation.git
synced 2026-08-09 15:19:11 +02:00
Compare commits
189 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44abeefb71 |
||
|
|
67f880b507 |
||
|
|
14d2f29f0a |
||
|
|
f3df24beb9 |
||
|
|
2379e852ce |
||
|
|
3c4011807b |
||
|
|
9dbdd59b0e |
||
|
|
ef94066d65 |
||
|
|
266f1b9597 |
||
|
|
856606c58b |
||
|
|
9336f25e7d |
||
|
|
3f55e6cdba |
||
|
|
c38c1cd2d0 |
||
|
|
31234bed3c |
||
|
|
067f2251e9 |
||
|
|
43d323332f |
||
|
|
4ec90fa807 |
||
|
|
85c0d67776 |
||
|
|
3bc538292d |
||
|
|
93c83863a6 |
||
|
|
596120a215 |
||
|
|
d98677c7ca |
||
|
|
469ad5f710 |
||
|
|
81291e0655 |
||
|
|
f9e4f141c0 |
||
|
|
1623d39b55 |
||
|
|
258a50cdfe |
||
|
|
ded17cf91d |
||
|
|
b051be8162 |
||
|
|
bd1ed7ea76 | ||
|
|
6bb0155c3a |
||
|
|
4773be1f00 |
||
|
|
7d023fadef |
||
|
|
826b159d03 |
||
|
|
ddd9b16ced |
||
|
|
1f561b8d2d | ||
|
|
02ba6e2cca |
||
|
|
da7482601e |
||
|
|
7dcb7e9315 |
||
|
|
b86c41187c | ||
|
|
1bb1453257 |
||
|
|
649d0d0bcf |
||
|
|
0e5203f2fa |
||
|
|
b11ac0bff9 | ||
|
|
6dea60dfc0 |
||
|
|
6e02459ee2 |
||
|
|
5492d39683 |
||
|
|
fb0dfa8d14 |
||
|
|
96dd6d937e |
||
|
|
2739d9b213 |
||
|
|
8b0e15eb9a |
||
|
|
51a929009c |
||
|
|
9108e3d4b4 |
||
|
|
e0e87e39ea |
||
|
|
01dd13117c |
||
|
|
c16ee8b379 | ||
|
|
b47675e913 |
||
|
|
1731d39a2d | ||
|
|
dc6d383f58 |
||
|
|
a3de7464d4 |
||
|
|
0415bab6c0 |
||
|
|
f788c87daa |
||
|
|
20fcc6e7b6 |
||
|
|
ede2b02501 |
||
|
|
d6a0d0733e |
||
|
|
1d93422e6f |
||
|
|
e7a96d7e0f |
||
|
|
3bc499db42 | ||
|
|
0a76d615ba |
||
|
|
bf3d6ccc59 |
||
|
|
01a37e2d33 | ||
|
|
8e160ff738 |
||
|
|
f85c85a592 |
||
|
|
7eaff8f810 |
||
|
|
acef695f8e |
||
|
|
3e52ff6ba1 |
||
|
|
644b30ae73 |
||
|
|
23cdfafefb |
||
|
|
49250576f3 |
||
|
|
a5d095e140 | ||
|
|
fd94a84a71 |
||
|
|
52f56a42cf |
||
|
|
6a44fda50a | ||
|
|
e2c47a7a2d |
||
|
|
a8ed2ea1ec |
||
|
|
648f5a088f |
||
|
|
a6865c01a5 |
||
|
|
5b52c2dbe6 | ||
|
|
2cac0cb2ca |
||
|
|
1ef7a9f67c |
||
|
|
745f03654c |
||
|
|
06e5dbd2db |
||
|
|
26b470d8a2 |
||
|
|
e23a0c9058 |
||
|
|
f8ccfa686c |
||
|
|
4cec1c0a91 |
||
|
|
8e852266b2 |
||
|
|
ae3c59cf89 |
||
|
|
82adb10fc0 |
||
|
|
32360a04b6 |
||
|
|
afe7deb29f |
||
|
|
37eaee257d |
||
|
|
5a60cfbe53 |
||
|
|
61e1bd29ad |
||
|
|
794fe96910 |
||
|
|
51eb6e0a87 |
||
|
|
0363d93e89 | ||
|
|
21da7c0b27 |
||
|
|
9aa03efdd6 |
||
|
|
d2e81c6629 |
||
|
|
8caff5c7fd |
||
|
|
2caad7db20 |
||
|
|
6047737118 |
||
|
|
c93331e8c8 | ||
|
|
b9216c0120 | ||
|
|
9f0e06774f | ||
|
|
513688c091 | ||
|
|
758a2639a1 |
||
|
|
a4f03d1b43 |
||
|
|
da0fdd238c |
||
|
|
5590b2f809 |
||
|
|
44a041bf74 |
||
|
|
17e5d07a84 |
||
|
|
9a6fbbd610 |
||
|
|
af50b48c68 |
||
|
|
6286b657ef |
||
|
|
6f4a13dbaf |
||
|
|
3e4b8d65e9 |
||
|
|
4fddde5cc9 |
||
|
|
aa280b042b |
||
|
|
718a7d32de |
||
|
|
a5136a6674 |
||
|
|
04c91de237 |
||
|
|
36841b4f12 |
||
|
|
03bcdfbfbf |
||
|
|
79f48b76dc |
||
|
|
5ef4eeac60 |
||
|
|
a1f29f1f71 |
||
|
|
eb3ba6b5a1 |
||
|
|
0084a70da5 | ||
|
|
ebec2292ce |
||
|
|
3db14bff8f |
||
|
|
6e22b26b46 |
||
|
|
0768731425 | ||
|
|
22e5afdfce |
||
|
|
55fafca735 |
||
|
|
e6d44912a7 | ||
|
|
33f15cb6df | ||
|
|
1c2d5ded76 |
||
|
|
8e7f9e2107 |
||
|
|
9befa59723 |
||
|
|
70fd5be601 |
||
|
|
1385d5dfb2 |
||
|
|
351147888c |
||
|
|
f60c8982a7 |
||
|
|
d8441bda83 |
||
|
|
93a71719b0 | ||
|
|
21d71f99fc |
||
|
|
3acd2301e7 |
||
|
|
8b7fe2b98a |
||
|
|
b47f81b641 |
||
|
|
c6b1f44bc9 |
||
|
|
106058826f |
||
|
|
7b6182722c | ||
|
|
6562bddfc1 |
||
|
|
77f5c30410 |
||
|
|
651f424029 |
||
|
|
7a1ca4f4c2 |
||
|
|
ea86289346 |
||
|
|
2f10530701 |
||
|
|
a0813db165 |
||
|
|
540a123e77 |
||
|
|
dfe9921aa0 |
||
|
|
9431c82637 |
||
|
|
0154056d5e | ||
|
|
9f140a4b23 |
||
|
|
e494a45717 | ||
|
|
435c68dca8 |
||
|
|
40e5281ee0 | ||
|
|
d288860ac8 |
||
|
|
ee1068ddf8 |
||
|
|
0a60b713db |
||
|
|
9ba456884c |
||
|
|
00b242368d |
||
|
|
99fb08e9d6 | ||
|
|
f92b806002 | ||
|
|
0f857fb92f |
||
|
|
4eda9c8c1c |
||
|
|
9f5e134d0a |
51 changed files with 5851 additions and 1317 deletions
16
.github/ISSUE_TEMPLATE/3-add-new-packages.yml
vendored
16
.github/ISSUE_TEMPLATE/3-add-new-packages.yml
vendored
|
|
@ -16,12 +16,7 @@ body:
|
|||
|
||||
**List**: `Google`|`Misc`|`OEM` (manufacturer) |`AOSP`|`Pending`|`Carrier` (ISP).
|
||||
|
||||
**Removal**:
|
||||
|
||||
- Recommended: Pointless, outright negative, or apps available through Google Play. Packages with the word "overlay" must be Advanced or of higher priority because they're likely dangerous, meanwhile they may seem useless or empty to your device, they could bootloop someone else's.
|
||||
- Advanced: Breaks obscure or minor parts of functionality, or apps that aren't easily enabled/installed through Settings/Google Play. This category is also used for apps that are useful (default keyboard/gallery/launcher/music app.) but that can easily be replaced by a better alternative.
|
||||
- Expert: Breaks widespread and/or important functionality, but nothing important to the basic operation of the operating system. Removing an Expert package should not bootloop the device (unless mentioned in the description) but we can't guarantee it 100%.
|
||||
- Unsafe: Can break vital parts of the operating system. Removing an Unsafe package have an extremely high risk of bootlooping your device.
|
||||
**Removal**: `Recommended`|`Advanced`|`Expert`|`Unsafe`. See the [Removal Definitions](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Debloat-Lists#removal-definitions) on the wiki for details. **Please ensure you follow these definitions when proposing changes.**
|
||||
|
||||
- type: textarea
|
||||
id: package-listing
|
||||
|
|
@ -36,6 +31,15 @@ body:
|
|||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: removal-definitions
|
||||
attributes:
|
||||
label: Removal Definitions
|
||||
description: Please ensure you follow the removal definitions when proposing changes.
|
||||
options:
|
||||
- label: I've read the [Removal Definitions](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Debloat-Lists#removal-definitions)
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
|
|
|
|||
|
|
@ -16,12 +16,7 @@ body:
|
|||
|
||||
**List**: `Google`|`Misc`|`OEM` (manufacturer) |`AOSP`|`Pending`|`Carrier` (ISP).
|
||||
|
||||
**Removal**:
|
||||
|
||||
- Recommended: Pointless or outright negative packages, and/or apps available through Google Play.
|
||||
- Advanced: Breaks obscure or minor parts of functionality, or apps that aren't easily enabled/installed through Settings/Google Play. This category is also used for apps that are useful (default keyboard/gallery/launcher/music app.) but that can easily be replaced by a better alternative.
|
||||
- Expert: Breaks widespread and/or important functionality, but nothing important to the basic operation of the operating system. Removing an Expert package should not bootloop the device (unless mentioned in the description) but we can't guarantee it 100%.
|
||||
- Unsafe: Can break vital parts of the operating system. Removing an Unsafe package have an extremely high risk of bootlooping your device.
|
||||
**Removal**: `Recommended`|`Advanced`|`Expert`|`Unsafe`. See the [Removal Definitions](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Debloat-Lists#removal-definitions) on the wiki for details. **Please ensure you follow these definitions when proposing changes.**
|
||||
|
||||
- type: textarea
|
||||
id: package-listing
|
||||
|
|
@ -39,6 +34,15 @@ body:
|
|||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: removal-definitions
|
||||
attributes:
|
||||
label: Removal Definitions
|
||||
description: Please ensure you follow the removal definitions when proposing changes.
|
||||
options:
|
||||
- label: I've read the [Removal Definitions](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Debloat-Lists#removal-definitions)
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
|
|
|
|||
31
.github/PULL_REQUEST_TEMPLATE/app.md
vendored
Normal file
31
.github/PULL_REQUEST_TEMPLATE/app.md
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
## Description
|
||||
|
||||
<!-- Please include a summary of your changes and any relevant context. -->
|
||||
|
||||
### Related issues
|
||||
|
||||
<!-- Link to related issues using keywords (e.g. Fixes <issue_number>) -->
|
||||
<!-- Learn more: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests -->
|
||||
|
||||
## How has this been tested?
|
||||
|
||||
<!-- Describe the tests you ran to verify your changes. -->
|
||||
|
||||
- [ ] Unit tests added/updated
|
||||
- [ ] Manual testing performed
|
||||
- [ ] Tested on the following environment(s):
|
||||
- OS:
|
||||
- Desktop Environment:
|
||||
- Window System/Manager:
|
||||
- ADB version:
|
||||
- Rust version:
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I have read the [CONTRIBUTING guidelines](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/blob/main/CONTRIBUTING.md)
|
||||
- [ ] My code follows the project's code style
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have tested the application with these changes
|
||||
- [ ] The CI/CD pipeline passes
|
||||
18
.github/PULL_REQUEST_TEMPLATE/documentation.md
vendored
Normal file
18
.github/PULL_REQUEST_TEMPLATE/documentation.md
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
## Description
|
||||
|
||||
<!-- Please describe the documentation changes you're adding or updating. -->
|
||||
<!-- Link to related issues using keywords (e.g. Fixes <issue_number>) -->
|
||||
<!-- Learn more: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests -->
|
||||
|
||||
## Documentation Changes
|
||||
|
||||
<!-- Briefly describe what documentation was added or modified, and why -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I have read the [CONTRIBUTING guidelines](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/blob/main/CONTRIBUTING.md)
|
||||
- [ ] My changes are clear and easy to understand
|
||||
- [ ] I have checked for grammar and spelling errors
|
||||
- [ ] I have verified all links are correct
|
||||
- [ ] I have tested any code examples provided
|
||||
- [ ] The documentation is properly formatted with markdown
|
||||
21
.github/PULL_REQUEST_TEMPLATE/packages.md
vendored
Normal file
21
.github/PULL_REQUEST_TEMPLATE/packages.md
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
## Description
|
||||
|
||||
<!-- Please describe the package changes you're adding or updating. -->
|
||||
<!-- Link to related issues using keywords (e.g. Fixes <issue_number>) -->
|
||||
<!-- Learn more: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests -->
|
||||
|
||||
## Testing
|
||||
|
||||
- [ ] I have tested these packages on a real device or emulator
|
||||
- [ ] Package names follow correct naming conventions (e.g., `com.example.app`)
|
||||
- [ ] Package names have been verified against device/ADB output
|
||||
- [ ] Descriptions are accurate and helpful
|
||||
- [ ] No duplicate packages added
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
- [ ] I have read the [CONTRIBUTING guidelines](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/blob/main/CONTRIBUTING.md)
|
||||
- [ ] I have followed the package addition guidelines
|
||||
- [ ] I have reviewed the [Removal Definitions](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Debloat-Lists#removal-definitions) in the wiki
|
||||
- [ ] All package information is accurate and tested
|
||||
- [ ] I understand my PR may be modified before merging
|
||||
21
.github/pull_request_template.md
vendored
Normal file
21
.github/pull_request_template.md
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
## Description
|
||||
|
||||
<!-- Please include a clear summary of the changes in this pull request. -->
|
||||
|
||||
## Related issues
|
||||
|
||||
<!-- Link to related issues using keywords (e.g. Fixes <issue_number>) -->
|
||||
<!-- Learn more: https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] I have read the [CONTRIBUTING guidelines](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/blob/main/CONTRIBUTING.md)
|
||||
- [ ] I have performed a self-review of my changes
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] The CI/CD pipeline passes (or is expected to pass)
|
||||
|
||||
<!--
|
||||
Optional: specialized templates are kept in .github/PULL_REQUEST_TEMPLATE/
|
||||
For app/package/documentation-focused PRs, you can use it by including the template in the URL when creating the PR, e.g.
|
||||
https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/pull/new/<branch_name>?template=app.md, https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/pull/new/<branch_name>?template=package.md or https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/pull/new/<branch_name>?template=documentation.md.
|
||||
-->
|
||||
10
.github/release.yml
vendored
10
.github/release.yml
vendored
|
|
@ -9,11 +9,6 @@ changelog:
|
|||
- title: Refactors
|
||||
labels:
|
||||
- refactor
|
||||
- title: Packages/apps
|
||||
labels:
|
||||
- package::documentation
|
||||
- package::addition
|
||||
- package::breakage
|
||||
- title: Development
|
||||
labels:
|
||||
- ci
|
||||
|
|
@ -21,3 +16,8 @@ changelog:
|
|||
- title: Documentation
|
||||
labels:
|
||||
- documentation
|
||||
- title: Packages/apps
|
||||
labels:
|
||||
- package::documentation
|
||||
- package::addition
|
||||
- package::breakage
|
||||
|
|
|
|||
58
.github/workflows/build_artifacts.yml
vendored
58
.github/workflows/build_artifacts.yml
vendored
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
restore-keys: ${{ runner.OS }}-release-
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
- name: Building
|
||||
run: cargo build --release --no-default-features --features wgpu,${{ matrix.update_feature }}
|
||||
run: cargo build --release -p uad-gui --no-default-features --features wgpu,${{ matrix.update_feature }},img
|
||||
- name: Renaming binaries [Windows]
|
||||
if: matrix.os == 'windows-2022'
|
||||
run: mv target/release/uad-ng.exe uad-ng-${{ matrix.build_target }}.exe
|
||||
|
|
@ -68,3 +68,59 @@ jobs:
|
|||
with:
|
||||
name: uad-ng${{ matrix.update_name }}-${{ matrix.build_target }}
|
||||
path: uad-ng-*
|
||||
|
||||
build-cli:
|
||||
name: Building CLI ${{ matrix.build_target }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- build_target: linux
|
||||
os: ubuntu-22.04
|
||||
- build_target: macos
|
||||
os: macos-15
|
||||
- build_target: macos-intel
|
||||
os: macos-15-intel
|
||||
- build_target: windows
|
||||
os: windows-2022
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
target
|
||||
key: ${{ runner.os }}-release-cli-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: ${{ runner.OS }}-release-cli-
|
||||
if: matrix.os == 'ubuntu-22.04'
|
||||
- name: Building
|
||||
run: cargo build --release -p uad-cli
|
||||
- name: Renaming binaries [Windows]
|
||||
if: matrix.os == 'windows-2022'
|
||||
run: mv target/release/uad.exe uad-cli-${{ matrix.build_target }}.exe
|
||||
- name: Renaming binaries [Others]
|
||||
if: matrix.os != 'windows-2022'
|
||||
run: mv target/release/uad uad-cli-${{ matrix.build_target }}
|
||||
- name: Tarball Linux/MacOS binary
|
||||
if: matrix.os != 'windows-2022'
|
||||
run: tar -czf uad-cli-${{ matrix.build_target }}{.tar.gz,}
|
||||
- name: Install coreutils for macOS
|
||||
if: startsWith(matrix.os, 'macos')
|
||||
run: brew install coreutils
|
||||
- name: Create checksums for binaries and archives [Windows]
|
||||
if: matrix.os == 'windows-2022'
|
||||
run: sha256sum uad-cli-${{ matrix.build_target }}.exe | tee uad-cli-${{ matrix.build_target }}.exe-checksum.txt
|
||||
- name: Create checksums for binaries and archives [Others]
|
||||
if: matrix.os != 'windows-2022'
|
||||
run: |
|
||||
sha256sum uad-cli-${{ matrix.build_target }} | tee uad-cli-${{ matrix.build_target }}-checksum
|
||||
sha256sum uad-cli-${{ matrix.build_target }}.tar.gz | tee uad-cli-${{ matrix.build_target }}.tar.gz-checksum
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: uad-cli-${{ matrix.build_target }}
|
||||
path: uad-cli-*
|
||||
|
|
|
|||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
|
@ -25,7 +25,9 @@ jobs:
|
|||
- name: Create release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: uad-ng-*/*
|
||||
files: |
|
||||
uad-ng-*/*
|
||||
uad-cli-*/*
|
||||
generate_release_notes: true
|
||||
body: |
|
||||
> [!NOTE]
|
||||
|
|
|
|||
11
.mailmap
Normal file
11
.mailmap
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
Rudxain <rudxain@localhost.localdomain>
|
||||
Rudxain <76864299+Rudxain@users.noreply.github.com> Ricardo Fernández Serrata <76864299+Rudxain@users.noreply.github.com>
|
||||
Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com> AnonymousWP <50231698+AnonymousWP@users.noreply.github.com>
|
||||
Anonymoussaurus <anonymouswp@oxygenupdater.com>
|
||||
# w1nst0n
|
||||
# 0x192
|
||||
# “Frigyes06”
|
||||
# Frigyes
|
||||
# Frigyes06
|
||||
# Frigyes Erdosi-Szucs
|
||||
# Frigyes Erdosi Szucs
|
||||
919
Cargo.lock
generated
919
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
71
Cargo.toml
71
Cargo.toml
|
|
@ -1,6 +1,12 @@
|
|||
[package]
|
||||
name = "uad-ng"
|
||||
description = "A cross-platform GUI debloater for android devices"
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/uad-core",
|
||||
"crates/uad-gui",
|
||||
"crates/uad-cli"
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "1.2.0"
|
||||
authors = ["Universal-Debloater-Alliance"]
|
||||
license = "GPL-3.0"
|
||||
|
|
@ -11,58 +17,38 @@ keywords = ["debloater", "android", "adb", "privacy", "bloatware"]
|
|||
categories = ["gui"]
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = ["wgpu", "self-update"]
|
||||
wgpu = [] # Iced/wgpu is default
|
||||
self-update = ["flate2", "tar"]
|
||||
no-self-update = []
|
||||
|
||||
[dependencies]
|
||||
dark-light = "2"
|
||||
[workspace.dependencies]
|
||||
# Core dependencies
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = "^1.0"
|
||||
fern = { version = "^0", features = ["colored"] }
|
||||
chrono = { version = "^0.4", default-features = false, features = [
|
||||
"std",
|
||||
"clock",
|
||||
] }
|
||||
log = "^0.4"
|
||||
chrono = { version = "^0.4", default-features = false, features = ["std", "clock"] }
|
||||
toml = "^0"
|
||||
dirs = "^6"
|
||||
ureq = { version = "3", features = ["json"] }
|
||||
retry = "^2.0.0"
|
||||
iced = { version = "=0.14.0", features = ["advanced", "image"] }
|
||||
image = "0.25"
|
||||
rfd = "^0.17.1"
|
||||
csv = "^1.3"
|
||||
|
||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||
flate2 = { version = "^1", optional = true }
|
||||
tar = { version = "^0.4", optional = true }
|
||||
# GUI dependencies
|
||||
iced = { version = "=0.14.0", features = ["advanced"] }
|
||||
image = "0.25"
|
||||
rfd = "^0.17.1"
|
||||
dark-light = "2"
|
||||
fern = { version = "^0", features = ["colored"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
# CLI dependencies
|
||||
clap = { version = "4.6.1", features = ["derive"] }
|
||||
tokio = { version = "1.52.3", features = ["full"] }
|
||||
|
||||
# Platform-specific dependencies
|
||||
flate2 = "^1"
|
||||
tar = "^0.4"
|
||||
win32console = "^0.1.5"
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
opt-level = 2
|
||||
codegen-units = 4
|
||||
|
||||
[profile.opt]
|
||||
inherits = "release"
|
||||
opt-level = "s"
|
||||
codegen-units = 1
|
||||
lto = "fat"
|
||||
strip = true
|
||||
panic = "abort"
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "3"
|
||||
|
||||
[lints.rust]
|
||||
[workspace.lints.rust]
|
||||
deprecated_safe = "warn"
|
||||
|
||||
[lints.clippy]
|
||||
[workspace.lints.clippy]
|
||||
undocumented_unsafe_blocks = "forbid"
|
||||
panic_in_result_fn = "warn"
|
||||
infinite_loop = "warn"
|
||||
|
|
@ -70,9 +56,8 @@ format_push_string = "warn"
|
|||
large_include_file = "warn"
|
||||
shadow_unrelated = "warn"
|
||||
struct_field_names = "allow" # annoying
|
||||
module_name_repetitions = "allow" # annoying
|
||||
|
||||
disallowed_types = "deny"
|
||||
disallowed_methods = "deny"
|
||||
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
10
README.md
10
README.md
|
|
@ -36,6 +36,16 @@ UAD-ng does not collect or transmit any user data. The only external connections
|
|||
|
||||
[<img width=64em src=https://matrix.org/images/matrix-favicon.svg>](https://matrix.to/#/#uad-ng:matrix.org)
|
||||
|
||||
## Friends
|
||||
|
||||
Several projects integrate with or depend on UAD-ng's data and tools:
|
||||
|
||||
- **[Canta](https://github.com/samolego/Canta)** — A mobile Android debloater that uses Shizuku for rootless privilege escalation. It integrates with UAD-ng's Universal Debloat List to provide safe package recommendations without requiring a PC.
|
||||
|
||||
- **Projects by [Muntashir Akon](https://github.com/MuntashirAkon):**
|
||||
- **[AppManager](https://github.com/MuntashirAkon/AppManager)** — An advanced Android app manager with powerful debloating features.
|
||||
- **[android-debloat-list](https://github.com/MuntashirAkon/android-debloat-list)** — A comprehensive, community-driven list of Android packages to debloat, based on UAD-ng's list.
|
||||
|
||||
## Special thanks
|
||||
|
||||
- [@0x192](https://github.com/0x192) who created the original UAD project.
|
||||
|
|
|
|||
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 }
|
||||
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"
|
||||
|
||||
470
crates/uad-cli/src/commands.rs
Normal file
470
crates/uad-cli/src/commands.rs
Normal file
|
|
@ -0,0 +1,470 @@
|
|||
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::{NO_DEVICES_FOUND, 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() {
|
||||
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() {
|
||||
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) => {
|
||||
return Err(format!("{indent}✗ Failed to execute `{cmd}`: {e:?}").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) => Err("Failed to update lists from remote, using cached version".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());
|
||||
}
|
||||
47
crates/uad-cli/src/device.rs
Normal file
47
crates/uad-cli/src/device.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
use uad_core::sync::{Phone, User, get_devices_list};
|
||||
|
||||
pub const NO_DEVICES_FOUND: &str =
|
||||
"No devices found. Make sure ADB is installed and devices are connected.";
|
||||
|
||||
/// 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() {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
251
crates/uad-cli/src/main.rs
Normal file
251
crates/uad-cli/src/main.rs
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
#![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 std::process::ExitCode;
|
||||
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() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(err) => {
|
||||
eprintln!("Error: {err}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> 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 }
|
||||
|
|
@ -38,21 +38,23 @@
|
|||
//! For comprehensive info about ADB,
|
||||
//! [see this](https://android.googlesource.com/platform/packages/modules/adb/+/refs/heads/master/docs/)
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::rc::Rc;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
use crate::core::utils::is_all_w_c;
|
||||
use crate::utils::is_all_w_c;
|
||||
use log::{error, info, warn};
|
||||
|
||||
/// Convert ADB output bytes to a trimmed UTF-8 string.
|
||||
/// Uses lossy conversion to prevent panics on non-UTF8 output from certain OEMs.
|
||||
#[must_use]
|
||||
pub fn to_trimmed_utf8(v: &[u8]) -> String {
|
||||
String::from_utf8_lossy(v).trim_end().to_string()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[cfg(debug_assertions)]
|
||||
#[must_use]
|
||||
fn is_version_triple(s: &str) -> bool {
|
||||
let mut components = s.split('.');
|
||||
for _ in 0..3 {
|
||||
|
|
@ -213,6 +215,12 @@ impl ACommand {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for ACommand {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder object for a command that runs on the device's default `sh` implementation.
|
||||
/// Typically MKSH, but could be Ash.
|
||||
///
|
||||
|
|
@ -267,13 +275,17 @@ pub const fn is_pkg_component(s: &[u8]) -> bool {
|
|||
|
||||
/// String with the invariant of being a valid package-name.
|
||||
/// See [`PackageId::new`] for validation details.
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct PackageId(Box<str>);
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct PackageId(Rc<str>);
|
||||
impl PackageId {
|
||||
/// Creates a package-ID if it's valid according to:
|
||||
/// Creates a package-ID if it's `"android"` or valid according to:
|
||||
/// - <https://developer.android.com/guide/topics/manifest/manifest-element.html#package>
|
||||
/// - <https://developer.android.com/build/configure-app-module#set-application-id>
|
||||
pub fn new(p_id: Box<str>) -> Option<Self> {
|
||||
#[must_use]
|
||||
pub fn new(p_id: &str) -> Option<Self> {
|
||||
if p_id == "android" {
|
||||
return Some(Self(p_id.into()));
|
||||
}
|
||||
let mut components = p_id.split('.');
|
||||
for _ in 0..2 {
|
||||
if !components
|
||||
|
|
@ -284,7 +296,7 @@ impl PackageId {
|
|||
}
|
||||
}
|
||||
if components.all(|comp| is_pkg_component(comp.as_bytes())) {
|
||||
Some(Self(p_id))
|
||||
Some(Self(p_id.into()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
@ -325,13 +337,14 @@ pub const PM_CLEAR_PACK: &str = "pm clear";
|
|||
/// Builder object for an Android Package Manager command.
|
||||
/// <https://developer.android.com/tools/adb#pm>
|
||||
#[derive(Debug)]
|
||||
#[must_use]
|
||||
pub struct PmCommand(ShellCommand);
|
||||
impl PmCommand {
|
||||
/// `list packages -s` sub-command, [`PACK_PREFIX`] stripped.
|
||||
/// `list packages -s` sub-command, [`PACK_PREFIX`] stripped from each element.
|
||||
///
|
||||
/// `Ok` variant:
|
||||
/// - isn't guaranteed to contain valid pack-IDs,
|
||||
/// as "android" can be printed but it's invalid
|
||||
/// - isn't 100% guaranteed to contain valid pack-IDs
|
||||
/// but you can assume it does (except in `unsafe {}` blocks)
|
||||
/// - isn't sorted
|
||||
/// - duplicates never _seem_ to happen, but don't assume uniqueness
|
||||
pub fn list_packages_sys(
|
||||
|
|
@ -353,11 +366,15 @@ impl PmCommand {
|
|||
self.0.0.run().map(|pack_ls| {
|
||||
pack_ls
|
||||
.lines()
|
||||
.map(|p_ln| {
|
||||
.filter_map(|p_ln| {
|
||||
debug_assert!(p_ln.starts_with(PACK_PREFIX));
|
||||
let p = &p_ln[PACK_PREFIX.len()..];
|
||||
debug_assert!(PackageId::new(p.into()).is_some() || p == "android");
|
||||
String::from(p)
|
||||
if PackageId::new(p).is_some() {
|
||||
Some(String::from(p))
|
||||
} else {
|
||||
warn!("skipping nonstandard package name: {p:?}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
|
|
@ -463,7 +480,7 @@ mod tests {
|
|||
"the.🎂.is.a.lie",
|
||||
"EXCLAMATION!!!!",
|
||||
] {
|
||||
assert_eq!(PackageId::new(p_id.into()), None);
|
||||
assert_eq!(PackageId::new(p_id), None);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -480,7 +497,7 @@ mod tests {
|
|||
"com.github.w1nst0n",
|
||||
"this_.String_.is_.not_.real_",
|
||||
] {
|
||||
assert_ne!(PackageId::new(p_id.into()), None);
|
||||
assert_ne!(PackageId::new(p_id), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
use crate::CACHE_DIR;
|
||||
use crate::CONFIG_DIR;
|
||||
use crate::core::utils::DisplayablePath;
|
||||
use crate::core::{sync::User, theme::Theme};
|
||||
use crate::gui::views::settings::Settings;
|
||||
use crate::sync::User;
|
||||
use crate::utils::DisplayablePath;
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
const DEFAULT_THEME: &str = "Auto (follow system theme)";
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
pub general: GeneralSettings,
|
||||
|
|
@ -35,7 +37,7 @@ pub struct BackupSettings {
|
|||
pub struct DeviceSettings {
|
||||
/// Unique serial identifier
|
||||
pub device_id: String,
|
||||
pub disable_mode: bool,
|
||||
pub disable_mode: bool, // should be `enum RemovalMode`
|
||||
pub multi_user_mode: bool,
|
||||
#[serde(skip)]
|
||||
pub backup: BackupSettings,
|
||||
|
|
@ -44,7 +46,7 @@ pub struct DeviceSettings {
|
|||
impl Default for GeneralSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme: Theme::default().to_string(),
|
||||
theme: DEFAULT_THEME.to_string(),
|
||||
expert_mode: false,
|
||||
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"));
|
||||
|
||||
impl Config {
|
||||
pub fn save_changes(settings: &Settings, device_id: &String) {
|
||||
let mut config = Self::load_configuration_file();
|
||||
if let Some(device) = config
|
||||
pub fn save_device_settings(
|
||||
&mut self,
|
||||
device_settings: DeviceSettings,
|
||||
general: GeneralSettings,
|
||||
) {
|
||||
if let Some(device) = self
|
||||
.devices
|
||||
.iter_mut()
|
||||
.find(|x| x.device_id == *device_id)
|
||||
.find(|x| x.device_id == device_settings.device_id)
|
||||
{
|
||||
device.clone_from(&settings.device);
|
||||
*device = device_settings;
|
||||
} else {
|
||||
debug!("config: New device settings saved");
|
||||
config.devices.push(settings.device.clone());
|
||||
self.devices.push(device_settings);
|
||||
}
|
||||
config.general.clone_from(&settings.general);
|
||||
let toml = toml::to_string(&config).unwrap();
|
||||
self.general = general;
|
||||
let toml = toml::to_string(&self).unwrap();
|
||||
fs::write(&*CONFIG_FILE, toml).expect("Could not write config file to disk!");
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn load_configuration_file() -> Self {
|
||||
match fs::read_to_string(&*CONFIG_FILE) {
|
||||
Ok(s) => match toml::from_str(&s) {
|
||||
|
|
@ -111,7 +116,7 @@ mod tests {
|
|||
let config = Config::load_configuration_file();
|
||||
// non-deterministic
|
||||
//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_eq!(config.general.backup_folder, CACHE_DIR.join("backups"));
|
||||
}
|
||||
|
|
@ -133,7 +138,7 @@ mod tests {
|
|||
fn test_default_config() {
|
||||
let config = Config::default();
|
||||
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_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::core::sync::{CorePackage, Phone, User, apply_pkg_state_commands};
|
||||
use crate::core::utils::DisplayablePath;
|
||||
use crate::gui::widgets::package_row::PackageRow;
|
||||
use crate::config::{Config, DeviceSettings};
|
||||
use crate::sync::{CorePackage, Phone, User, apply_pkg_state_commands};
|
||||
use crate::utils::DisplayablePath;
|
||||
use log::{error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
|
|
@ -22,10 +21,10 @@ pub struct UserBackup {
|
|||
}
|
||||
|
||||
/// Backup all `Uninstalled` and `Disabled` packages
|
||||
pub async fn backup_phone(
|
||||
pub fn backup_phone(
|
||||
users: Vec<User>,
|
||||
device_id: String,
|
||||
phone_packages: Vec<Vec<PackageRow>>,
|
||||
phone_packages: &[Vec<CorePackage>],
|
||||
) -> Result<bool, String> {
|
||||
let mut backup = PhoneBackup {
|
||||
device_id: device_id.clone(),
|
||||
|
|
@ -38,11 +37,8 @@ pub async fn backup_phone(
|
|||
..UserBackup::default()
|
||||
};
|
||||
|
||||
for p in phone_packages[u.index].clone() {
|
||||
user_backup.packages.push(CorePackage {
|
||||
name: p.name.clone(),
|
||||
state: p.state,
|
||||
});
|
||||
for p in phone_packages[u.index].iter().cloned() {
|
||||
user_backup.packages.push(p);
|
||||
}
|
||||
backup.users.push(user_backup);
|
||||
}
|
||||
|
|
@ -79,6 +75,7 @@ pub fn list_available_backups(dir: &Path) -> Vec<DisplayablePath> {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn list_available_backup_user(backup: DisplayablePath) -> Vec<User> {
|
||||
match fs::read_to_string(backup.path) {
|
||||
Ok(data) => serde_json::from_str::<PhoneBackup>(&data)
|
||||
|
|
@ -113,7 +110,7 @@ pub struct RestoreResult {
|
|||
|
||||
pub fn restore_backup(
|
||||
selected_device: &Phone,
|
||||
packages: &[Vec<PackageRow>],
|
||||
packages: &[Vec<CorePackage>],
|
||||
settings: &DeviceSettings,
|
||||
) -> Result<RestoreResult, String> {
|
||||
match fs::read_to_string(
|
||||
|
|
@ -142,7 +139,7 @@ pub fn restore_backup(
|
|||
.iter()
|
||||
.find(|x| x.name == backup_package.name)
|
||||
{
|
||||
p.into()
|
||||
p.clone()
|
||||
} else {
|
||||
skipped_packages += 1;
|
||||
warn!(
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
use crate::core::{
|
||||
adb::{ACommand as AdbCommand, PM_CLEAR_PACK},
|
||||
use crate::{
|
||||
adb::{ACommand as AdbCommand, PM_CLEAR_PACK, PackageId},
|
||||
uad_lists::PackageState,
|
||||
};
|
||||
use crate::gui::{views::list::PackageInfo, widgets::package_row::PackageRow};
|
||||
use log::{error, info};
|
||||
use retry::{OperationResult, delay::Fixed, retry};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
|
@ -61,27 +61,25 @@ pub enum AdbError {
|
|||
/// This replaces the deprecated `adb_shell_command`.
|
||||
///
|
||||
/// If `serial` is empty, it lets ADB choose the default device.
|
||||
pub async fn run_adb_action<S: AsRef<str>>(
|
||||
pub fn run_adb_shell_action<S: AsRef<str>>(
|
||||
device_serial: S,
|
||||
action: String,
|
||||
p: PackageInfo,
|
||||
) -> Result<PackageInfo, AdbError> {
|
||||
action: &str,
|
||||
) -> Result<String, AdbError> {
|
||||
let serial = device_serial.as_ref();
|
||||
let label = &p.removal;
|
||||
|
||||
match AdbCommand::new().shell(serial).raw(&action) {
|
||||
match AdbCommand::new().shell(serial).raw(action) {
|
||||
Ok(o) => {
|
||||
if ["Error", "Failure"].iter().any(|&e| o.contains(e)) {
|
||||
let friendly_msg = make_friendly_error_message(&o, &action);
|
||||
return Err(AdbError::Generic(format!("[{label}] {friendly_msg}")));
|
||||
let friendly_msg = make_friendly_error_message(&o, action);
|
||||
return Err(AdbError::Generic(friendly_msg));
|
||||
}
|
||||
info!("[{label}] {action} -> {o}");
|
||||
Ok(p)
|
||||
info!("{action} -> {o}");
|
||||
Ok(o)
|
||||
}
|
||||
Err(err) => {
|
||||
if !err.contains("[not installed for") {
|
||||
let friendly_msg = make_friendly_error_message(&err, &action);
|
||||
return Err(AdbError::Generic(format!("[{label}] {friendly_msg}")));
|
||||
let friendly_msg = make_friendly_error_message(&err, action);
|
||||
return Err(AdbError::Generic(friendly_msg));
|
||||
}
|
||||
Err(AdbError::Generic(err))
|
||||
}
|
||||
|
|
@ -141,6 +139,7 @@ fn make_friendly_error_message(error_output: &str, action: &str) -> String {
|
|||
}
|
||||
|
||||
/// If `None`, returns an empty String, not " --user 0"
|
||||
#[must_use]
|
||||
pub fn user_flag(user_id: Option<User>) -> String {
|
||||
user_id
|
||||
.map(|user| format!(" --user {}", user.id))
|
||||
|
|
@ -151,35 +150,16 @@ pub fn user_flag(user_id: Option<User>) -> String {
|
|||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct CorePackage {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub removal: crate::uad_lists::Removal,
|
||||
#[serde(default)]
|
||||
pub state: PackageState,
|
||||
#[serde(default)]
|
||||
pub list: crate::uad_lists::UadList,
|
||||
}
|
||||
|
||||
impl From<&mut PackageRow> for CorePackage {
|
||||
fn from(pr: &mut PackageRow) -> Self {
|
||||
Self {
|
||||
name: pr.name.clone(),
|
||||
state: pr.state,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<PackageRow> for CorePackage {
|
||||
fn from(pr: PackageRow) -> Self {
|
||||
Self {
|
||||
name: pr.name.clone(),
|
||||
state: pr.state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PackageRow> for CorePackage {
|
||||
fn from(pr: &PackageRow) -> Self {
|
||||
Self {
|
||||
name: pr.name.clone(),
|
||||
state: pr.state,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn apply_pkg_state_commands(
|
||||
package: &CorePackage,
|
||||
wanted_state: PackageState,
|
||||
|
|
@ -200,16 +180,19 @@ pub fn apply_pkg_state_commands(
|
|||
_ => vec![],
|
||||
},
|
||||
PackageState::Disabled => match package.state {
|
||||
PackageState::Uninstalled | PackageState::Enabled => match phone.android_sdk {
|
||||
sdk if sdk >= 23 => vec!["pm disable-user", "am force-stop", PM_CLEAR_PACK],
|
||||
_ => vec![],
|
||||
},
|
||||
PackageState::Uninstalled | PackageState::Enabled => {
|
||||
if supports_disabling(phone) {
|
||||
vec!["pm disable-user", "am force-stop", PM_CLEAR_PACK]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
_ => vec![],
|
||||
},
|
||||
PackageState::Uninstalled => match package.state {
|
||||
PackageState::Enabled | PackageState::Disabled => match phone.android_sdk {
|
||||
sdk if sdk >= 23 => vec!["pm uninstall"], // > Android Marshmallow (6.0)
|
||||
21 | 22 => vec!["pm hide", PM_CLEAR_PACK], // Android Lollipop (5.x)
|
||||
sdk if sdk >= 23 => vec!["pm uninstall"], // > Marshmallow (6.0)
|
||||
21 | 22 => vec!["pm hide", PM_CLEAR_PACK], // Lollipop (5.x)
|
||||
_ => vec!["pm block", PM_CLEAR_PACK], // Disable mode is unavailable on older devices because the specific ADB commands need root
|
||||
},
|
||||
_ => vec![],
|
||||
|
|
@ -224,7 +207,19 @@ pub fn apply_pkg_state_commands(
|
|||
/// Build a command request to be sent via ADB to a device.
|
||||
/// `commands` accepts one or more ADB shell commands
|
||||
/// which act on a common `package` and `user`.
|
||||
#[must_use]
|
||||
pub fn request_builder(commands: &[&str], package: &str, user: Option<User>) -> Vec<String> {
|
||||
// Defense-in-depth: `package` is interpolated verbatim into a device-shell
|
||||
// action, so refuse any invalid name/ID. Every current caller already
|
||||
// reconciles the name against the live device package list before reaching
|
||||
// here, so this rejects nothing legitimate — it just keeps the no-injection
|
||||
// guarantee local to the sink instead of relying on each caller to sanitise.
|
||||
// Fail closed: emit no command for a malformed name rather than an injectable
|
||||
// device-shell string.
|
||||
if PackageId::new(package).is_none() {
|
||||
error!("request_builder: refusing invalid package name: {package:?}");
|
||||
return Vec::new();
|
||||
}
|
||||
let maybe_user_flag = user_flag(user);
|
||||
commands
|
||||
.iter()
|
||||
|
|
@ -235,6 +230,7 @@ pub fn request_builder(commands: &[&str], package: &str, user: Option<User>) ->
|
|||
/// Get the model by querying the `ro.product.model` property.
|
||||
///
|
||||
/// If `serial` is empty, it lets ADB choose the default device.
|
||||
#[must_use]
|
||||
pub fn get_device_model(serial: &str) -> String {
|
||||
AdbCommand::new()
|
||||
.shell(serial)
|
||||
|
|
@ -253,6 +249,7 @@ pub fn get_device_model(serial: &str) -> String {
|
|||
/// Get the brand by querying the `ro.product.brand` property.
|
||||
///
|
||||
/// If `serial` is empty, it lets ADB choose the default device.
|
||||
#[must_use]
|
||||
pub fn get_device_brand(serial: &str) -> String {
|
||||
AdbCommand::new()
|
||||
.shell(serial)
|
||||
|
|
@ -266,6 +263,7 @@ pub fn get_device_brand(serial: &str) -> String {
|
|||
// `ro.build.version.sdk` property or defaulting to 0.
|
||||
///
|
||||
/// If `device_serial` is empty, it lets ADB choose the default device.
|
||||
#[must_use]
|
||||
pub fn get_android_sdk(device_serial: &str) -> u8 {
|
||||
AdbCommand::new()
|
||||
.shell(device_serial)
|
||||
|
|
@ -280,6 +278,7 @@ pub fn get_android_sdk(device_serial: &str) -> u8 {
|
|||
///
|
||||
/// Only includes users where the package exists (Some state). Users where the package
|
||||
/// doesn't exist (None) are not tracked.
|
||||
#[must_use]
|
||||
pub fn capture_cross_user_states(
|
||||
package_name: &str,
|
||||
device_serial: &str,
|
||||
|
|
@ -299,8 +298,9 @@ pub fn capture_cross_user_states(
|
|||
/// Detect cross-user behavior and return appropriate notification message.
|
||||
/// This handles unexpected cross-user behavior:
|
||||
/// - 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)
|
||||
#[must_use]
|
||||
pub fn detect_cross_user_behavior(
|
||||
package_name: &str,
|
||||
device_serial: &str,
|
||||
|
|
@ -404,10 +404,11 @@ pub fn detect_cross_user_behavior(
|
|||
}
|
||||
}
|
||||
|
||||
/// Minimum inclusive Android SDK version
|
||||
/// that supports multi-user mode.
|
||||
/// Lollipop 5.0
|
||||
pub const MULTI_USER_SDK: u8 = 21;
|
||||
#[must_use]
|
||||
pub const fn supports_disabling(dev: &Phone) -> bool {
|
||||
// >= Marshmallow (6.0)
|
||||
dev.android_sdk >= 23
|
||||
}
|
||||
|
||||
/// Check if it might support multi-user mode,
|
||||
/// by simply comparing SDK version.
|
||||
|
|
@ -418,7 +419,8 @@ pub const MULTI_USER_SDK: u8 = 21;
|
|||
/// - <https://developer.android.com/reference/android/os/UserManager#supportsMultipleUsers()>
|
||||
#[must_use]
|
||||
pub const fn supports_multi_user(dev: &Phone) -> bool {
|
||||
dev.android_sdk >= MULTI_USER_SDK
|
||||
// >= Lollipop (5.0)
|
||||
dev.android_sdk >= 21
|
||||
}
|
||||
|
||||
/// Check if a `user_id` is protected on a device by trying
|
||||
|
|
@ -433,6 +435,7 @@ pub fn is_protected_user<S: AsRef<str>>(user_id: u16, device_serial: S) -> bool
|
|||
.is_err()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn list_users_idx_prot(device_serial: &str) -> Vec<User> {
|
||||
AdbCommand::new()
|
||||
.shell(device_serial)
|
||||
|
|
@ -456,9 +459,10 @@ pub fn list_users_idx_prot(device_serial: &str) -> Vec<User> {
|
|||
|
||||
/// This matches serials (`getprop ro.serialno`)
|
||||
/// that are authorized by the user.
|
||||
pub async fn get_devices_list() -> Vec<Phone> {
|
||||
#[must_use]
|
||||
pub fn get_devices_list() -> Vec<Phone> {
|
||||
retry(
|
||||
Fixed::from_millis(500).take(if cfg!(debug_assertions) { 3 } else { 120 }),
|
||||
Fixed::from_millis(500).take(if cfg!(debug_assertions) { 3 } else { 10 }),
|
||||
|| match AdbCommand::new().devices() {
|
||||
Ok(devices) => {
|
||||
let mut device_list: Vec<Phone> = vec![];
|
||||
|
|
@ -486,20 +490,22 @@ pub async fn get_devices_list() -> Vec<Phone> {
|
|||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub async fn initial_load() -> bool {
|
||||
#[must_use]
|
||||
pub fn initial_load() -> bool {
|
||||
match AdbCommand::new().devices() {
|
||||
Ok(_devices) => true,
|
||||
Err(_err) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify the actual state of a package on the device
|
||||
pub fn verify_package_state(
|
||||
package_name: &str,
|
||||
/// Get the current state of a package on a device
|
||||
#[must_use]
|
||||
pub fn get_package_state(
|
||||
device_serial: &str,
|
||||
package_name: &str,
|
||||
user_id: Option<u16>,
|
||||
) -> Option<PackageState> {
|
||||
use crate::core::adb::{ACommand as AdbCommand, PmListPacksFlag};
|
||||
use crate::adb::{ACommand as AdbCommand, PmListPacksFlag};
|
||||
|
||||
// Check if package is enabled
|
||||
if let Ok(enabled_packages) = AdbCommand::new()
|
||||
|
|
@ -535,11 +541,23 @@ pub fn verify_package_state(
|
|||
None
|
||||
}
|
||||
|
||||
/// Verify the current state of a package on a device.
|
||||
/// Returns the package state, or `None` if the package doesn't exist.
|
||||
#[must_use]
|
||||
pub fn verify_package_state(
|
||||
package_name: &str,
|
||||
device_serial: &str,
|
||||
user_id: Option<u16>,
|
||||
) -> Option<PackageState> {
|
||||
get_package_state(device_serial, package_name, user_id)
|
||||
}
|
||||
|
||||
/// Check if a package exists on any other users besides the target user.
|
||||
/// This helps detect OEM-specific cross-user restoration behavior.
|
||||
///
|
||||
/// Only includes users where the package exists in a non-uninstalled state
|
||||
/// (i.e., Enabled or Disabled).
|
||||
#[must_use]
|
||||
pub fn check_cross_user_package_existence(
|
||||
package_name: &str,
|
||||
device_serial: &str,
|
||||
|
|
@ -561,9 +579,29 @@ pub fn check_cross_user_package_existence(
|
|||
other_user_states
|
||||
}
|
||||
|
||||
/// Creates a `CorePackage` with the specified state, preserving other fields from the original.
|
||||
fn package_with_state(package: &CorePackage, state: PackageState) -> CorePackage {
|
||||
CorePackage {
|
||||
state,
|
||||
..package.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes the first available command and returns the result.
|
||||
fn execute_first_command(commands: &[String], phone: &Phone) -> Result<(), String> {
|
||||
if commands.is_empty() {
|
||||
return Err("No command available".to_string());
|
||||
}
|
||||
AdbCommand::new()
|
||||
.shell(&phone.adb_id)
|
||||
.raw(&commands[0])
|
||||
.map(|_| ())
|
||||
.map_err(|err| err.clone())
|
||||
}
|
||||
|
||||
/// Attempt fallback action when package state verification fails
|
||||
pub fn attempt_fallback(
|
||||
package: &crate::gui::widgets::package_row::PackageRow,
|
||||
package: &CorePackage,
|
||||
wanted_state: PackageState,
|
||||
actual_state: PackageState,
|
||||
user: User,
|
||||
|
|
@ -572,101 +610,49 @@ pub fn attempt_fallback(
|
|||
match (wanted_state, actual_state) {
|
||||
// Case 1: Tried to uninstall but package was reinstalled -> disable it
|
||||
(PackageState::Uninstalled, PackageState::Enabled) => {
|
||||
let core_package = CorePackage {
|
||||
name: package.name.clone(),
|
||||
state: PackageState::Enabled,
|
||||
};
|
||||
let commands =
|
||||
apply_pkg_state_commands(&core_package, PackageState::Disabled, user, phone);
|
||||
|
||||
if commands.is_empty() {
|
||||
Err("No disable command available for this Android version".to_string())
|
||||
} else {
|
||||
// Execute the disable command
|
||||
let action = commands[0].clone();
|
||||
match AdbCommand::new().shell(&phone.adb_id).raw(&action) {
|
||||
Ok(_) => Ok("disabled package instead of uninstalling".to_string()),
|
||||
Err(err) => Err(format!("Failed to disable package: {err}")),
|
||||
}
|
||||
}
|
||||
let pkg = package_with_state(package, PackageState::Enabled);
|
||||
let commands = apply_pkg_state_commands(&pkg, PackageState::Disabled, user, phone);
|
||||
execute_first_command(&commands, phone)
|
||||
.map(|()| "disabled package instead of uninstalling".to_string())
|
||||
.map_err(|e| format!("Failed to disable package: {e}"))
|
||||
}
|
||||
|
||||
// Case 2: Tried to disable but package re-enabled itself -> try uninstall
|
||||
(PackageState::Disabled, PackageState::Enabled) => {
|
||||
let core_package = CorePackage {
|
||||
name: package.name.clone(),
|
||||
state: PackageState::Enabled,
|
||||
};
|
||||
let commands =
|
||||
apply_pkg_state_commands(&core_package, PackageState::Uninstalled, user, phone);
|
||||
let pkg = package_with_state(package, PackageState::Enabled);
|
||||
let commands = apply_pkg_state_commands(&pkg, PackageState::Uninstalled, user, phone);
|
||||
execute_first_command(&commands, phone)
|
||||
.map_err(|e| format!("Failed to uninstall: {e}"))?;
|
||||
|
||||
if commands.is_empty() {
|
||||
Err("No uninstall command available for this Android version".to_string())
|
||||
} else {
|
||||
// Execute the uninstall command
|
||||
let action = commands[0].clone();
|
||||
match AdbCommand::new().shell(&phone.adb_id).raw(&action) {
|
||||
Ok(_) => {
|
||||
// Verify the package was actually uninstalled
|
||||
match verify_package_state(&package.name, &phone.adb_id, Some(user.id)) {
|
||||
Some(PackageState::Uninstalled) | None => {
|
||||
Ok("uninstalled package instead of disabling".to_string())
|
||||
}
|
||||
_ => Err("Package still exists after uninstall attempt".to_string()),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(format!("Failed to uninstall package: {err}")),
|
||||
// Verify the package was actually uninstalled
|
||||
match verify_package_state(&package.name, &phone.adb_id, Some(user.id)) {
|
||||
Some(PackageState::Uninstalled) | None => {
|
||||
Ok("uninstalled package instead of disabling".to_string())
|
||||
}
|
||||
_ => Err("Package still exists after uninstall attempt".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// Case 3: Tried to enable but package was disabled -> try uninstall then reinstall
|
||||
(PackageState::Enabled, PackageState::Disabled) => {
|
||||
// First try to uninstall
|
||||
let core_package = CorePackage {
|
||||
name: package.name.clone(),
|
||||
state: PackageState::Disabled,
|
||||
};
|
||||
let uninstall_commands =
|
||||
apply_pkg_state_commands(&core_package, PackageState::Uninstalled, user, phone);
|
||||
let pkg = package_with_state(package, PackageState::Disabled);
|
||||
let uninstall_cmds =
|
||||
apply_pkg_state_commands(&pkg, PackageState::Uninstalled, user, phone);
|
||||
execute_first_command(&uninstall_cmds, phone)
|
||||
.map_err(|e| format!("Failed to uninstall for reinstall: {e}"))?;
|
||||
|
||||
if uninstall_commands.is_empty() {
|
||||
Err("No uninstall command available for reinstall attempt".to_string())
|
||||
} else {
|
||||
let uninstall_action = uninstall_commands[0].clone();
|
||||
match AdbCommand::new()
|
||||
.shell(&phone.adb_id)
|
||||
.raw(&uninstall_action)
|
||||
{
|
||||
Ok(_) => {
|
||||
// Now try to reinstall/enable
|
||||
let core_package_uninstalled = CorePackage {
|
||||
name: package.name.clone(),
|
||||
state: PackageState::Uninstalled,
|
||||
};
|
||||
let enable_commands = apply_pkg_state_commands(
|
||||
&core_package_uninstalled,
|
||||
PackageState::Enabled,
|
||||
user,
|
||||
phone,
|
||||
);
|
||||
// Now try to reinstall/enable
|
||||
let pkg_uninstalled = package_with_state(package, PackageState::Uninstalled);
|
||||
let enable_cmds =
|
||||
apply_pkg_state_commands(&pkg_uninstalled, PackageState::Enabled, user, phone);
|
||||
|
||||
if enable_commands.is_empty() {
|
||||
Ok("uninstalled package but couldn't reinstall".to_string())
|
||||
} else {
|
||||
let enable_action = enable_commands[0].clone();
|
||||
match AdbCommand::new().shell(&phone.adb_id).raw(&enable_action) {
|
||||
Ok(_) => {
|
||||
Ok("uninstalled and reinstalled package to enable it"
|
||||
.to_string())
|
||||
}
|
||||
Err(err) => Err(format!("Failed to reinstall package: {err}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => Err(format!("Failed to uninstall package for reinstall: {err}")),
|
||||
}
|
||||
if enable_cmds.is_empty() {
|
||||
return Ok("uninstalled package but couldn't reinstall".to_string());
|
||||
}
|
||||
|
||||
execute_first_command(&enable_cmds, phone)
|
||||
.map(|()| "uninstalled and reinstalled package to enable it".to_string())
|
||||
.map_err(|e| format!("Failed to reinstall package: {e}"))
|
||||
}
|
||||
|
||||
// Other cases - no fallback available
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::CACHE_DIR;
|
||||
use crate::core::utils::{format_diff_time_from_now, last_modified_date};
|
||||
use crate::utils::{format_diff_time_from_now, last_modified_date};
|
||||
use log::warn;
|
||||
use retry::{OperationResult, delay::Fixed, retry};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json;
|
||||
|
|
@ -15,7 +16,7 @@ pub const LIST_FNAME: &str = "uad_lists.json";
|
|||
reason = "https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/discussions/608"
|
||||
)]
|
||||
// not `const`, because it's too big
|
||||
pub static DATA: &str = include_str!("../../resources/assets/uad_lists.json");
|
||||
pub static DATA: &str = include_str!("../../../resources/assets/uad_lists.json");
|
||||
|
||||
#[derive(Deserialize, Debug, Clone, PartialEq, Hash, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
@ -28,7 +29,7 @@ pub struct Package {
|
|||
pub removal: Removal,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Default, Deserialize, Serialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum UadList {
|
||||
#[default]
|
||||
All,
|
||||
|
|
@ -73,6 +74,7 @@ impl UadList {
|
|||
Self::Unlisted,
|
||||
];
|
||||
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::All => "All lists",
|
||||
|
|
@ -148,7 +150,7 @@ impl Opposite for PackageState {
|
|||
}
|
||||
|
||||
// Bad names. To be changed!
|
||||
#[derive(Default, Debug, Deserialize, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Default, Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Removal {
|
||||
#[default]
|
||||
Recommended,
|
||||
|
|
@ -176,6 +178,7 @@ impl Removal {
|
|||
Self::Unlisted,
|
||||
];
|
||||
|
||||
#[must_use]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::All => "All removals",
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
use crate::core::utils::NAME;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
use {
|
||||
crate::utils::NAME,
|
||||
log::{debug, error},
|
||||
retry::{OperationResult, delay::Fibonacci, retry},
|
||||
std::fs,
|
||||
std::io,
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
use crate::core::{
|
||||
use crate::{
|
||||
adb::{ACommand as AdbCommand, PmListPacksFlag},
|
||||
sync::User,
|
||||
theme::Theme,
|
||||
sync::{CorePackage, User},
|
||||
uad_lists::{PackageHashMap, PackageState, Removal, UadList},
|
||||
};
|
||||
use crate::gui::widgets::package_row::PackageRow;
|
||||
use chrono::{DateTime, offset::Utc};
|
||||
use csv::Writer;
|
||||
use log::error;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt, fs,
|
||||
|
|
@ -66,11 +65,12 @@ pub enum Error {
|
|||
DialogClosed,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn fetch_packages(
|
||||
uad_lists: &PackageHashMap,
|
||||
device_serial: &str,
|
||||
user_id: Option<u16>,
|
||||
) -> Vec<PackageRow> {
|
||||
) -> Vec<CorePackage> {
|
||||
let all_sys_packs = AdbCommand::new()
|
||||
.shell(device_serial)
|
||||
.pm()
|
||||
|
|
@ -92,24 +92,24 @@ pub fn fetch_packages(
|
|||
.collect();
|
||||
|
||||
let mut description;
|
||||
let mut uad_list;
|
||||
let mut state;
|
||||
let mut removal;
|
||||
let mut user_package: Vec<PackageRow> = Vec::new();
|
||||
let mut state;
|
||||
let mut list;
|
||||
let mut user_package: Vec<CorePackage> = Vec::new();
|
||||
|
||||
for pack_name in all_sys_packs {
|
||||
let p_name = &pack_name;
|
||||
state = PackageState::Uninstalled;
|
||||
description = "[No description]: CONTRIBUTION WELCOMED";
|
||||
uad_list = UadList::Unlisted;
|
||||
description = String::from("[No description]: CONTRIBUTION WELCOMED");
|
||||
removal = Removal::Unlisted;
|
||||
state = PackageState::Uninstalled;
|
||||
list = UadList::Unlisted;
|
||||
|
||||
if let Some(package) = uad_lists.get(p_name) {
|
||||
if !package.description.is_empty() {
|
||||
description = &package.description;
|
||||
description = package.description.clone();
|
||||
}
|
||||
uad_list = package.list;
|
||||
removal = package.removal;
|
||||
list = package.list;
|
||||
}
|
||||
|
||||
if enabled_sys_packs.contains(p_name) {
|
||||
|
|
@ -118,25 +118,20 @@ pub fn fetch_packages(
|
|||
state = PackageState::Disabled;
|
||||
}
|
||||
|
||||
let package_row =
|
||||
PackageRow::new(p_name, state, description, uad_list, removal, false, false);
|
||||
user_package.push(package_row);
|
||||
let package = CorePackage {
|
||||
name: p_name.clone(),
|
||||
description,
|
||||
removal,
|
||||
state,
|
||||
list,
|
||||
};
|
||||
user_package.push(package);
|
||||
}
|
||||
user_package.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||
user_package.sort_by_key(|package| package.name.to_lowercase());
|
||||
user_package
|
||||
}
|
||||
|
||||
pub fn string_to_theme(theme: &str) -> Theme {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn setup_uad_dir(dir: &Path) -> PathBuf {
|
||||
let dir = dir.join("uad");
|
||||
if let Err(e) = fs::create_dir_all(&dir) {
|
||||
|
|
@ -173,6 +168,7 @@ pub fn open_url(dir: PathBuf) {
|
|||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[must_use]
|
||||
pub fn last_modified_date(file: PathBuf) -> DateTime<Utc> {
|
||||
fs::metadata(file).map_or_else(|_| Utc::now(), |metadata| match metadata.modified() {
|
||||
Ok(time) => time.into(),
|
||||
|
|
@ -180,6 +176,7 @@ pub fn last_modified_date(file: PathBuf) -> DateTime<Utc> {
|
|||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn format_diff_time_from_now(date: DateTime<Utc>) -> String {
|
||||
let now: DateTime<Utc> = Utc::now();
|
||||
let last_update = now - date;
|
||||
|
|
@ -194,15 +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.
|
||||
pub async fn export_selection(packages: Vec<PackageRow>) -> Result<bool, String> {
|
||||
let selected = packages
|
||||
.iter()
|
||||
.filter(|p| p.selected)
|
||||
.map(|p| p.name.clone())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
pub fn export_selection(package_names: &[String]) -> Result<bool, String> {
|
||||
let selected = package_names.join("\n");
|
||||
|
||||
match fs::write(EXPORT_FILE_NAME, selected) {
|
||||
Ok(()) => Ok(true),
|
||||
|
|
@ -236,22 +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.
|
||||
/// Exported information will contain package name and description.
|
||||
pub async fn export_packages(
|
||||
user: User,
|
||||
phone_packages: Vec<Vec<PackageRow>>,
|
||||
) -> Result<bool, String> {
|
||||
pub fn export_packages(user: User, phone_packages: &[Vec<CorePackage>]) -> Result<bool, String> {
|
||||
let backup_file = generate_backup_name(chrono::Local::now());
|
||||
|
||||
let file = fs::File::create(backup_file).map_err(|err| err.to_string())?;
|
||||
|
|
@ -260,7 +239,7 @@ pub async fn export_packages(
|
|||
wtr.write_record(["Package Name", "Description"])
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
let uninstalled_packages: Vec<&PackageRow> = phone_packages[user.index]
|
||||
let uninstalled_packages: Vec<&CorePackage> = phone_packages[user.index]
|
||||
.iter()
|
||||
.filter(|p| p.state == PackageState::Uninstalled)
|
||||
.collect();
|
||||
|
|
@ -275,6 +254,36 @@ pub async fn export_packages(
|
|||
Ok(true)
|
||||
}
|
||||
|
||||
/// Truncate description to fit within max length, taking only the first line
|
||||
#[must_use]
|
||||
pub fn truncate_description(desc: &str, max_len: usize) -> String {
|
||||
let first_line = desc.split('\n').next().unwrap_or("").trim_end();
|
||||
|
||||
if first_line.len() <= max_len {
|
||||
first_line.to_string()
|
||||
} else {
|
||||
format!("{}...", &first_line[..max_len.saturating_sub(3)])
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if package matches search term (checks name and description)
|
||||
#[must_use]
|
||||
pub fn matches_search(pkg_name: &str, search_term: &str, pkg_description: Option<&str>) -> bool {
|
||||
let search_lower = search_term.to_lowercase();
|
||||
|
||||
if pkg_name.to_lowercase().contains(&search_lower) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(description) = pkg_description {
|
||||
if description.to_lowercase().contains(&search_lower) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
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,31 +1,32 @@
|
|||
pub mod style;
|
||||
pub mod views;
|
||||
pub mod widgets;
|
||||
|
||||
use crate::core::adb;
|
||||
use crate::core::sync::{Phone, get_devices_list, initial_load};
|
||||
use crate::core::theme::{OS_COLOR_SCHEME, Theme};
|
||||
use crate::core::uad_lists::UadListState;
|
||||
use crate::core::update::{Release, SelfUpdateState, SelfUpdateStatus, get_latest_release};
|
||||
use crate::core::utils::{FULL_NAME, NAME, string_to_theme};
|
||||
|
||||
#[cfg(feature = "img")]
|
||||
use crate::theme::OS_COLOR_SCHEME;
|
||||
use crate::theme::Theme;
|
||||
use crate::theme::string_to_theme;
|
||||
use crate::views::about::{About as AboutView, Message as AboutMessage};
|
||||
use crate::views::list::{
|
||||
List as AppsView, LoadingState as ListLoadingState, Message as AppsMessage,
|
||||
};
|
||||
use crate::views::settings::{Message as SettingsMessage, Settings as SettingsView};
|
||||
use crate::widgets::navigation_menu::nav_menu;
|
||||
use iced::font;
|
||||
use iced::window::icon;
|
||||
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;
|
||||
#[cfg(feature = "img")]
|
||||
use iced::window::icon;
|
||||
use iced::{Alignment, Element, Length, Settings, Task, window::Settings as Window};
|
||||
use iced::{Subscription, event, keyboard};
|
||||
|
||||
#[cfg(feature = "img")]
|
||||
use image::ImageFormat;
|
||||
use log::{debug, error, info};
|
||||
#[cfg(feature = "self-update")]
|
||||
use std::path::PathBuf;
|
||||
use uad_core::adb;
|
||||
use uad_core::sync::{Phone, get_devices_list, initial_load};
|
||||
use uad_core::uad_lists::UadListState;
|
||||
use uad_core::update::{Release, SelfUpdateState, SelfUpdateStatus, get_latest_release};
|
||||
use uad_core::utils::{FULL_NAME, NAME};
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
use crate::core::update::{BIN_NAME, download_update_to_temp_file, remove_file};
|
||||
use uad_core::update::{BIN_NAME, download_update_to_temp_file, remove_file};
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
enum View {
|
||||
|
|
@ -37,8 +38,8 @@ enum View {
|
|||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct UpdateState {
|
||||
self_update: SelfUpdateState,
|
||||
uad_list: UadListState,
|
||||
pub self_update: SelfUpdateState,
|
||||
pub uad_list: UadListState,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -77,19 +78,26 @@ pub enum Message {
|
|||
}
|
||||
|
||||
impl UadGui {
|
||||
#[allow(
|
||||
clippy::unused_self,
|
||||
reason = "required by iced's Application trait interface"
|
||||
)]
|
||||
fn title(&self) -> String {
|
||||
FULL_NAME.to_string()
|
||||
self.selected_device.as_ref().map_or_else(
|
||||
|| FULL_NAME.to_string(),
|
||||
|device| format!("{FULL_NAME} - {}", device.model),
|
||||
)
|
||||
}
|
||||
|
||||
fn new() -> (Self, Task<Message>) {
|
||||
(
|
||||
Self::default(),
|
||||
Task::batch([
|
||||
// Used in crate::gui::widgets::navigation_menu::ICONS. Name is `icomoon`.
|
||||
font::load(include_bytes!("../../resources/assets/icons.ttf").as_slice())
|
||||
// Used in crate::widgets::navigation_menu::ICONS. Name is `icomoon`.
|
||||
font::load(include_bytes!("../../../resources/assets/icons.ttf").as_slice())
|
||||
.map(Message::FontLoaded),
|
||||
Task::perform(initial_load(), Message::ADBSatisfied),
|
||||
Task::perform(get_devices_list(), Message::LoadDevices),
|
||||
Task::perform(async { initial_load() }, Message::ADBSatisfied),
|
||||
Task::perform(async { get_devices_list() }, Message::LoadDevices),
|
||||
Task::perform(
|
||||
async move { get_latest_release() },
|
||||
Message::GetLatestRelease,
|
||||
|
|
@ -98,25 +106,50 @@ impl UadGui {
|
|||
)
|
||||
}
|
||||
|
||||
#[allow(
|
||||
clippy::unused_self,
|
||||
reason = "required by iced's Application trait interface"
|
||||
)]
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
event::listen_with(|event, _status, _env| match event {
|
||||
iced::Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. })
|
||||
if modifiers.control() && modifiers.shift() =>
|
||||
{
|
||||
match key {
|
||||
keyboard::Key::Character(c) => match c.as_str() {
|
||||
"r" => Some(Message::RebootButtonPressed),
|
||||
"5" => Some(Message::RefreshButtonPressed),
|
||||
"a" => Some(Message::AppsPress),
|
||||
"i" => Some(Message::AboutPressed),
|
||||
"s" => Some(Message::SettingsPressed),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
if self.selected_device.is_some() {
|
||||
event::listen_with(Self::keyboard_shortcuts_with_device)
|
||||
} else {
|
||||
event::listen_with(Self::keyboard_shortcuts_without_device)
|
||||
}
|
||||
}
|
||||
|
||||
fn keyboard_shortcuts_with_device(
|
||||
event: iced::Event,
|
||||
_status: iced::event::Status,
|
||||
_window: iced::window::Id,
|
||||
) -> Option<Message> {
|
||||
Self::keyboard_shortcuts(event, true)
|
||||
}
|
||||
|
||||
fn keyboard_shortcuts_without_device(
|
||||
event: iced::Event,
|
||||
_status: iced::event::Status,
|
||||
_window: iced::window::Id,
|
||||
) -> Option<Message> {
|
||||
Self::keyboard_shortcuts(event, false)
|
||||
}
|
||||
|
||||
fn keyboard_shortcuts(event: iced::Event, can_control_device: bool) -> Option<Message> {
|
||||
match event {
|
||||
iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
||||
key: keyboard::Key::Character(c),
|
||||
modifiers,
|
||||
..
|
||||
}) if modifiers.control() && modifiers.shift() => match c.as_str() {
|
||||
"r" if can_control_device => Some(Message::RebootButtonPressed),
|
||||
"5" if can_control_device => Some(Message::RefreshButtonPressed),
|
||||
"a" => Some(Message::AppsPress),
|
||||
"i" => Some(Message::AboutPressed),
|
||||
"s" => Some(Message::SettingsPressed),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(
|
||||
|
|
@ -169,7 +202,7 @@ impl UadGui {
|
|||
self.adb_satisfied,
|
||||
)));
|
||||
}
|
||||
Task::perform(get_devices_list(), Message::LoadDevices)
|
||||
Task::perform(async { get_devices_list() }, Message::LoadDevices)
|
||||
}
|
||||
Message::RebootButtonPressed => {
|
||||
self.apps_view = AppsView::default();
|
||||
|
|
@ -399,13 +432,14 @@ impl UadGui {
|
|||
|
||||
impl UadGui {
|
||||
pub fn start() -> iced::Result {
|
||||
#[cfg(feature = "img")]
|
||||
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 => {
|
||||
include_bytes!("../../resources/assets/logo-dark.png")
|
||||
include_bytes!("../../../resources/assets/logo-dark.png")
|
||||
}
|
||||
dark_light::Mode::Light => {
|
||||
include_bytes!("../../resources/assets/logo-light.png")
|
||||
include_bytes!("../../../resources/assets/logo-light.png")
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -416,6 +450,13 @@ impl UadGui {
|
|||
.settings(Settings {
|
||||
id: Some(String::from(NAME)),
|
||||
default_text_size: iced::Pixels(16.0),
|
||||
// HACK: avoid glyph clipping on some systems. Details:
|
||||
// https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/pull/1337
|
||||
#[cfg(target_os = "linux")]
|
||||
default_font: iced::Font {
|
||||
family: iced::font::Family::Name("Adwaita Sans"),
|
||||
..Default::default()
|
||||
},
|
||||
..Settings::default()
|
||||
})
|
||||
.window(Window {
|
||||
|
|
@ -425,6 +466,7 @@ impl UadGui {
|
|||
},
|
||||
resizable: true,
|
||||
decorations: true,
|
||||
#[cfg(feature = "img")]
|
||||
icon: icon::from_file_data(logo, Some(ImageFormat::Png)).ok(),
|
||||
..iced::window::Settings::default()
|
||||
})
|
||||
23
crates/uad-gui/src/helpers.rs
Normal file
23
crates/uad-gui/src/helpers.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
use crate::style;
|
||||
use crate::theme::Theme;
|
||||
use iced::Element;
|
||||
use iced::widget::button::Button;
|
||||
|
||||
/// Wrapper function for `iced::widget::button` with padding and style applied
|
||||
pub fn button_primary<'a, Message>(
|
||||
content: impl Into<Element<'a, Message, Theme>>,
|
||||
) -> Button<'a, Message, Theme> {
|
||||
iced::widget::button(content)
|
||||
.padding([5, 10])
|
||||
.style(style::Button::Primary)
|
||||
}
|
||||
|
||||
/// Opens a file picker dialog to select a folder
|
||||
pub async fn open_folder() -> Result<std::path::PathBuf, uad_core::utils::Error> {
|
||||
rfd::AsyncFileDialog::new()
|
||||
.set_title("Choose a backup location")
|
||||
.pick_folder()
|
||||
.await
|
||||
.ok_or(uad_core::utils::Error::DialogClosed)
|
||||
.map(|f| f.path().to_path_buf())
|
||||
}
|
||||
11
crates/uad-gui/src/lib.rs
Normal file
11
crates/uad-gui/src/lib.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#![allow(
|
||||
clippy::missing_errors_doc,
|
||||
reason = "Short-circuit doc lint to keep CI green"
|
||||
)]
|
||||
|
||||
pub mod gui;
|
||||
pub mod helpers;
|
||||
pub mod style;
|
||||
pub mod theme;
|
||||
pub mod views;
|
||||
pub mod widgets;
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
#![windows_subsystem = "windows"]
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use crate::core::utils::setup_uad_dir;
|
||||
use fern::{
|
||||
FormatCallback,
|
||||
colors::{Color, ColoredLevelConfig},
|
||||
|
|
@ -10,12 +7,10 @@ use fern::{
|
|||
use log::Record;
|
||||
use std::sync::LazyLock;
|
||||
use std::{fmt::Arguments, fs::OpenOptions, path::PathBuf};
|
||||
use uad_core::utils::setup_uad_dir;
|
||||
|
||||
mod core;
|
||||
mod gui;
|
||||
use uad_gui::gui::UadGui;
|
||||
|
||||
static CONFIG_DIR: LazyLock<PathBuf> =
|
||||
LazyLock::new(|| setup_uad_dir(&dirs::config_dir().expect("Can't detect config dir")));
|
||||
static CACHE_DIR: LazyLock<PathBuf> =
|
||||
LazyLock::new(|| setup_uad_dir(&dirs::cache_dir().expect("Can't detect cache dir")));
|
||||
|
||||
|
|
@ -30,7 +25,7 @@ fn main() -> iced::Result {
|
|||
}
|
||||
|
||||
setup_logger().expect("setup logging");
|
||||
gui::UadGui::start()
|
||||
UadGui::start()
|
||||
}
|
||||
|
||||
/// Sets up logging to a new file in `CACHE_DIR"/uadng.log"`
|
||||
|
|
@ -74,14 +69,14 @@ fn setup_logger() -> Result<(), fern::InitError> {
|
|||
.format(make_formatter(false))
|
||||
.level(default_log_level)
|
||||
// Rust compiler makes module names use _ instead of -
|
||||
.level_for("uad_ng", log::LevelFilter::Debug)
|
||||
.level_for("uad_gui", log::LevelFilter::Debug)
|
||||
.chain(log_file);
|
||||
|
||||
let stdout_dispatcher = fern::Dispatch::new()
|
||||
.format(make_formatter(true))
|
||||
.level(default_log_level)
|
||||
// Rust compiler makes module names use _ instead of -
|
||||
.level_for("uad_ng", log::LevelFilter::Warn)
|
||||
.level_for("uad_gui", log::LevelFilter::Warn)
|
||||
.chain(std::io::stdout());
|
||||
|
||||
fern::Dispatch::new()
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
clippy::wildcard_imports,
|
||||
reason = "Iced style modules use PascalCase and &Theme; wildcard for local convenience"
|
||||
)]
|
||||
use crate::core::theme::{ColorPalette, Theme};
|
||||
use crate::theme::{ColorPalette, Theme};
|
||||
use iced::widget::{
|
||||
button, checkbox, container, overlay, pick_list, radio, scrollable, text, text_editor,
|
||||
text_input,
|
||||
|
|
@ -321,10 +321,12 @@ pub mod Container {
|
|||
use super::*;
|
||||
|
||||
#[allow(dead_code, reason = "Used by other themes or future styles")]
|
||||
#[must_use]
|
||||
pub fn Invisible(_: &Theme) -> container::Style {
|
||||
container::Style::default()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Frame(theme: &Theme) -> container::Style {
|
||||
let p = theme.palette();
|
||||
container::Style {
|
||||
|
|
@ -340,6 +342,7 @@ pub mod Container {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn BorderedFrame(theme: &Theme) -> container::Style {
|
||||
let p = theme.palette();
|
||||
container::Style {
|
||||
|
|
@ -356,6 +359,7 @@ pub mod Container {
|
|||
}
|
||||
|
||||
#[allow(dead_code, reason = "Currently unused in some views")]
|
||||
#[must_use]
|
||||
pub fn Tooltip(theme: &Theme) -> container::Style {
|
||||
let p = theme.palette();
|
||||
container::Style {
|
||||
|
|
@ -371,6 +375,7 @@ pub mod Container {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Background(theme: &Theme) -> container::Style {
|
||||
let p = theme.palette();
|
||||
container::Style {
|
||||
|
|
@ -408,6 +413,7 @@ pub mod Button {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Primary(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let p = theme.palette();
|
||||
let mut style = style_active_hover_disabled(p.bright.primary, p.bright.primary, status);
|
||||
|
|
@ -421,10 +427,12 @@ pub mod Button {
|
|||
dead_code,
|
||||
reason = "Alias kept for semantic clarity in some call-sites"
|
||||
)]
|
||||
#[must_use]
|
||||
pub fn SelfUpdate(theme: &Theme, status: button::Status) -> button::Style {
|
||||
Primary(theme, status)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn RestorePackage(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let p = theme.palette();
|
||||
let mut style = style_active_hover_disabled(p.bright.secondary, p.bright.secondary, status);
|
||||
|
|
@ -441,6 +449,7 @@ pub mod Button {
|
|||
style
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn UninstallPackage(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let p = theme.palette();
|
||||
let mut style = style_active_hover_disabled(p.bright.error, p.bright.error, status);
|
||||
|
|
@ -454,10 +463,12 @@ pub mod Button {
|
|||
dead_code,
|
||||
reason = "Style exposed for disabled state buttons in some contexts"
|
||||
)]
|
||||
#[must_use]
|
||||
pub fn Unavailable(theme: &Theme, status: button::Status) -> button::Style {
|
||||
UninstallPackage(theme, status)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn NormalPackage(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let p = theme.palette();
|
||||
match status {
|
||||
|
|
@ -489,6 +500,7 @@ pub mod Button {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn SelectedPackage(theme: &Theme, _status: button::Status) -> button::Style {
|
||||
let p = theme.palette();
|
||||
button::Style {
|
||||
|
|
@ -508,6 +520,7 @@ pub mod Button {
|
|||
}
|
||||
|
||||
#[allow(dead_code, reason = "Used in views where buttons must be invisible")]
|
||||
#[must_use]
|
||||
pub fn Hidden(_: &Theme, _: button::Status) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
|
|
@ -605,6 +618,7 @@ pub mod Scrollable {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Description(theme: &Theme, _status: scrollable::Status) -> scrollable::Style {
|
||||
let p = theme.palette();
|
||||
let (v, h) = rails(p.normal.surface);
|
||||
|
|
@ -617,6 +631,7 @@ pub mod Scrollable {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Packages(theme: &Theme, _status: scrollable::Status) -> scrollable::Style {
|
||||
let p = theme.palette();
|
||||
let (v, h) = rails(p.base.foreground);
|
||||
|
|
@ -633,6 +648,7 @@ pub mod Scrollable {
|
|||
pub mod CheckBox {
|
||||
use super::*;
|
||||
|
||||
#[must_use]
|
||||
pub fn PackageEnabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
||||
let p = theme.palette();
|
||||
checkbox::Style {
|
||||
|
|
@ -647,6 +663,7 @@ pub mod CheckBox {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn PackageDisabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
||||
let p = theme.palette();
|
||||
checkbox::Style {
|
||||
|
|
@ -664,6 +681,7 @@ pub mod CheckBox {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn SettingsEnabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
||||
let p = theme.palette();
|
||||
checkbox::Style {
|
||||
|
|
@ -678,6 +696,7 @@ pub mod CheckBox {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn SettingsDisabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
|
||||
let p = theme.palette();
|
||||
checkbox::Style {
|
||||
|
|
@ -696,11 +715,13 @@ pub mod CheckBox {
|
|||
pub mod Text {
|
||||
use super::*;
|
||||
|
||||
#[must_use]
|
||||
pub fn Default(theme: &Theme) -> text::Style {
|
||||
let _ = theme;
|
||||
text::Style::default()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Ok(theme: &Theme) -> text::Style {
|
||||
let p = theme.palette();
|
||||
text::Style {
|
||||
|
|
@ -708,6 +729,7 @@ pub mod Text {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Danger(theme: &Theme) -> text::Style {
|
||||
let p = theme.palette();
|
||||
text::Style {
|
||||
|
|
@ -715,6 +737,7 @@ pub mod Text {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn Commentary(theme: &Theme) -> text::Style {
|
||||
let p = theme.palette();
|
||||
text::Style {
|
||||
|
|
@ -738,7 +761,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_palette() {
|
||||
let palette = Theme::default().palette();
|
||||
let palette = Theme::Dark.palette();
|
||||
|
||||
assert_ne!(palette.base.background, palette.base.foreground);
|
||||
assert_ne!(palette.normal.primary, Color::BLACK);
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
use dark_light;
|
||||
use iced::theme::{self, Mode, Palette, Style};
|
||||
use iced::{Color, color};
|
||||
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 {
|
||||
fn default(preference: Mode) -> Self {
|
||||
match preference {
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
use crate::CACHE_DIR;
|
||||
use crate::core::adb;
|
||||
use crate::core::helpers::button_primary;
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::uad_lists::LIST_FNAME;
|
||||
use crate::core::utils::{FULL_NAME, NAME, last_modified_date, open_url};
|
||||
use crate::gui::{UpdateState, style, widgets::text};
|
||||
use crate::helpers::button_primary;
|
||||
use crate::theme::Theme;
|
||||
use crate::{gui::UpdateState, style, widgets::text};
|
||||
use iced::widget::{Space, column, container, row};
|
||||
use iced::{Alignment, Element, Length, Renderer};
|
||||
use log::error;
|
||||
use std::path::PathBuf;
|
||||
use uad_core::CACHE_DIR;
|
||||
use uad_core::adb;
|
||||
use uad_core::uad_lists::LIST_FNAME;
|
||||
use uad_core::utils::{FULL_NAME, NAME, last_modified_date, open_url};
|
||||
|
||||
#[cfg(feature = "self-update")]
|
||||
use crate::core::update::SelfUpdateStatus;
|
||||
use uad_core::update::SelfUpdateStatus;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct About {}
|
||||
|
|
@ -1,19 +1,22 @@
|
|||
use crate::core::config::DeviceSettings;
|
||||
use crate::core::helpers::button_primary;
|
||||
use crate::core::sync::{AdbError, Phone, User, apply_pkg_state_commands, run_adb_action};
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::uad_lists::{
|
||||
use crate::helpers::button_primary;
|
||||
use crate::style;
|
||||
use crate::theme::Theme;
|
||||
use crate::widgets::navigation_menu::ICONS;
|
||||
use log::{error, info, warn};
|
||||
use std::path::PathBuf;
|
||||
use uad_core::config::DeviceSettings;
|
||||
use uad_core::sync::{AdbError, Phone, User, apply_pkg_state_commands};
|
||||
use uad_core::uad_lists::{
|
||||
Opposite, PackageHashMap, PackageState, Removal, UadList, UadListState, load_debloat_lists,
|
||||
};
|
||||
use crate::core::utils::{EXPORT_FILE_NAME, NAME, export_selection, fetch_packages, open_url};
|
||||
use crate::gui::style;
|
||||
use crate::gui::widgets::navigation_menu::ICONS;
|
||||
use std::path::PathBuf;
|
||||
use uad_core::utils::{
|
||||
EXPORT_FILE_NAME, NAME, export_selection, fetch_packages, matches_search, open_url,
|
||||
};
|
||||
|
||||
use crate::gui::views::settings::Settings;
|
||||
use crate::gui::widgets::modal::Modal;
|
||||
use crate::gui::widgets::package_row::{Message as RowMessage, PackageRow};
|
||||
use crate::gui::widgets::text;
|
||||
use crate::views::settings::Settings;
|
||||
use crate::widgets::modal::Modal;
|
||||
use crate::widgets::package_row::{Message as RowMessage, PackageRow};
|
||||
use crate::widgets::text;
|
||||
use iced::widget::scrollable::{Direction, Scrollbar};
|
||||
use iced::widget::{
|
||||
Column, Space, button, checkbox, column, container, pick_list, radio, row, rule, scrollable,
|
||||
|
|
@ -52,8 +55,8 @@ pub struct List {
|
|||
filtered_packages: Vec<usize>,
|
||||
/// Vec of `(user_index, pkg_index)`
|
||||
selected_packages: Vec<(usize, usize)>,
|
||||
selected_package_state: Option<PackageState>,
|
||||
selected_removal: Option<Removal>,
|
||||
selected_package_state: Option<PackageState>,
|
||||
selected_list: Option<UadList>,
|
||||
pub selected_user: Option<User>,
|
||||
all_selected: bool,
|
||||
|
|
@ -81,8 +84,8 @@ pub enum Message {
|
|||
ToggleAllSelected(bool),
|
||||
ListSelected(UadList),
|
||||
UserSelected(User),
|
||||
PackageStateSelected(PackageState),
|
||||
RemovalSelected(Removal),
|
||||
PackageStateSelected(PackageState),
|
||||
ApplyActionOnSelection,
|
||||
List(usize, RowMessage),
|
||||
VerifyAndFallback(Result<PackageInfo, AdbError>),
|
||||
|
|
@ -149,8 +152,8 @@ impl List {
|
|||
}
|
||||
Message::SearchInputChanged(letter) => self.on_search_input_changed(letter),
|
||||
Message::ListSelected(list) => self.on_list_selected(list),
|
||||
Message::PackageStateSelected(state) => self.on_package_state_selected(state),
|
||||
Message::RemovalSelected(removal) => self.on_removal_selected(removal),
|
||||
Message::PackageStateSelected(state) => self.on_package_state_selected(state),
|
||||
Message::List(i, row_msg) => self.on_list_row(i, &row_msg, settings, selected_device),
|
||||
Message::ApplyActionOnSelection => self.on_apply_action_on_selection(),
|
||||
Message::UserSelected(user) => self.on_user_selected(user),
|
||||
|
|
@ -635,7 +638,7 @@ impl List {
|
|||
.width(120),
|
||||
row![text(
|
||||
self.phone_packages[selection.0][selection.1]
|
||||
.uad_list
|
||||
.list
|
||||
.to_string()
|
||||
)]
|
||||
.width(55),
|
||||
|
|
@ -752,12 +755,11 @@ impl List {
|
|||
// that's why `enumerate` is before `filter`.
|
||||
.enumerate()
|
||||
.filter(|(_, p)| {
|
||||
(list_filter == UadList::All || p.uad_list == list_filter)
|
||||
(list_filter == UadList::All || p.list == list_filter)
|
||||
&& (package_filter == PackageState::All || p.state == package_filter)
|
||||
&& (removal_filter == Removal::All || p.removal == removal_filter)
|
||||
&& (self.input_value.is_empty()
|
||||
|| p.name.contains(&self.input_value)
|
||||
|| p.description.contains(&self.input_value))
|
||||
|| matches_search(&p.name, &self.input_value, Some(&p.description)))
|
||||
})
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
|
@ -771,11 +773,21 @@ impl List {
|
|||
) -> Vec<Vec<PackageRow>> {
|
||||
let serial = device_serial.as_ref();
|
||||
if user_list.len() <= 1 {
|
||||
vec![fetch_packages(&uad_list, serial, None)]
|
||||
vec![
|
||||
fetch_packages(&uad_list, serial, None)
|
||||
.into_iter()
|
||||
.map(PackageRow::from)
|
||||
.collect(),
|
||||
]
|
||||
} else {
|
||||
user_list
|
||||
.iter()
|
||||
.map(|user| fetch_packages(&uad_list, serial, Some(user.id)))
|
||||
.map(|user| {
|
||||
fetch_packages(&uad_list, serial, Some(user.id))
|
||||
.into_iter()
|
||||
.map(PackageRow::from)
|
||||
.collect()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
|
@ -877,8 +889,8 @@ impl List {
|
|||
let i_user = self.selected_user.unwrap_or_default().index;
|
||||
self.phone_packages = packages;
|
||||
self.filtered_packages = (0..self.phone_packages[i_user].len()).collect();
|
||||
self.selected_package_state = Some(PackageState::Enabled);
|
||||
self.selected_removal = Some(Removal::Recommended);
|
||||
self.selected_package_state = Some(PackageState::Enabled);
|
||||
self.selected_list = Some(UadList::All);
|
||||
self.selected_user = Some(User::default());
|
||||
self.fallback_notifications.clear();
|
||||
|
|
@ -1053,7 +1065,7 @@ impl List {
|
|||
Task::perform(
|
||||
async move {
|
||||
// Blocking ADB calls happen here (off UI thread)
|
||||
let actual_state_opt = crate::core::sync::verify_package_state(
|
||||
let actual_state_opt = uad_core::sync::verify_package_state(
|
||||
&pkg_name,
|
||||
device.adb_id.as_str(),
|
||||
Some(user_id),
|
||||
|
|
@ -1062,7 +1074,7 @@ impl List {
|
|||
match actual_state_opt {
|
||||
Some(actual_state) if actual_state == wanted_state => {
|
||||
// Check cross-user behavior
|
||||
let error_modal = crate::core::sync::detect_cross_user_behavior(
|
||||
let error_modal = uad_core::sync::detect_cross_user_behavior(
|
||||
&pkg_name,
|
||||
device.adb_id.as_str(),
|
||||
user_id,
|
||||
|
|
@ -1091,16 +1103,16 @@ impl List {
|
|||
// Package doesn't exist (None) or has wrong state - try fallback
|
||||
let actual_state =
|
||||
actual_state_opt.unwrap_or(PackageState::Uninstalled);
|
||||
let fallback_result = crate::core::sync::attempt_fallback(
|
||||
&crate::gui::widgets::package_row::PackageRow::new(
|
||||
&pkg_name,
|
||||
current_state,
|
||||
"",
|
||||
UadList::All,
|
||||
Removal::All,
|
||||
false,
|
||||
false,
|
||||
),
|
||||
// Create CorePackage for fallback
|
||||
let core_package = uad_core::sync::CorePackage {
|
||||
name: pkg_name.clone(),
|
||||
description: String::new(),
|
||||
removal: Removal::All,
|
||||
state: current_state,
|
||||
list: UadList::Unlisted,
|
||||
};
|
||||
let fallback_result = uad_core::sync::attempt_fallback(
|
||||
&core_package,
|
||||
wanted_state,
|
||||
actual_state,
|
||||
device.user_list[i_user],
|
||||
|
|
@ -1195,14 +1207,17 @@ impl List {
|
|||
// fallback task
|
||||
return Task::perform(
|
||||
async move {
|
||||
let temp_pkg = crate::core::sync::CorePackage {
|
||||
let temp_pkg = uad_core::sync::CorePackage {
|
||||
name: pkg_name.clone(),
|
||||
description: String::new(),
|
||||
removal: Removal::All,
|
||||
state: PackageState::Disabled,
|
||||
list: UadList::Unlisted,
|
||||
};
|
||||
|
||||
// get uninstall command
|
||||
let uninstall_cmds =
|
||||
crate::core::sync::apply_pkg_state_commands(
|
||||
uad_core::sync::apply_pkg_state_commands(
|
||||
&temp_pkg,
|
||||
PackageState::Uninstalled,
|
||||
user,
|
||||
|
|
@ -1210,24 +1225,24 @@ impl List {
|
|||
);
|
||||
|
||||
if let Some(cmd) = uninstall_cmds.first() {
|
||||
let dummy_info = PackageInfo::default();
|
||||
let _ = crate::core::sync::run_adb_action(
|
||||
device.adb_id.clone(),
|
||||
cmd.clone(),
|
||||
dummy_info,
|
||||
)
|
||||
.await;
|
||||
let _ = uad_core::sync::run_adb_shell_action(
|
||||
&device.adb_id,
|
||||
cmd,
|
||||
);
|
||||
}
|
||||
|
||||
// reinstall/enable
|
||||
let temp_pkg_uninstalled =
|
||||
crate::core::sync::CorePackage {
|
||||
uad_core::sync::CorePackage {
|
||||
name: pkg_name.clone(),
|
||||
description: String::new(),
|
||||
removal: Removal::All,
|
||||
state: PackageState::Uninstalled,
|
||||
list: UadList::Unlisted,
|
||||
};
|
||||
|
||||
let enable_cmds =
|
||||
crate::core::sync::apply_pkg_state_commands(
|
||||
uad_core::sync::apply_pkg_state_commands(
|
||||
&temp_pkg_uninstalled,
|
||||
PackageState::Enabled,
|
||||
user,
|
||||
|
|
@ -1236,18 +1251,15 @@ impl List {
|
|||
|
||||
// lets enable the new package
|
||||
if let Some(cmd) = enable_cmds.first() {
|
||||
let dummy_info = PackageInfo::default();
|
||||
let _ = crate::core::sync::run_adb_action(
|
||||
device.adb_id.clone(),
|
||||
cmd.clone(),
|
||||
dummy_info,
|
||||
)
|
||||
.await;
|
||||
let _ = uad_core::sync::run_adb_shell_action(
|
||||
&device.adb_id,
|
||||
cmd,
|
||||
);
|
||||
}
|
||||
|
||||
//verifies the final state for thread confirmation
|
||||
let actual_state =
|
||||
crate::core::sync::verify_package_state(
|
||||
uad_core::sync::verify_package_state(
|
||||
&pkg_name,
|
||||
&device.adb_id,
|
||||
Some(user.id),
|
||||
|
|
@ -1316,8 +1328,13 @@ impl List {
|
|||
|
||||
fn on_export_selection(&mut self) -> Task<Message> {
|
||||
let i_user = self.selected_user.unwrap_or_default().index;
|
||||
let package_names: Vec<String> = self.phone_packages[i_user]
|
||||
.iter()
|
||||
.filter(|p| p.selected)
|
||||
.map(|p| p.name.clone())
|
||||
.collect();
|
||||
Task::perform(
|
||||
export_selection(self.phone_packages[i_user].clone()),
|
||||
async move { export_selection(&package_names) },
|
||||
Message::SelectionExported,
|
||||
)
|
||||
}
|
||||
|
|
@ -1483,17 +1500,24 @@ fn build_action_pkg_commands(
|
|||
// Will be filled asynchronously before running the adb action
|
||||
before_cross_user_states: vec![],
|
||||
};
|
||||
// Clone data before async block to avoid borrowing issues
|
||||
let device_serial = device.adb_id.clone();
|
||||
let package_name = u_pkg.name.clone();
|
||||
let user_id = u.id;
|
||||
let phone = device.clone();
|
||||
// In the end there is only one package state change
|
||||
// even if we run multiple adb commands
|
||||
commands.push(Task::perform(
|
||||
run_adb_action_with_before_states(
|
||||
device.adb_id.clone(),
|
||||
action,
|
||||
p_info,
|
||||
u_pkg.name.clone(),
|
||||
u.id,
|
||||
device.clone(),
|
||||
),
|
||||
async move {
|
||||
run_adb_action_with_before_states(
|
||||
&device_serial,
|
||||
&action,
|
||||
p_info,
|
||||
&package_name,
|
||||
user_id,
|
||||
&phone,
|
||||
)
|
||||
},
|
||||
if j == 0 {
|
||||
Message::VerifyAndFallback
|
||||
} else {
|
||||
|
|
@ -1505,23 +1529,25 @@ fn build_action_pkg_commands(
|
|||
commands
|
||||
}
|
||||
|
||||
async fn run_adb_action_with_before_states(
|
||||
device_serial: String,
|
||||
action: String,
|
||||
fn run_adb_action_with_before_states(
|
||||
device_serial: &str,
|
||||
action: &str,
|
||||
mut p_info: PackageInfo,
|
||||
package_name: String,
|
||||
package_name: &str,
|
||||
target_user_id: u16,
|
||||
phone: Phone,
|
||||
phone: &Phone,
|
||||
) -> Result<PackageInfo, AdbError> {
|
||||
// Capture before-state in background to avoid blocking UI thread
|
||||
let before_states = crate::core::sync::capture_cross_user_states(
|
||||
&package_name,
|
||||
&device_serial,
|
||||
let before_states = uad_core::sync::capture_cross_user_states(
|
||||
package_name,
|
||||
device_serial,
|
||||
target_user_id,
|
||||
&phone,
|
||||
phone,
|
||||
);
|
||||
p_info.before_cross_user_states = before_states;
|
||||
run_adb_action(device_serial, action, p_info).await
|
||||
// Run the ADB action
|
||||
uad_core::sync::run_adb_shell_action(device_serial, action)?;
|
||||
Ok(p_info)
|
||||
}
|
||||
|
||||
fn recap<'a>(settings: &Settings, recap: &SummaryEntry) -> Element<'a, Message, Theme, Renderer> {
|
||||
|
|
@ -1,15 +1,7 @@
|
|||
use crate::core::{
|
||||
config::{BackupSettings, Config, DeviceSettings, GeneralSettings},
|
||||
helpers::button_primary,
|
||||
save::{backup_phone, list_available_backup_user, list_available_backups, restore_backup},
|
||||
sync::{AdbError, Phone, User, get_android_sdk, run_adb_action, supports_multi_user},
|
||||
theme::Theme,
|
||||
utils::{
|
||||
DisplayablePath, Error, NAME, export_packages, generate_backup_name, open_folder, open_url,
|
||||
string_to_theme,
|
||||
},
|
||||
};
|
||||
use crate::gui::{
|
||||
use crate::helpers::{button_primary, open_folder};
|
||||
use crate::theme::Theme;
|
||||
use crate::theme::string_to_theme;
|
||||
use crate::{
|
||||
style,
|
||||
views::list::{List as AppsView, PackageInfo},
|
||||
widgets::modal::Modal,
|
||||
|
|
@ -17,9 +9,20 @@ use crate::gui::{
|
|||
widgets::package_row::PackageRow,
|
||||
widgets::text,
|
||||
};
|
||||
use chrono;
|
||||
use iced::widget::{Space, button, checkbox, column, container, pick_list, radio, row, scrollable};
|
||||
use iced::{Alignment, Element, Length, Renderer, Task, alignment};
|
||||
use log::{debug, error, info};
|
||||
use std::path::PathBuf;
|
||||
use uad_core::{
|
||||
config::{BackupSettings, Config, DeviceSettings, GeneralSettings},
|
||||
save::{backup_phone, list_available_backup_user, list_available_backups, restore_backup},
|
||||
sync::{
|
||||
AdbError, CorePackage, Phone, User, get_android_sdk, run_adb_shell_action,
|
||||
supports_disabling, supports_multi_user,
|
||||
},
|
||||
utils::{DisplayablePath, Error, NAME, export_packages, generate_backup_name, open_url},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PopUpModal {
|
||||
|
|
@ -101,33 +104,37 @@ impl Settings {
|
|||
Task::none()
|
||||
}
|
||||
|
||||
fn handle_expert_mode(&mut self, phone: &Phone, toggled: bool) -> Task<Message> {
|
||||
fn handle_expert_mode(&mut self, _phone: &Phone, toggled: bool) -> Task<Message> {
|
||||
self.general.expert_mode = toggled;
|
||||
debug!("Config change: {self:?}");
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
let mut config = Config::load_configuration_file();
|
||||
config.save_device_settings(self.device.clone(), self.general.clone());
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn handle_disable_mode(&mut self, phone: &Phone, toggled: bool) -> Task<Message> {
|
||||
if phone.android_sdk >= 23 {
|
||||
if supports_disabling(phone) {
|
||||
self.device.disable_mode = toggled;
|
||||
debug!("Config change: {self:?}");
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
let mut config = Config::load_configuration_file();
|
||||
config.save_device_settings(self.device.clone(), self.general.clone());
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn handle_multi_user_mode(&mut self, phone: &Phone, toggled: bool) -> Task<Message> {
|
||||
fn handle_multi_user_mode(&mut self, _phone: &Phone, toggled: bool) -> Task<Message> {
|
||||
self.device.multi_user_mode = toggled;
|
||||
debug!("Config change: {self:?}");
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
let mut config = Config::load_configuration_file();
|
||||
config.save_device_settings(self.device.clone(), self.general.clone());
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn handle_apply_theme(&mut self, phone: &Phone, theme: Theme) -> Task<Message> {
|
||||
fn handle_apply_theme(&mut self, _phone: &Phone, theme: Theme) -> Task<Message> {
|
||||
self.general.theme = theme.to_string();
|
||||
debug!("Config change: {self:?}");
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
let mut config = Config::load_configuration_file();
|
||||
config.save_device_settings(self.device.clone(), self.general.clone());
|
||||
Task::none()
|
||||
}
|
||||
|
||||
|
|
@ -163,8 +170,9 @@ impl Settings {
|
|||
None => {
|
||||
self.device = DeviceSettings {
|
||||
device_id: phone.adb_id.clone(),
|
||||
// see FAQ, and GH-issue #1426
|
||||
disable_mode: supports_disabling(phone),
|
||||
multi_user_mode: supports_multi_user(phone),
|
||||
disable_mode: false,
|
||||
backup,
|
||||
};
|
||||
}
|
||||
|
|
@ -182,12 +190,16 @@ impl Settings {
|
|||
phone: &Phone,
|
||||
packages: &[Vec<PackageRow>],
|
||||
) -> Task<Message> {
|
||||
let core_packages: Vec<Vec<_>> = packages
|
||||
.iter()
|
||||
.map(|user_packages| user_packages.iter().map(CorePackage::from).collect())
|
||||
.collect();
|
||||
// Avoid capturing &self and &phone in the future; clone owned data first.
|
||||
let user_list = phone.user_list.clone();
|
||||
let device_id = self.device.device_id.clone();
|
||||
let packages_owned = core_packages;
|
||||
Task::perform(
|
||||
backup_phone(
|
||||
phone.user_list.clone(),
|
||||
self.device.device_id.clone(),
|
||||
packages.to_vec(),
|
||||
),
|
||||
async move { backup_phone(user_list, device_id, &packages_owned) },
|
||||
Message::DeviceBackedUp,
|
||||
)
|
||||
}
|
||||
|
|
@ -217,7 +229,11 @@ impl Settings {
|
|||
packages: &[Vec<PackageRow>],
|
||||
nb_running_async_adb_commands: &mut u32,
|
||||
) -> Task<Message> {
|
||||
match restore_backup(phone, packages, &self.device) {
|
||||
let core_packages: Vec<Vec<_>> = packages
|
||||
.iter()
|
||||
.map(|user_packages| user_packages.iter().map(CorePackage::from).collect())
|
||||
.collect();
|
||||
match restore_backup(phone, &core_packages, &self.device) {
|
||||
Ok(restore_result) => {
|
||||
let mut commands = vec![];
|
||||
*nb_running_async_adb_commands = 0;
|
||||
|
|
@ -230,8 +246,13 @@ impl Settings {
|
|||
};
|
||||
for command in p.commands.clone() {
|
||||
*nb_running_async_adb_commands += 1;
|
||||
let p_info_clone = p_info.clone();
|
||||
let phone_id = phone.adb_id.clone();
|
||||
commands.push(Task::perform(
|
||||
run_adb_action(phone.adb_id.clone(), command, p_info.clone()),
|
||||
async move {
|
||||
run_adb_shell_action(phone_id, command.as_str())
|
||||
.map(|_| p_info_clone)
|
||||
},
|
||||
Message::RestoringDevice,
|
||||
));
|
||||
}
|
||||
|
|
@ -274,7 +295,8 @@ impl Settings {
|
|||
|
||||
if let Ok(path) = result {
|
||||
self.general.backup_folder = path;
|
||||
Config::save_changes(self, &phone.adb_id);
|
||||
let mut config = Config::load_configuration_file();
|
||||
config.save_device_settings(self.device.clone(), self.general.clone());
|
||||
self.load_device_settings(phone);
|
||||
}
|
||||
Task::none()
|
||||
|
|
@ -293,8 +315,12 @@ impl Settings {
|
|||
selected_user: Option<User>,
|
||||
packages: &[Vec<PackageRow>],
|
||||
) -> Task<Message> {
|
||||
let core_packages: Vec<Vec<_>> = packages
|
||||
.iter()
|
||||
.map(|user_packages| user_packages.iter().map(CorePackage::from).collect())
|
||||
.collect();
|
||||
Task::perform(
|
||||
export_packages(selected_user.unwrap_or_default(), packages.to_vec()),
|
||||
async move { export_packages(selected_user.unwrap_or_default(), &core_packages) },
|
||||
Message::PackagesExported,
|
||||
)
|
||||
}
|
||||
|
|
@ -473,14 +499,14 @@ impl Settings {
|
|||
.size(20)
|
||||
.style(style::CheckBox::SettingsEnabled);
|
||||
|
||||
let disable_checkbox_style = if phone.android_sdk >= 23 {
|
||||
let disable_checkbox_style = if supports_disabling(phone) {
|
||||
style::CheckBox::SettingsEnabled
|
||||
} else {
|
||||
style::CheckBox::SettingsDisabled
|
||||
};
|
||||
|
||||
let disable_mode_descr =
|
||||
text("In some cases, it can be better to disable a package instead of uninstalling it")
|
||||
text("In many cases, it's better to disable a package instead of uninstalling it")
|
||||
.style(style::Text::Commentary);
|
||||
|
||||
let unavailable_btn = button(text("Unavailable").size(14))
|
||||
|
|
@ -497,7 +523,7 @@ impl Settings {
|
|||
.size(20)
|
||||
.style(disable_checkbox_style);
|
||||
|
||||
let disable_setting_row = if phone.android_sdk >= 23 {
|
||||
let disable_setting_row = if supports_disabling(phone) {
|
||||
row![
|
||||
disable_mode_checkbox,
|
||||
Space::new().width(Length::Fill).height(Length::Shrink),
|
||||
|
|
@ -25,6 +25,7 @@ impl<'a, Message, Theme, Renderer> Modal<'a, Message, Theme, Renderer> {
|
|||
|
||||
/// Sets the message that will be produces when the background
|
||||
/// of the [`Modal`] is pressed
|
||||
#[must_use]
|
||||
pub fn on_blur(self, on_blur: Message) -> Self {
|
||||
Self {
|
||||
on_blur: Some(on_blur),
|
||||
|
|
@ -1,18 +1,22 @@
|
|||
use crate::core::helpers::button_primary;
|
||||
pub use crate::core::sync::Phone;
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::update::{SelfUpdateState, SelfUpdateStatus};
|
||||
pub use crate::gui::views::about::Message as AboutMessage;
|
||||
pub use crate::gui::views::list::{List as AppsView, LoadingState as ListLoadingState};
|
||||
use crate::gui::{Message, style, widgets::text};
|
||||
use crate::helpers::button_primary;
|
||||
use crate::theme::Theme;
|
||||
pub use crate::views::about::Message as AboutMessage;
|
||||
pub use crate::views::list::{List as AppsView, LoadingState as ListLoadingState};
|
||||
use crate::{gui::Message, style, widgets::text};
|
||||
use iced::widget::{Space, button, container, pick_list, row, tooltip};
|
||||
use iced::{Alignment, Element, Font, Length, Renderer, alignment, font};
|
||||
pub use uad_core::sync::Phone;
|
||||
use uad_core::update::{SelfUpdateState, SelfUpdateStatus};
|
||||
/// resources/assets/icons.ttf, loaded in [`crate::gui::UadGui`]
|
||||
pub const ICONS: Font = Font {
|
||||
family: font::Family::Name("icomoon"),
|
||||
..Font::DEFAULT
|
||||
};
|
||||
|
||||
#[allow(
|
||||
clippy::too_many_lines,
|
||||
reason = "Navigation layout composes many widgets inline"
|
||||
)]
|
||||
pub fn nav_menu<'a>(
|
||||
device_list: &'a [Phone],
|
||||
selected_device: Option<Phone>,
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
use crate::core::sync::Phone;
|
||||
use crate::core::theme::Theme;
|
||||
use crate::core::uad_lists::{PackageState, Removal, UadList};
|
||||
use crate::gui::style;
|
||||
use crate::gui::views::settings::Settings;
|
||||
use crate::gui::widgets::text;
|
||||
use crate::style;
|
||||
use crate::theme::Theme;
|
||||
use crate::views::settings::Settings;
|
||||
use crate::widgets::text;
|
||||
use log::warn;
|
||||
use uad_core::sync::{CorePackage, Phone};
|
||||
use uad_core::uad_lists::{PackageState, Removal, UadList};
|
||||
|
||||
use iced::widget::{Space, button, checkbox, row};
|
||||
use iced::{Alignment, Element, Length, Renderer, Task, alignment};
|
||||
|
|
@ -11,10 +12,10 @@ use iced::{Alignment, Element, Length, Renderer, Task, alignment};
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct PackageRow {
|
||||
pub name: String,
|
||||
pub state: PackageState,
|
||||
pub description: String,
|
||||
pub uad_list: UadList,
|
||||
pub removal: Removal,
|
||||
pub state: PackageState,
|
||||
pub list: UadList,
|
||||
pub selected: bool,
|
||||
pub current: bool,
|
||||
}
|
||||
|
|
@ -27,21 +28,22 @@ pub enum Message {
|
|||
}
|
||||
|
||||
impl PackageRow {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
name: &str,
|
||||
state: PackageState,
|
||||
description: &str,
|
||||
uad_list: UadList,
|
||||
removal: Removal,
|
||||
state: PackageState,
|
||||
list: UadList,
|
||||
selected: bool,
|
||||
current: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.to_string(),
|
||||
state,
|
||||
description: description.to_string(),
|
||||
uad_list,
|
||||
removal,
|
||||
state,
|
||||
list,
|
||||
selected,
|
||||
current,
|
||||
}
|
||||
|
|
@ -143,3 +145,56 @@ impl PackageRow {
|
|||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
// Conversions between PackageRow and CorePackage
|
||||
impl From<CorePackage> for PackageRow {
|
||||
fn from(core: CorePackage) -> Self {
|
||||
Self {
|
||||
name: core.name.clone(),
|
||||
description: core.description,
|
||||
removal: core.removal,
|
||||
state: core.state,
|
||||
list: core.list,
|
||||
selected: false, // Default to not selected
|
||||
current: false, // Default to not current
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CorePackage> for PackageRow {
|
||||
fn from(core: &CorePackage) -> Self {
|
||||
Self {
|
||||
name: core.name.clone(),
|
||||
description: core.description.clone(),
|
||||
removal: core.removal,
|
||||
state: core.state,
|
||||
list: core.list,
|
||||
selected: false,
|
||||
current: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&PackageRow> for CorePackage {
|
||||
fn from(row: &PackageRow) -> Self {
|
||||
Self {
|
||||
name: row.name.clone(),
|
||||
description: row.description.clone(),
|
||||
removal: row.removal,
|
||||
state: row.state,
|
||||
list: row.list,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PackageRow> for CorePackage {
|
||||
fn from(row: PackageRow) -> Self {
|
||||
Self {
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
removal: row.removal,
|
||||
state: row.state,
|
||||
list: row.list,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,11 @@
|
|||
reason = "this is the replacement that enforces advanced shaping for disallowed [`iced::widget::Text`]"
|
||||
)]
|
||||
|
||||
use crate::theme::Theme;
|
||||
use iced::advanced::text::IntoFragment;
|
||||
use iced::widget;
|
||||
|
||||
// Creates a new Text widget with advanced shaping.
|
||||
pub fn text<'a>(text: impl IntoFragment<'a>) -> widget::Text<'a, crate::core::theme::Theme> {
|
||||
pub fn text<'a>(text: impl IntoFragment<'a>) -> widget::Text<'a, Theme> {
|
||||
widget::Text::new(text).shaping(iced::widget::text::Shaping::Advanced)
|
||||
}
|
||||
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1765270179,
|
||||
"narHash": "sha256-g2a4MhRKu4ymR4xwo+I+auTknXt/+j37Lnf0Mvfl1rE=",
|
||||
"lastModified": 1776949667,
|
||||
"narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "677fbe97984e7af3175b6c121f3c39ee5c8d62c9",
|
||||
"rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
|
|
@ -27,10 +27,10 @@
|
|||
with pkgs;
|
||||
[
|
||||
libglvnd
|
||||
xorg.libX11
|
||||
xorg.libXcursor
|
||||
xorg.libXi
|
||||
xorg.libXrandr
|
||||
libX11
|
||||
libXcursor
|
||||
libXi
|
||||
libXrandr
|
||||
libxkbcommon
|
||||
wayland
|
||||
]
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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