Compare commits

...

189 commits

Author SHA1 Message Date
Anonymoussaurus
44abeefb71
Merge pull request #1451 from Universal-Debloater-Alliance/feat/default-disable
feat(settings): disable-mode by default
2026-08-09 02:57:12 +02:00
Rudxain
67f880b507
feat(settings): Disable-mode by default
- feat(settings): `disable_mode=true` if device supports it; update description
- refactor: define `supports_disabling` and use it whenever possible
- refactor(sync): inline `MULTI_USER_SDK`, with tiny comment
- docs(sync): rm redundant "Android"
2026-08-07 18:38:46 -04:00
Anonymoussaurus
14d2f29f0a
Merge pull request #1450 from jarekt/patch-1
pkg(com.android.networkstack.tethering.inprocess): change description and removal
2026-08-05 15:27:31 +02:00
Jarek
f3df24beb9
change com.android.networkstack.tethering.inprocess to unsafe 2026-08-05 14:57:22 +02:00
Anonymoussaurus
2379e852ce
Merge pull request #1438 from TechyDodoDevMan/patch-1
pkg(com.google.android.wearable.pixel.aspen): add package
2026-08-03 14:42:15 +02:00
TechyDodoDevMan
3c4011807b
Change removal recommendation from 'Recommended' to 'Advanced'
Resolved suggestion
2026-07-30 16:12:33 -04:00
TechyDodoDevMan
9dbdd59b0e
Merge branch 'main' into patch-1 2026-07-30 16:10:39 -04:00
Anonymoussaurus
ef94066d65
Merge pull request #1439 from insanebecauseimbestwheniminlove/main
pkg(xiaomi): add package
2026-07-25 15:16:21 +02:00
insanebecauseimbestwheniminlove
266f1b9597
Update uad_lists.json 2026-07-25 03:53:55 +02:00
TechyDodoDevMan
856606c58b
Update uad_lists.json
Added Fitbit Connected Fitness
2026-07-24 12:05:16 -04:00
Anonymoussaurus
9336f25e7d
Merge pull request #1433 from Wakelock/main
pkg(blackview, huawei): add and edit packages
2026-07-22 11:54:43 +02:00
Wakelock
3f55e6cdba
Update uad_lists.json 2026-07-18 10:25:28 +00:00
Wakelock
c38c1cd2d0
Update uad_lists.json 2026-07-18 10:05:39 +00:00
Wakelock
31234bed3c
Update uad_lists.json 2026-07-18 09:50:33 +00:00
Wakelock
067f2251e9
Update uad_lists.json 2026-07-18 09:43:54 +00:00
André Klein
43d323332f
fix(core): two bugs blocking package operations on real devices (#1430)
* fix(core): two bugs blocking package operations on real devices

- adb.rs: replace debug_assert! in list_packages_sys with filter_map
  to avoid panicking on nonstandard package names from real devices.
- sync.rs: fix inverted guard in request_builder (is_some -> is_none)
  that was rejecting every valid package name instead of invalid ones.
2026-07-16 17:35:15 -04:00
Wakelock
4ec90fa807
Update uad_lists.json 2026-07-16 10:07:28 +00:00
Wakelock
85c0d67776
Update uad_lists.json 2026-07-16 10:04:11 +00:00
Wakelock
3bc538292d
Update uad_lists.json 2026-07-16 07:47:04 +00:00
Wakelock
93c83863a6
Update uad_lists.json 2026-07-16 07:28:22 +00:00
Anonymoussaurus
596120a215
Merge pull request #1432 from Universal-Debloater-Alliance/docs/remove-cli-readme
docs(cli): remove README after moving it to the wiki
2026-07-14 11:16:37 +02:00
Anonymoussaurus
d98677c7ca
Merge pull request #1420 from iamanaws/chore/cli-deps-and-errors
fix(cli): avoid duplicate error output
2026-07-12 01:15:09 +02:00
Angel J
469ad5f710
docs(cli): remove README after moving it to the wiki 2026-07-11 08:08:16 -07:00
Angel J
81291e0655
fix(cli): avoid duplicate error output 2026-07-11 07:43:16 -07:00
Angel J
f9e4f141c0
chore(deps): bump tokio to 1.52.3 2026-07-11 07:38:48 -07:00
Angel J
1623d39b55
chore(deps): bump clap to 4.6.1 2026-07-11 07:38:45 -07:00
Anonymoussaurus
258a50cdfe
Merge pull request #1427 from Universal-Debloater-Alliance/refactor/pack_id-android
refactor(core): allow "android" `PackageId`
2026-07-06 10:47:34 +02:00
Rudxain
ded17cf91d
refactor(core): allow "android" PackageId 2026-07-01 19:14:56 -04:00
Anonymoussaurus
b051be8162
Merge pull request #1424 from aaronjmars/security/validate-package-id-in-request-builder
harden: validate package-name charset in request_builder
2026-06-26 15:48:17 +02:00
aeonframework
bd1ed7ea76 harden: validate package-name charset in request_builder
`request_builder` interpolates `package` verbatim into a device-shell
action string. Every current caller reconciles the name against the live
device package list first, so this is safe today — but the safety lives in
the callers, not the sink. Add a local charset guard (Android app-ID set
`[A-Za-z0-9_.]`) so a future caller passing an unreconciled or file-sourced
name can't produce an injectable device-shell command. Rejects nothing
legitimate; fails closed (emits no command) on a malformed name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 10:54:06 -04:00
Anonymoussaurus
6bb0155c3a
Merge pull request #1416 from W1Z00/main
pkg(com.mi.appfinder): update description
2026-06-17 20:55:23 +02:00
Anonymoussaurus
4773be1f00
Merge pull request #1297 from iamanaws/feature/cli
feat: add CLI and restructure into multi-package workspace
2026-06-17 20:49:43 +02:00
Anonymoussaurus
7d023fadef
Merge pull request #1414 from Forero-0/main
pkg(samsung): add/update packages
2026-06-17 20:49:07 +02:00
Anonymoussaurus
826b159d03
Merge pull request #1413 from strumiv/pkg-motorola-setup-update
pkg(motorola): update com.motorola.setup removal level and description
2026-06-17 20:48:10 +02:00
W1Z00
ddd9b16ced
update desc for com.mi.appfinder 2026-06-17 18:38:00 +08:00
Forero-0
1f561b8d2d Update uad_lists.json 2026-06-16 18:22:28 -05:00
Angel J
02ba6e2cca
refactor: move theme to uad-gui; drop core gui deps; mark PmCommand must-use 2026-06-13 17:21:31 -07:00
Angel J
da7482601e
fix(cli): improve package verification, and output robustness 2026-06-13 17:21:28 -07:00
iamanaws
7dcb7e9315
feat: add CLI and restructure into multi-package workspace
- Add crates/uad-cli for command-line interface
- Extract shared logic into crates/uad-core
- Separate GUI into crates/uad-gui
- Enable code reuse across components
2026-06-13 17:21:27 -07:00
itz8bitstudios
b86c41187c pkg(motorola): update com.motorola.setup removal level and description
- Change removal level from Advanced to Expert
- Update description to emphasize security patch implications
- Clarify that freezing breaks Google Play System Updates (Project Mainline)
- Add clear warning about security patch impact

Closes #1412
2026-06-13 17:06:29 +02:00
Anonymoussaurus
1bb1453257
Merge pull request #1410 from Universal-Debloater-Alliance/chore/keyboard
pkg(aosp): fix typo in AOSP keyboard app description
2026-06-08 00:15:33 +02:00
Angel J
649d0d0bcf
pkg(aosp): fix typo in AOSP keyboard app description 2026-06-04 19:31:58 -07:00
Anonymoussaurus
0e5203f2fa
Merge pull request #1408 from Lost-Entrepreneur439/main
pkg(samsung, bell): add packages from Samsung Galaxy A21
2026-05-31 15:42:37 +02:00
Evelyn Torres
b11ac0bff9 pkg(samsung, bell): Add packages from Samsung Galaxy A21 2026-05-31 08:17:58 -02:30
Ricardo Fernández Serrata
6dea60dfc0
Merge pull request #1407 from Universal-Debloater-Alliance/dependabot/cargo/tar-0.4.46
chore(deps): bump tar from 0.4.45 to 0.4.46
2026-05-30 16:39:44 -04:00
dependabot[bot]
6e02459ee2
chore(deps): bump tar from 0.4.45 to 0.4.46
Bumps [tar](https://github.com/composefs/tar-rs) from 0.4.45 to 0.4.46.
- [Release notes](https://github.com/composefs/tar-rs/releases)
- [Commits](https://github.com/composefs/tar-rs/compare/0.4.45...0.4.46)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 0.4.46
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-29 21:57:11 +00:00
Anonymoussaurus
5492d39683
Merge pull request #1397 from Lost-Entrepreneur439/main
pkg(insulet): add packes from omnipod dash pdm
2026-05-18 11:27:47 +02:00
Evelyn
fb0dfa8d14
pkg(aosp): Update description for onetimeinitalizer 2026-05-12 17:17:10 -02:30
Anonymoussaurus
96dd6d937e
Merge pull request #1399 from Universal-Debloater-Alliance/g.aosp.wifi.res.ol-update
pkg(com.google.android.wifi.resources.overlay.common): update description
2026-05-11 14:36:35 +02:00
Anonymoussaurus
2739d9b213
Merge pull request #1398 from Universal-Debloater-Alliance/fix-linux-font
fix(gui): use Adwaita on Linux
2026-05-11 14:03:21 +02:00
Rudxain
8b0e15eb9a
pkg: update com.google.android.wifi.resources.overlay.common 2026-05-09 05:17:41 -04:00
Rudxain
51a929009c
docs(gui): explain Adwaita hack 2026-05-09 04:54:02 -04:00
bobbie.w
9108e3d4b4
fix(gui): explicitly request Adwaita Sans font on Linux to prevent Bounding Box clipping from monospace fallback 2026-05-09 04:48:27 -04:00
dependabot[bot]
e0e87e39ea
chore(deps): bump rand from 0.8.5 to 0.8.6 (#1387)
Bumps [rand](https://github.com/rust-random/rand) from 0.8.5 to 0.8.6.
- [Release notes](https://github.com/rust-random/rand/releases)
- [Changelog](https://github.com/rust-random/rand/blob/0.8.6/CHANGELOG.md)
- [Commits](https://github.com/rust-random/rand/compare/0.8.5...0.8.6)

---
updated-dependencies:
- dependency-name: rand
  dependency-version: 0.8.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>
2026-05-09 04:30:51 -04:00
dependabot[bot]
01dd13117c
chore(deps): bump rustls-webpki from 0.103.10 to 0.103.13 (#1383)
Bumps [rustls-webpki](https://github.com/rustls/webpki) from 0.103.10 to 0.103.13.
- [Release notes](https://github.com/rustls/webpki/releases)
- [Commits](https://github.com/rustls/webpki/compare/v/0.103.10...v/0.103.13)

---
updated-dependencies:
- dependency-name: rustls-webpki
  dependency-version: 0.103.13
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>
2026-05-09 04:08:01 -04:00
Evelyn Torres
c16ee8b379 pkg(insulet): add packes from omnipod dash pdm 2026-05-08 13:18:58 -02:30
Anonymoussaurus
b47675e913
Merge pull request #1347 from Mickmick21/main
pkg(honor) : add packages
2026-05-05 15:39:31 +02:00
Mickmick21
1731d39a2d Update uad_lists.json
Update uad_lists.json

Final ?

Update uad_lists.json

Update uad_lists.json0

Update uad_lists.json

Final commit.

Perfect example of why to check your commits.

pkg(jp.co.omronsoft.iwnnime.ml): change to unsafe

Don't know how to name this.

Try to resolve conflicts

I hate conflicts so much
2026-05-05 14:59:14 +02:00
Anonymoussaurus
dc6d383f58
Merge pull request #1393 from JustArchi/patch-1
pkg(com.motorola.securevault): change removal
2026-05-05 11:49:27 +02:00
Anonymoussaurus
a3de7464d4
Merge pull request #1392 from Raihann22/patch-1
pkg(com.android.providers.contacts): change description
2026-05-05 11:48:29 +02:00
Anonymoussaurus
0415bab6c0
Merge pull request #1375 from nyxSeal/main
pkg: change descriptions and removals
2026-05-05 11:43:44 +02:00
Anonymoussaurus
f788c87daa
Merge branch 'main' into main 2026-05-05 11:40:46 +02:00
Łukasz Domeradzki
20fcc6e7b6
pkg(com.motorola.securevault): Improve description 2026-05-01 14:09:49 +02:00
Łukasz Domeradzki
ede2b02501
pkg(com.motorola.securevault) Move under advanced 2026-05-01 00:09:12 +02:00
Raihan
d6a0d0733e
Update uad_lists.json
Updated the description for the com.android.providers.contacts to include additional information about Bluetooth malfunction on POCO Pad M1.
2026-04-30 15:53:37 +08:00
Anonymoussaurus
1d93422e6f
build: fix error and warnings (#1384)
* build: fix error and warnings

Address clippy suggestions and clean up pattern matching:

- Replace manual sort_by with sort_by_key(|a| a.name.to_lowercase()) for user_package in fetch_packages.
- Add #[allow(clippy::unused_self)] to UadGui::title and UadGui::subscription to silence unused_self warnings.
- Simplify the keyboard event match by destructuring the Character variant directly for clearer pattern matching.
- Add #[allow(clippy::too_many_lines)] to nav_menu to silence the long-function lint.

These are lint and readability-focused changes with no intended behavioral change.

* chore(clippy): add reason

* build(ci): fix formatting
2026-04-29 20:08:56 -04:00
nyxSeal
e7a96d7e0f
docs: update conflicted package description
added both the part where it will drain your battery and where it contains a lot of useful features
2026-04-29 22:22:57 +00:00
nyxSeal
3bc499db42 Merge remote-tracking branch 'upstream/main' 2026-04-29 17:54:25 -04:00
nyxSeal
0a76d615ba
docs: update personal safety app
change from recommended to advanced, plus a bit of info about what this app adds without a google account
2026-04-29 20:49:16 +00:00
Anonymoussaurus
bf3d6ccc59
Merge pull request #1388 from asahi-xd/main
pkg(com.transsion.batterylab): change description
2026-04-29 22:43:19 +02:00
Asahi
01a37e2d33 fixed typo and added personal observations
I have fixed a few typos, and added my personal experiences with this program. I was feeling experimental and tried to use its 'battery saving' features, and ended up completely regretting it.
2026-04-25 00:39:14 +05:30
Anonymoussaurus
8e160ff738
Merge pull request #1382 from JustArchi/patch-1
pkg(com.xiaomi.cameramind): change description and removal
2026-04-22 18:06:42 +02:00
Łukasz Domeradzki
f85c85a592
Merge branch 'main' into patch-1 2026-04-22 18:05:20 +02:00
Anonymoussaurus
7eaff8f810
Merge pull request #1380 from Universal-Debloater-Alliance/opt-image
build(logo): optional `image` dep
2026-04-22 17:31:22 +02:00
Łukasz Domeradzki
acef695f8e
Add camera as dependency to cameramind 2026-04-22 01:38:39 +02:00
Łukasz Domeradzki
3e52ff6ba1
Move com.xiaomi.cameramind under advanced 2026-04-22 01:20:53 +02:00
Leonardo Guerreiro
644b30ae73
Merge pull request #1379 from Forero-0/add-packages
pkg(samsung): add packages
2026-04-13 16:01:08 +01:00
Anonymoussaurus
23cdfafefb
Merge pull request #1367 from reindex-ot/main
pkg(aura): add packages
2026-04-12 16:39:36 +02:00
Rudxain
49250576f3
build(logo): optional image dep 2026-04-11 00:58:55 -04:00
Forero-0
a5d095e140 Update uad_lists.json 2026-04-09 19:02:25 -05:00
Anonymoussaurus
fd94a84a71
Merge pull request #1371 from lawson85/LDplayer-packages
pkg(cyanogemod): add packages
2026-04-06 12:38:21 +02:00
Anonymoussaurus
52f56a42cf
Merge pull request #1374 from Efreak/main
pkg(com.samsung.storyservice): change description
2026-04-06 12:37:33 +02:00
Efreak
6a44fda50a Removal advanced for com.samsung.storyservice 2026-04-05 23:50:09 -07:00
Jacob L
e2c47a7a2d
Fix formatting in uad_lists.json 2026-04-05 15:20:23 +01:00
Re*Index. (ot_inc)
a8ed2ea1ec
Fix typo 2026-04-05 06:58:17 +09:00
nyxSeal
648f5a088f
docs: change description and removal for com.transsion.zahooc and com.transsion.batterylab 2026-04-03 22:52:14 +00:00
nyxSeal
a6865c01a5
docs: change 7 package descriptions and/or removal 2026-04-03 22:38:17 +00:00
Efreak
5b52c2dbe6 disabling com.samsung.storyservice disables samsung edge panels 2026-04-03 15:29:45 -07:00
nyxSeal
2cac0cb2ca
Revert 1ef7a9f and 745f036
Didn't see that the issue was awaiting author response
2026-04-03 21:37:00 +00:00
nyxSeal
1ef7a9f67c
docs: re-add missing description segment 2026-04-03 21:31:48 +00:00
nyxSeal
745f03654c
docs: update description and removal for android.auto_generated_rro_product__ 2026-04-03 21:30:21 +00:00
nyxSeal
06e5dbd2db
docs: update description and removal for com.google.android.apps.camera.services 2026-04-03 21:26:36 +00:00
nyxSeal
26b470d8a2
docs: update com.google.android.settings.intelligence
add comprehensive comparison chart for alternative launchers
2026-04-03 21:24:53 +00:00
nyxSeal
e23a0c9058
docs: update description for com.google.android.apps.nexuslauncher 2026-04-03 21:20:46 +00:00
nyxSeal
f8ccfa686c
docs: update description and removal for com.google.hearing.visualization.accessibility.scribe 2026-04-03 21:18:11 +00:00
nyxSeal
4cec1c0a91
docs: update descriptions and removals for com.google.android.soundpicker and com.google.android.settings.intelligence 2026-04-03 21:12:14 +00:00
Jacob L
8e852266b2
Add Cyanogenmod file manager to uad_lists.json
Added entry for Cyanogenmod file manager with details.
2026-04-02 14:32:33 +01:00
Jacob L
ae3c59cf89
Add android.ext.shared entry to uad_lists.json 2026-04-02 13:42:46 +01:00
Re*Index. (ot_inc)
82adb10fc0
Add Aura from Unity section 2026-03-27 02:13:11 +09:00
Re*Index. (ot_inc)
32360a04b6
Add Aura from Unity srction. 2026-03-27 02:12:24 +09:00
Anonymoussaurus
afe7deb29f
Merge pull request #1356 from Universal-Debloater-Alliance/dependabot/cargo/tar-0.4.45
chore(deps): bump tar from 0.4.44 to 0.4.45
2026-03-26 15:04:24 +01:00
Anonymoussaurus
37eaee257d
Merge pull request #1357 from Universal-Debloater-Alliance/dependabot/cargo/rustls-webpki-0.103.10
chore(deps): bump rustls-webpki from 0.103.8 to 0.103.10
2026-03-26 15:04:11 +01:00
Anonymoussaurus
5a60cfbe53
Merge pull request #1354 from hstsethi/main
pkg(in.gov.uidai.pehchaan): add package
2026-03-26 11:05:29 +01:00
Harshit Sethi
61e1bd29ad
Merge branch 'main' into main 2026-03-26 12:40:47 +05:30
Anonymoussaurus
794fe96910
Merge pull request #1359 from truelockmc/main
pkg(com.samsung.android.timezone.data_R): change description and removal
2026-03-25 20:54:37 +01:00
true_lock
51eb6e0a87
Restored Changes on com.samsung.android.timezone.data_R description 2026-03-25 20:20:50 +01:00
truelockmc
0363d93e89 Merge remote-tracking branch 'upstream/main' 2026-03-25 20:14:54 +01:00
Anonymoussaurus
21da7c0b27
Merge pull request #1355 from nift4/main
pkg(aosp): change removals and descriptions
2026-03-25 19:44:16 +01:00
Anonymoussaurus
9aa03efdd6
Merge pull request #1352 from Forero-0/main
pkg(samsung): add packages
2026-03-25 19:39:40 +01:00
true_lock
d2e81c6629
Update description for Samsung Galaxy A50 bootloop issue 2026-03-21 22:25:52 +01:00
true_lock
8caff5c7fd
fixed data for com.samsung.android.timezone.data_R 2026-03-21 17:36:35 +01:00
dependabot[bot]
2caad7db20
chore(deps): bump rustls-webpki from 0.103.8 to 0.103.10
Bumps [rustls-webpki](https://github.com/rustls/webpki) from 0.103.8 to 0.103.10.
- [Release notes](https://github.com/rustls/webpki/releases)
- [Commits](https://github.com/rustls/webpki/compare/v/0.103.8...v/0.103.10)

---
updated-dependencies:
- dependency-name: rustls-webpki
  dependency-version: 0.103.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-21 09:38:03 +00:00
dependabot[bot]
6047737118
chore(deps): bump tar from 0.4.44 to 0.4.45
Bumps [tar](https://github.com/alexcrichton/tar-rs) from 0.4.44 to 0.4.45.
- [Commits](https://github.com/alexcrichton/tar-rs/compare/0.4.44...0.4.45)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 0.4.45
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-20 18:12:23 +00:00
nift4
c93331e8c8 Some AOSP apps are better left alone 2026-03-20 16:05:54 +01:00
HstSethi
b9216c0120 pkg(in.gov.uidai.pehchaan) Add new Aadhaar app mandated by government to list 2026-03-20 09:31:37 +05:30
Forero-0
9f0e06774f Update uad_lists.json 2026-03-19 12:22:20 -05:00
Forero-0
513688c091 Update uad_lists.json 2026-03-18 22:09:07 -05:00
Anonymoussaurus
758a2639a1
Merge pull request #1348 from wiggleforlife/main
pkg(jp.co.omronsoft.iwnnime.ml): change to unsafe
2026-03-18 20:02:42 +01:00
Cian Ormond
a4f03d1b43
pkg(jp.co.omronsoft.iwnnime.ml): change to unsafe 2026-03-15 19:09:14 -07:00
Anonymoussaurus
da0fdd238c
Merge pull request #1334 from flojo007/main
pkg: change descriptions for soundalive & scpm
2026-03-12 14:10:36 +01:00
flojo007
5590b2f809
Update descriptions and removal recommendations 2026-03-10 19:45:22 +01:00
Ricardo Fernández Serrata
44a041bf74
Merge pull request #1285 from Universal-Debloater-Alliance/dependabot/cargo/time-0.3.47
chore(deps): bump time from 0.3.44 to 0.3.47
2026-03-09 02:25:00 -04:00
flojo007
17e5d07a84
Enhance app descriptions in uad_lists.json
Updated descriptions for Samsung Cloud Platform Manager and SoundAlive apps to include additional notes on uninstallation and potential issues.
2026-03-08 22:02:02 +01:00
Anonymoussaurus
9a6fbbd610
Merge pull request #1324 from Universal-Debloater-Alliance/inputsettings-miui-cam-ring
pkg(xiaomi): Camera Ring dependency
2026-03-08 13:14:42 +01:00
Rudxain
af50b48c68
pkg(xiaomi): Camera Ring dependency 2026-03-01 00:38:44 -04:00
Anonymoussaurus
6286b657ef
Merge pull request #1303 from pm-alonso/patch-1
pkg(com.motorola.systemserver): update description
2026-02-27 21:04:56 +01:00
Anonymoussaurus
6f4a13dbaf
Merge pull request #1310 from 4lphy/patch-1
pkg(com.samsung.android.smartmirroring): change description
2026-02-27 20:47:19 +01:00
Alperen Poyraz
3e4b8d65e9
Uad list update - uninstalling smartmirroring package breaks edge panels feature
Uninstalling this breaks edge panels feature; edge panels won't work and will also crash OneUI Home when trying to switch edge panels on from settings > display. The edge panels package (com.samsung.android.app.cocktailbarservice and com.samsung.android.app.appsedge) doesn't exist on my phone so maybe on newer devices Samsung moved that functionality to this package instead. Tested on Galaxy S25 FE Android 16.
2026-02-25 15:21:06 +03:00
pm-alonso
4fddde5cc9
Please Update uad_lists.json
This updated description will help someone like me to identify the one missing package that breaks Fast Flashlight Gesture (a very useful feature of Motorola phones indeed)
2026-02-23 23:47:20 -03:00
Anonymoussaurus
aa280b042b
Merge pull request #1300 from AstroMeYT/patch-1
pkg(com.sec.android.app.tfunlock): add package
2026-02-22 16:08:44 +01:00
AstroMeYT
718a7d32de
feat(app-description): added com.sec.android.app.tfunlock app description 2026-02-22 08:42:04 -06:00
Anonymoussaurus
a5136a6674
chore(pull-request-templates): add templates (#1283)
* chore(pull-request-template): add templates

Introduce two pull request templates to standardize contributions: .github/pull_request_template/app.md for app-related changes (description, related issues, testing matrix, checklist) and .github/pull_request_template/packages.md for package updates (description, testing, acknowledgements, package validation checklist). These templates help ensure consistent PR metadata and verification steps for maintainers and contributors.

* chore(pull-request-templates): add documentation template

Add .github/pull_request_template/documentation.md to provide a standardized pull request template for documentation updates. The template includes a Description section, Documentation Changes section, and a checklist (link to CONTRIBUTING, clarity, grammar/spelling, link verification, code example testing, and markdown formatting) to improve consistency and quality of doc PRs.

* chore(pull-request-templates): add desktop/window manager fields

Add 'Desktop Environment' and 'Window System/Manager' checklist items to the app PR template to capture more detailed testing environment information. Also adjust the ADB version line indentation to align with the checklist structure.

* chore(pull-request-templates): use absolute URL for contributing guidelines

* chore(pull-request-templates): move files to dedicated folder

* chore(pull-request-template): reword checkbox

Co-authored-by: Angel J <78835633+iamanaws@users.noreply.github.com>

---------

Co-authored-by: Angel J <78835633+iamanaws@users.noreply.github.com>
2026-02-18 10:52:31 -04:00
Anonymoussaurus
04c91de237
Merge pull request #1294 from AlekEagle/main
pkg(com.samsung.android.honeyboard): update description
2026-02-16 15:39:51 +01:00
Anonymoussaurus
36841b4f12
Merge pull request #1265 from Universal-Debloater-Alliance/pkg(oneplus/oppo/realme)-add-packages
pkg(oneplus/oppo/realme): add packages and change descriptions and removals
2026-02-16 15:35:54 +01:00
Anonymoussaurus
03bcdfbfbf
Merge pull request #1299 from Spyder-0/main
pkg(samsung): add/update packages from Samsung Galaxy S7 Edge
2026-02-16 15:35:27 +01:00
Spyder
79f48b76dc
Add/Update packages from Samsung Galaxy S7 Edge
Added new package: com.samsung.widgetapp.yahooedge.news
Updated existing package: com.microsoft.office.powerpoint
2026-02-15 14:54:49 +04:00
Leonardo Guerreiro
5ef4eeac60
Merge pull request #1293 from Lost-Entrepreneur439/main
pkg(samsung): add packages from Samsung Galaxy A20
2026-02-10 22:31:13 +00:00
Anonymoussaurus
a1f29f1f71
pkg(com.oplus.aipaint): add package 2026-02-10 13:16:59 +01:00
Alek Evans
eb3ba6b5a1
pkg(samsung): Update package description for Samsung Keyboard
Added warning for Edge panel clipboard component breaking when removing the com.samsung.android.honeyboard package.
2026-02-09 17:32:11 -09:00
Evelyn Torres
0084a70da5 pkg(samsung): add packages from Samsung Galaxy A20 2026-02-09 18:22:38 -03:30
Cosm
ebec2292ce
Merge pull request #1286 from MiguVT/patch-1
Add LineageOS Browser App details to uad_lists.json
2026-02-08 22:00:57 +00:00
Cosm
3db14bff8f
Merge pull request #1287 from toboil-features/palm-store-addition
Add notice about functionality loss (Palm Store)
2026-02-08 21:59:05 +00:00
Cosm
6e22b26b46
Merge pull request #1292 from NikolaOroz/NikolaOroz/issue1291
pkg(com.apple.atve.androidtv.appletv): add package
2026-02-08 21:57:39 +00:00
N.Oroz
0768731425 pkg(com.apple.atve.androidtv.appletv): add package 2026-02-08 12:20:02 +00:00
toboil-features
22e5afdfce
Add notice about functionality loss 2026-02-06 12:23:24 +00:00
MiguVT
55fafca735
Merge branch 'main' into patch-1 2026-02-06 10:28:32 +01:00
Anonymoussaurus
e6d44912a7 chore(issue-templates): add removal level options to issue templates
Clarify allowed Removal levels in issue templates by adding explicit options (`Recommended`|`Advanced`|`Expert`|`Unsafe`) to the Removal field. Updates .github/ISSUE_TEMPLATE/3-add-new-packages.yml and .github/ISSUE_TEMPLATE/5-update-apps-description-or-recommendation.yml to prompt contributors to use the defined removal categories from the wiki.
2026-02-06 05:07:21 -04:00
Anonymoussaurus
33f15cb6df chore(issue-templates): add checkbox for reading about removals 2026-02-06 05:07:21 -04:00
MiguVT
1c2d5ded76
Add LineageOS Browser App details to uad_lists.json
Added details for LineageOS Browser App
2026-02-05 23:47:46 +01:00
dependabot[bot]
8e7f9e2107
chore(deps): bump time from 0.3.44 to 0.3.47
Bumps [time](https://github.com/time-rs/time) from 0.3.44 to 0.3.47.
- [Release notes](https://github.com/time-rs/time/releases)
- [Changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md)
- [Commits](https://github.com/time-rs/time/compare/v0.3.44...v0.3.47)

---
updated-dependencies:
- dependency-name: time
  dependency-version: 0.3.47
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-05 18:58:49 +00:00
Cosm
9befa59723
Merge pull request #1281 from Universal-Debloater-Alliance/dependabot/cargo/bytes-1.11.1
chore(deps): bump bytes from 1.11.0 to 1.11.1
2026-02-03 20:17:51 +00:00
dependabot[bot]
70fd5be601
chore(deps): bump bytes from 1.11.0 to 1.11.1
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.11.0...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 19:42:49 +00:00
Cosm
1385d5dfb2
Merge pull request #1277 from MintJapan/docomo-pkg
pkg(docomo, samsung, felica): add docomo pkg, update FeliCa pkg
2026-02-02 15:19:19 +00:00
Cosm
351147888c
Merge pull request #1275 from Lost-Entrepreneur439/main
pkg(samsung, bell, hp): add packages from Samsung Galaxy Core LTE
2026-02-02 15:16:39 +00:00
MintJapan
f60c8982a7
pkg(docomo): fix a mistake 2026-02-01 16:28:00 +09:00
MintJapan
d8441bda83
pkg(docomo, samsung, felica): add docomo pkg, update FeliCa pkg
- add lots of docomo package (Only confirmed on SC-53E, more can be found on older devices)
- some fixes to FeliCa pkg
2026-02-01 16:10:23 +09:00
Evelyn Torres
93a71719b0 pkg(samsung, bell, hp): add packages from Samsung Galaxy Core LTE 2026-01-29 00:27:24 -03:30
Anonymoussaurus
21d71f99fc
Merge pull request #1273 from Universal-Debloater-Alliance/docs/add-appmanager-to-friends
docs(readme): add projects of Muntashir Akon to 'Friends' section
2026-01-26 17:32:41 +01:00
Anonymoussaurus
3acd2301e7
docs(readme): add projects of Muntashir Akon to 'Friends' section 2026-01-26 17:25:39 +01:00
Anonymoussaurus
8b7fe2b98a
pkg(oneplus/oppo/realme): change descriptions for OTA packages 2026-01-26 17:06:55 +01:00
Anonymoussaurus
b47f81b641
pkg(com.oplus.trafficmonitor): revert partially removed description 2026-01-25 15:31:43 +01:00
Anonymoussaurus
c6b1f44bc9
pkg(oneplus/oppo/realme): change descriptions and removal 2026-01-25 15:27:32 +01:00
Anonymoussaurus
106058826f
Merge pull request #1268 from Lost-Entrepreneur439/main
pkg(rogers): add packages from Samsung Galaxy S5 Neo
2026-01-25 13:11:42 +01:00
Ivy
7b6182722c pkg(rogers): add packages from Samsung Galaxy S5 Neo 2026-01-24 23:44:29 -03:30
Anonymoussaurus
6562bddfc1
Merge pull request #1267 from Universal-Debloater-Alliance/1263-choreissue-templates-fix-incorrect-link-removal-definitions
chore(issue-templates): add link to removal definitions
2026-01-24 23:45:11 +01:00
Anonymoussaurus
77f5c30410
chore(issue-templates): add link to removal definitions 2026-01-24 23:29:23 +01:00
Anonymoussaurus
651f424029
Merge pull request #1266 from Universal-Debloater-Alliance/1256-docsreadme-add-friendspartners-section-in-readme
docs(readme): add friends section
2026-01-24 18:03:27 +01:00
Anonymoussaurus
7a1ca4f4c2
docs(readme): add friends section 2026-01-24 16:11:06 +01:00
Anonymoussaurus
ea86289346
pkg(oneplus/oppo/realme): change descriptions and removal
Fixes #839
2026-01-24 13:46:33 +01:00
Anonymoussaurus
2f10530701
pkg(com.oplus.audio.effectcenter): change description and removal
I don't really see why "it's so bloated" if it's just an equalizer for Dolby.

Fixes #900
2026-01-24 13:36:22 +01:00
Anonymoussaurus
a0813db165
pkg(com.oppo.usercenter): add package
fixes #1071
2026-01-24 13:28:19 +01:00
Anonymoussaurus
540a123e77
pkg(com.oplus.cosa): change description and removal
Expanded the description for com.oplus.cosa to clarify its role in managing refresh rates for gaming and streaming apps, and changed its removal recommendation from 'Recommended' to 'Advanced'.
2026-01-24 13:26:25 +01:00
Anonymoussaurus
dfe9921aa0
pkg(oneplus/oppo/realme): add new AI-related packages
fixes #1146
2026-01-24 13:20:49 +01:00
Anonymoussaurus
9431c82637
pkg(lg, sprint, google): add packages from LG Nexus 5 (#1262)
* pkg(lg, sprint, google): add packages from LG Nexus 5

* remove stray newlines
2026-01-21 23:23:09 +01:00
Ivy
0154056d5e remove stray newlines 2026-01-21 16:15:37 -03:30
Den1136
9f140a4b23
pkg: add many new packages (#1180)
* Add 13 new packages from Unlisted

* add 1 more package

* Add 2 more packages

* Change descriptions of some packages

* Fix typo

* Change description for 2 packages

Change description for com.miui.miwallpaper and com.google.android.networkstack.tethering. Didn't move them.

* fix 1 package

* Add 8 new packages

* Fix a package

* Add 1 more package

* Add 3 more packages

* Add 2 more packages

* Finalize new packages (30 Packages)

* Remove unnecessary

* One more change

* Finalize edited packages

* Cover issue #1184

* Add package from issue #1197

* Squashed commit of the following:

commit d00dd63d10
Author: Rudxain <76864299+Rudxain@users.noreply.github.com>
Date:   Sun Nov 23 14:31:49 2025 -0400

    docs(README): replace Matrix logo by icon

commit a12d8412f7
Author: Rudxain <76864299+Rudxain@users.noreply.github.com>
Date:   Sun Nov 23 13:56:30 2025 -0400

    replace discord icon by SVG
    also use MD rather than HTML in README

commit 56e267c85d
Author: Rudxain <76864299+Rudxain@users.noreply.github.com>
Date:   Sun Nov 23 13:11:28 2025 -0400

    docs(CONTRIB): improve `CONTRIBUTING`

commit 944a704ee7
Author: Rudxain <76864299+Rudxain@users.noreply.github.com>
Date:   Sun Nov 23 13:11:02 2025 -0400

    docs(README): improve `README`

commit 83f9f43fde
Merge: fca7922 f62edc1
Author: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>
Date:   Tue Dec 9 13:46:31 2025 +0100

    Merge pull request #1188 from zencd/pkg/pocogames

    pkg(xiaomi): add pre-installed Xiaomi Poco games

commit f62edc16e8
Author: zzz <z@z.com>
Date:   Mon Dec 8 19:55:42 2025 +0300

    Add games to the `uad_lists.json`, pre-installed on Xiaomi Poco X7 Pro. I've uninstalled them without any problem.

commit fca7922b3c
Author: Ihor Romanyshyn <ihor.vapor.romanyshyn@gmail.com>
Date:   Tue Dec 9 14:39:05 2025 +0200

    pkg(com.taboola.mip): add package (#1186)

    * Add description for com.taboola.mip (intrusive Taboola lock-screen module)

    * remove redundant empty line

    ---------

    Co-authored-by: Ihor Romanyshyn <iromanyshyn@laptop>
    Co-authored-by: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>

commit 581f5d4d37
Author: aRav3n <24660772+aRav3n@users.noreply.github.com>
Date:   Tue Dec 9 17:21:55 2025 +0530

    Update uad_lists.json (#1178)

    Update descriptions for:
    - com.samsung.android.app.omcagent
    - de.axelspringer.yana.zeropage
    - com.samsung.android.rampart

    Change description for:
    - com.samsung.android.mapsagent

    Add package:
    - com.google.android.apps.aiwallpapers

    Credit: https://xdaforums.com/t/definitive-oneui-guide-tweaks-optimization-privacy-2025.4685716/

    Co-authored-by: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>

commit 539fa098d0
Author: Clippy <227344378+johny579@users.noreply.github.com>
Date:   Tue Dec 9 11:40:00 2025 +0000

    Merge pull request #1175 from johny579/main

    pkg(overlays): change descriptions and removals

commit bca4148fb9
Merge: 1ea4fdc 6b60ca4
Author: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>
Date:   Tue Dec 9 12:11:23 2025 +0100

    Merge branch 'Lost-Entrepreneur439-main'

commit 6b60ca4926
Merge: 1ea4fdc ddde5e6
Author: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>
Date:   Tue Dec 9 12:10:49 2025 +0100

    Merge branch 'main' of github.com:Lost-Entrepreneur439/universal-android-debloater-next-generation into Lost-Entrepreneur439-main

commit 1ea4fdca30
Merge: 9c87b56 69e63a9
Author: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>
Date:   Tue Dec 9 11:29:33 2025 +0100

    Merge pull request #1022 from Universal-Debloater-Alliance/pkg/tcl

    pkg(tcl): add and update many packs

commit ddde5e6811
Author: Ivy <78825510+Lost-Entrepreneur439@users.noreply.github.com>
Date:   Mon Dec 1 22:07:17 2025 -0330

    Chane com.ipsec.service and com.lge.shutdownmonitor to advanced.

commit e6cdca6c22
Merge: 4651fe6 1f71a85
Author: Ivy <78825510+Lost-Entrepreneur439@users.noreply.github.com>
Date:   Mon Dec 1 22:05:31 2025 -0330

    Merge branch 'main' into main

commit 69e63a9a15
Author: Rudxain <76864299+Rudxain@users.noreply.github.com>
Date:   Sat Jul 19 12:10:01 2025 -0400

    pkg(tcl): updates and adds

    mostly Iris/NXTVISION

commit 4651fe6c49
Author: Ivy <78825510+Lost-Entrepreneur439@users.noreply.github.com>
Date:   Fri Nov 21 12:26:02 2025 -0330

     pkg(lg, misc): add packages from LG Stylo 3 Plus

* Squashed commit of the following:

commit d00dd63d10
Author: Rudxain <76864299+Rudxain@users.noreply.github.com>
Date:   Sun Nov 23 14:31:49 2025 -0400

    docs(README): replace Matrix logo by icon

commit a12d8412f7
Author: Rudxain <76864299+Rudxain@users.noreply.github.com>
Date:   Sun Nov 23 13:56:30 2025 -0400

    replace discord icon by SVG
    also use MD rather than HTML in README

commit 56e267c85d
Author: Rudxain <76864299+Rudxain@users.noreply.github.com>
Date:   Sun Nov 23 13:11:28 2025 -0400

    docs(CONTRIB): improve `CONTRIBUTING`

commit 944a704ee7
Author: Rudxain <76864299+Rudxain@users.noreply.github.com>
Date:   Sun Nov 23 13:11:02 2025 -0400

    docs(README): improve `README`

commit 83f9f43fde
Merge: fca7922 f62edc1
Author: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>
Date:   Tue Dec 9 13:46:31 2025 +0100

    Merge pull request #1188 from zencd/pkg/pocogames

    pkg(xiaomi): add pre-installed Xiaomi Poco games

commit f62edc16e8
Author: zzz <z@z.com>
Date:   Mon Dec 8 19:55:42 2025 +0300

    Add games to the `uad_lists.json`, pre-installed on Xiaomi Poco X7 Pro. I've uninstalled them without any problem.

commit fca7922b3c
Author: Ihor Romanyshyn <ihor.vapor.romanyshyn@gmail.com>
Date:   Tue Dec 9 14:39:05 2025 +0200

    pkg(com.taboola.mip): add package (#1186)

    * Add description for com.taboola.mip (intrusive Taboola lock-screen module)

    * remove redundant empty line

    ---------

    Co-authored-by: Ihor Romanyshyn <iromanyshyn@laptop>
    Co-authored-by: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>

commit 581f5d4d37
Author: aRav3n <24660772+aRav3n@users.noreply.github.com>
Date:   Tue Dec 9 17:21:55 2025 +0530

    Update uad_lists.json (#1178)

    Update descriptions for:
    - com.samsung.android.app.omcagent
    - de.axelspringer.yana.zeropage
    - com.samsung.android.rampart

    Change description for:
    - com.samsung.android.mapsagent

    Add package:
    - com.google.android.apps.aiwallpapers

    Credit: https://xdaforums.com/t/definitive-oneui-guide-tweaks-optimization-privacy-2025.4685716/

    Co-authored-by: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>

commit 539fa098d0
Author: Clippy <227344378+johny579@users.noreply.github.com>
Date:   Tue Dec 9 11:40:00 2025 +0000

    Merge pull request #1175 from johny579/main

    pkg(overlays): change descriptions and removals

commit bca4148fb9
Merge: 1ea4fdc 6b60ca4
Author: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>
Date:   Tue Dec 9 12:11:23 2025 +0100

    Merge branch 'Lost-Entrepreneur439-main'

commit 6b60ca4926
Merge: 1ea4fdc ddde5e6
Author: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>
Date:   Tue Dec 9 12:10:49 2025 +0100

    Merge branch 'main' of github.com:Lost-Entrepreneur439/universal-android-debloater-next-generation into Lost-Entrepreneur439-main

commit 1ea4fdca30
Merge: 9c87b56 69e63a9
Author: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>
Date:   Tue Dec 9 11:29:33 2025 +0100

    Merge pull request #1022 from Universal-Debloater-Alliance/pkg/tcl

    pkg(tcl): add and update many packs

commit ddde5e6811
Author: Ivy <78825510+Lost-Entrepreneur439@users.noreply.github.com>
Date:   Mon Dec 1 22:07:17 2025 -0330

    Chane com.ipsec.service and com.lge.shutdownmonitor to advanced.

commit e6cdca6c22
Merge: 4651fe6 1f71a85
Author: Ivy <78825510+Lost-Entrepreneur439@users.noreply.github.com>
Date:   Mon Dec 1 22:05:31 2025 -0330

    Merge branch 'main' into main

commit 69e63a9a15
Author: Rudxain <76864299+Rudxain@users.noreply.github.com>
Date:   Sat Jul 19 12:10:01 2025 -0400

    pkg(tcl): updates and adds

    mostly Iris/NXTVISION

commit 4651fe6c49
Author: Ivy <78825510+Lost-Entrepreneur439@users.noreply.github.com>
Date:   Fri Nov 21 12:26:02 2025 -0330

     pkg(lg, misc): add packages from LG Stylo 3 Plus

* Edit list descs (approval required)

I wanted to change the description of Unsafe long ago

* Add 2 overlays for com.android.wifi.resources

One more package left undocumented but i have to figure out its purpose first

* Add 1 more package

* Add 1 more package; change 2 packages

* Add 1 package, change 2 packages

* Edit list descs (APPROVAL REQUIRED)

* typo

* Add 1 important package

* Add 10 new packages from #1211

* pkg: fix missing comma in JSON

* Update uad_lists.json

* Update .github/ISSUE_TEMPLATE/3-add-new-packages.yml

Co-authored-by: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>

* #1211 package placeholder for simplicity in my next PR (APPROVAL REQUIRED)

* fix attempt

* fix attempt 2

* Fix attempt 3

* Fix attempt 4

* Fix attempt 5

* Test

* Cancel template

* Correct packages for comments

Finally figured it out

* pkg(com.google.android.health.connect.backuprestore): change removal

* pkg(com.google.android.healthconnect.controller): change removal

* pkg(com.android.healthconnect.overlay): change removal

* SarReduce update

* Fix \n

* Delete resources/assets/uad_lists (1).json

* Fix

---------

Co-authored-by: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>
2026-01-21 11:19:23 +01:00
Ivy
e494a45717 pkg(lg, sprint, google): add packages from LG Nexus 5 2026-01-21 05:53:28 -03:30
Frigyes
435c68dca8
Merge pull request #1260 from Universal-Debloater-Alliance/clippy-allow-noop
chore(cargo-clippy): rm no-op lint line
2026-01-20 08:59:23 -08:00
Rudxain
40e5281ee0 chore(cargo-clippy): rm no-op lint line 2026-01-17 10:11:02 -04:00
Cosm
d288860ac8
Merge pull request #1257 from Cosm-gedenke/package/com.coloros.widget.smallweather
pkg(com.coloros.widget.smallweather): add description and change removal
2026-01-15 19:39:31 +01:00
Anonymoussaurus
ee1068ddf8
Merge pull request #1258 from Universal-Debloater-Alliance/1251-cirelease-move-packages-to-bottom-of-release
ci(release): move packages to bottom of release
2026-01-15 16:39:37 +01:00
Anonymoussaurus
0a60b713db
ci(release): move packages to bottom of release 2026-01-15 16:34:09 +01:00
Anonymoussaurus
9ba456884c
Merge pull request #1222 from Universal-Debloater-Alliance/mailmap
chore(git): add `.mailmap`
2026-01-14 19:00:44 +01:00
Anonymoussaurus
00b242368d
Merge pull request #1253 from Cosm-gedenke/changed_minor_uad_list
pkg: change descriptions and removals
2026-01-13 16:16:16 +01:00
ist1114011
99fb08e9d6 whoops 2026-01-13 15:01:38 +00:00
ist1114011
f92b806002 (pkg): fixed some documentation according to previous PR and new findings 2026-01-13 14:57:36 +00:00
Anonymoussaurus
0f857fb92f
Merge pull request #1250 from chu121su12/patch-2
pkg(com.miui.misound): update description
2026-01-12 14:39:33 +01:00
Saya
4eda9c8c1c
Update uad_lists.json 2026-01-12 19:43:47 +08:00
Rudxain
9f5e134d0a chore(git): add .mailmap 2026-01-11 09:04:00 -04:00
51 changed files with 5851 additions and 1317 deletions

View file

@ -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:

View file

@ -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
View 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

View 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

View 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
View 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
View file

@ -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

View file

@ -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-*

View file

@ -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
View 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

File diff suppressed because it is too large Load diff

View file

@ -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 }

View file

@ -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
View 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 }

View 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"

View 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());
}

View 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,
})
}
}

View 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
View 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(())
}

View 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
View 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");
}

View 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 }

View file

@ -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);
}
}
}

View file

@ -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"));
}

View 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")));

View file

@ -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!(

View file

@ -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

View file

@ -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",

View file

@ -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,

View file

@ -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
View 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 }

View file

@ -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()
})

View 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
View 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;

View file

@ -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()

View file

@ -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);

View file

@ -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 {

View file

@ -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 {}

View file

@ -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> {

View file

@ -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),

View file

@ -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),

View file

@ -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>,

View file

@ -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,
}
}
}

View file

@ -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
View file

@ -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": {

View file

@ -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

View file

@ -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)
}

View file

@ -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;