Compare commits

...

473 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
Anonymoussaurus
dcaf90396e
Merge pull request #1248 from Universal-Debloater-Alliance/doc-readme-start
docs(readme): link to Getting-started
2026-01-10 20:40:18 +01:00
Anonymoussaurus
c4af1434c0
Merge pull request #1247 from Universal-Debloater-Alliance/pkg-samsung-talkback
pkg(samsung): add TalkBack link
2026-01-10 20:39:38 +01:00
Anonymoussaurus
91e77493ee
Merge pull request #1246 from Universal-Debloater-Alliance/pkg-typos
pkg: fix some typos
2026-01-10 20:38:22 +01:00
Rudxain
9cc69e6a7d pkg(samsung): TalkBack link 2026-01-10 12:02:00 -04:00
Rudxain
ec5a98109d pkg: fix some typos 2026-01-10 12:01:49 -04:00
Rudxain
52eaead284 docs(README): link to Getting-started 2026-01-10 12:00:22 -04:00
Anonymoussaurus
686b41084e
Merge pull request #1245 from Universal-Debloater-Alliance/doc-adb-pack-url
docs: add proper source for package-ID format
2026-01-10 16:01:15 +01:00
Rudxain
0a81642567 docs: add proper source for package-ID format 2026-01-10 07:44:24 -04:00
Ricardo Fernández Serrata
94d8321e2b
Merge pull request #1242 from lawson85/main
Simplify keyboard shortcut code
2026-01-10 06:14:14 -04:00
Jacob L
424ddeb522 Simplify keyboard shortcut code
Ran cargo fmt as well
2026-01-10 09:59:48 +00:00
Jacob L
9f4327aa4b
Merge pull request #1220 from lawson85/Add-keyboard-shortcuts
feat: add keyboard shortcuts
2026-01-09 20:32:29 +01:00
Cosm
b82c71e1c5
Merge pull request #1240 from Cosm-gedenke/chore/rfd
(chore): bump rfd to 0.17.1
2026-01-09 12:06:30 +00:00
Cosm
76e54c63f8
Merge branch 'main' into chore/rfd 2026-01-09 12:02:45 +00:00
ist1114011
ba8e6ee472 (chore): bump rfd to 0.17.1 2026-01-09 11:57:00 +00:00
Anonymoussaurus
b05ebbf603
Merge pull request #1239 from Universal-Debloater-Alliance/fix/windows-application-has-incorrect-title
fix(window): incorrect title of UAD-ng
2026-01-09 12:23:11 +01:00
Anonymoussaurus
6867c56dae
fix(window): incorrect title of UAD-ng
Cause: iced update to 0.14
2026-01-09 11:53:10 +01:00
Anonymoussaurus
2d4789cb68
Merge pull request #1238 from Universal-Debloater-Alliance/1237-docsreadme-remove-line-numbers-referencing-code
docs(readme): remove line numbers
2026-01-09 11:13:42 +01:00
Anonymoussaurus
1dd2fc5ec1
docs(readme): remove line numbers
Because line numbers can change, it's hard to reference, see https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/pull/1231#discussion_r2672430419.
2026-01-09 11:04:31 +01:00
Ricardo Fernández Serrata
38627b716f
Merge pull request #1235 from Universal-Debloater-Alliance/1024-cireleases-mention-that-packages-are-updated-upon-launch-uad-ng
ci(releases): add note about package list updates
2026-01-08 09:58:41 -04:00
Anonymoussaurus
5dd2d52bbb
ci(releases): add note about package list updates
Added a note to the release workflow clarifying that package list updates do not require a new app version, as UAD-ng fetches the latest package definitions on launch if connected to the internet.
2026-01-08 14:00:11 +01:00
Anonymoussaurus
74575c5541
Merge pull request #1234 from Universal-Debloater-Alliance/1018-choreissue-templates-encourage-users-to-create-pr-from-issue-regarding-packages
chore(issue-templates): encourage PRs in issue templates and add checklist items
2026-01-08 12:39:52 +01:00
Anonymoussaurus
fe238d0e2d
Merge pull request #1233 from Universal-Debloater-Alliance/920-docscontributing-clarify-usage-of-issue-labels
docs(contributing): add issue label and changelog section
2026-01-08 12:39:37 +01:00
Anonymoussaurus
634c91d1db
Merge pull request #1231 from Universal-Debloater-Alliance/863-docsreadme-put-note-that-we-do-not-collect-nor-store-any-telemetry
docs(readme): add privacy section
2026-01-08 12:39:15 +01:00
Anonymoussaurus
a2d112392a
chore(issue-templates): encourage PRs in issue templates and add checklist items
Added sections to issue templates encouraging users to submit Pull Requests and linked to the contribution guide. Also introduced new checklist options for users to indicate willingness to submit a PR, aiming to speed up processing of package-related issues and documentation updates.
2026-01-08 12:19:37 +01:00
Anonymoussaurus
841f0ee6b2
docs(contributing): add issue label and changelog section
Expanded CONTRIBUTING.md with a section describing issue labels, their purposes, and how they map to changelog categories. This helps contributors categorize issues and understand how labels affect release notes.
2026-01-08 12:05:38 +01:00
Anonymoussaurus
c598e4cf9b
docs(readme): fix formatting in documentation list
Added a missing newline before the documentation list in README.md for improved readability.
2026-01-08 11:47:54 +01:00
Anonymoussaurus
cbc0496577
docs(readme): add privacy section
Added a new section detailing that UAD-ng does not collect or transmit user data, and specifying the only external connections made are to GitHub for package lists and updates.
2026-01-08 11:46:39 +01:00
Frigyes
050c8c371e
Merge pull request #1230 from Universal-Debloater-Alliance/dependabot/cargo/lru-0.16.3
chore(deps): bump lru from 0.16.2 to 0.16.3
2026-01-08 09:53:45 +00:00
dependabot[bot]
4832da0f97
chore(deps): bump lru from 0.16.2 to 0.16.3
Bumps [lru](https://github.com/jeromefroe/lru-rs) from 0.16.2 to 0.16.3.
- [Changelog](https://github.com/jeromefroe/lru-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jeromefroe/lru-rs/compare/0.16.2...0.16.3)

---
updated-dependencies:
- dependency-name: lru
  dependency-version: 0.16.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-07 20:49:05 +00:00
Anonymoussaurus
0c80fc9392
Merge pull request #1229 from Universal-Debloater-Alliance/chore(ci)/update-macos-runner-versions
build(ci): bump MacOS runner versions
2026-01-07 19:19:29 +01:00
Frigyes Erdosi Szucs
bf57e9804d
chore(ci): change latest to 15 2026-01-07 10:13:51 -08:00
Frigyes Erdosi Szucs
60fb42c5e7
chore(ci): standardize macos capitalization to line up with GH runners 2026-01-07 04:26:12 -08:00
Frigyes Erdosi Szucs
a68e391bb6
chore(c): bumped runner versions 2026-01-07 04:20:52 -08:00
Frigyes
9d147a691e
Merge pull request #1227 from Universal-Debloater-Alliance/chore/prepare-v1.2.0-release
chore(release): bump version to v1.2.0
2026-01-07 12:01:00 +00:00
Frigyes Erdosi Szucs
65a9bf9efe chore: regenerate lockfile 2026-01-06 11:52:22 -08:00
Frigyes Erdosi Szucs
2eff902f69 chore: Bump UAD-ng version in cargo.toml 2026-01-06 11:52:06 -08:00
Frigyes
61c08773bf
Merge pull request #1221 from Cosm-gedenke/main
fix: Fixed shell issues for packages that can't be enabled
2026-01-06 19:46:54 +00:00
ist1114011
67dbb137c9 fixed clippy issues 2026-01-01 16:48:50 +00:00
ist1114011
a3d4412c8a fixed formatting issues 2026-01-01 16:41:22 +00:00
ist1114011
9c29d6c408 fixed formatting issues
rebasing
2026-01-01 16:37:13 +00:00
Cosm
d839546612
Update list.rs
simple fix, should not break
2025-12-31 20:55:13 +00:00
ist1114011
cf74cead29 fixed shell issues for packages that can't be enabled 2025-12-31 20:10:03 +00:00
Anonymoussaurus
3a0529300a
Merge pull request #1218 from Lkaoof/com.huawei.imedia.sws-description-update
pkg(com.huawei.imedia.sws): update description
2025-12-30 12:31:42 +01:00
Lkaoof
6aa8c42487 pkg(com.huawei.imedia.sws): update description 2025-12-30 13:55:25 +03:00
Anonymoussaurus
f7a37df382
Merge pull request #1214 from Universal-Debloater-Alliance/fix/gui-visual-glitch
fix: resize boxes to fit text without visual weirdness
2025-12-28 12:48:40 +01:00
Frigyes Erdosi Szucs
190967fd12 fix formatting 2025-12-27 21:43:07 -08:00
Frigyes Erdosi Szucs
f7ca723c83 Resize boxes to fit text without visual weirdness 2025-12-27 21:41:30 -08:00
Anonymoussaurus
8c77d463d9
Merge pull request #1207 from Universal-Debloater-Alliance/docs/contrib/big
docs(contributing): rules for mass-edits
2025-12-27 19:51:00 +01:00
Rudxain
3fd3fe334e
docs(contrib): elaborate on AI 2025-12-27 14:15:56 -04:00
Ricardo Fernández Serrata
7dec7926d4 docs(contrib): rm clipboard-emoji
Co-authored-by: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>
2025-12-27 14:03:30 -04:00
Anonymoussaurus
d903483f46 docs(contributing): fix markdownlint 2025-12-27 14:03:30 -04:00
Rudxain
929dfba58d docs(contrib): rules for mass-edits 2025-12-27 14:03:30 -04:00
Anonymoussaurus
c90bd4cdd7
Merge pull request #1196 from iamanaws/deps/iced-14
chore(deps): bump iced from 0.13.1 to 0.14.0
2025-12-25 13:08:56 +01:00
angel
a5c18ee295 chore(deps): bump iced from 0.13.1 to 0.14.0 2025-12-21 20:37:19 -08:00
angel
fdca1495f8 refactor(nix): add macOS 2025-12-21 20:37:19 -08:00
Anonymoussaurus
5a8d1faa5a
Merge pull request #1201 from orianodev/patch-1
pkg(miui.systemui.plugin): added warning
2025-12-21 02:54:44 +01:00
Mecatroniq
0d82aa1c11
pkg(miui.systemui.plugin): added warning
Warning for critical UI breakage concerning Redmi devices.
2025-12-18 13:15:10 -05:00
Rudxain
d00dd63d10 docs(README): replace Matrix logo by icon 2025-12-17 03:41:59 -04:00
Rudxain
a12d8412f7 replace discord icon by SVG
also use MD rather than HTML in README
2025-12-17 03:41:59 -04:00
Rudxain
56e267c85d docs(CONTRIB): improve CONTRIBUTING 2025-12-17 03:41:59 -04:00
Rudxain
944a704ee7 docs(README): improve README 2025-12-17 03:41:59 -04:00
Anonymoussaurus
83f9f43fde
Merge pull request #1188 from zencd/pkg/pocogames
pkg(xiaomi): add pre-installed Xiaomi Poco games
2025-12-09 13:46:31 +01:00
zzz
f62edc16e8 Add games to the uad_lists.json, pre-installed on Xiaomi Poco X7 Pro. I've uninstalled them without any problem. 2025-12-09 13:42:16 +01:00
Ihor Romanyshyn
fca7922b3c
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>
2025-12-09 13:39:05 +01:00
aRav3n
581f5d4d37
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>
2025-12-09 12:51:55 +01:00
Clippy
539fa098d0
Merge pull request #1175 from johny579/main
pkg(overlays): change descriptions and removals
2025-12-09 12:40:00 +01:00
Anonymoussaurus
bca4148fb9
Merge branch 'Lost-Entrepreneur439-main' 2025-12-09 12:11:23 +01:00
Anonymoussaurus
6b60ca4926
Merge branch 'main' of github.com:Lost-Entrepreneur439/universal-android-debloater-next-generation into Lost-Entrepreneur439-main 2025-12-09 12:10:49 +01:00
Anonymoussaurus
1ea4fdca30
Merge pull request #1022 from Universal-Debloater-Alliance/pkg/tcl
pkg(tcl): add and update many packs
2025-12-09 11:29:33 +01:00
Anonymoussaurus
9c87b56bb3
Merge pull request #1185 from hstsethi/ss
pkg(com.dot.app.sancharsaathi): Add Sanchar Saathi App
2025-12-02 15:26:53 +01:00
Harshit Sethi
5b2258661d
pkg(com.dot.app.sancharsaathi): Add Sanchaar Saathi App 2025-12-02 18:34:31 +05:30
Ivy
ddde5e6811
Chane com.ipsec.service and com.lge.shutdownmonitor to advanced. 2025-12-01 22:07:17 -03:30
Ivy
e6cdca6c22
Merge branch 'main' into main 2025-12-01 22:05:31 -03:30
Ricardo Fernández Serrata
1f71a85071
Merge pull request #1167 from iamanaws/fix/core-hardening
fix(core): add package state verification and cross-user detection
2025-11-27 11:31:24 -04:00
Rudxain
69e63a9a15
pkg(tcl): updates and adds
mostly Iris/NXTVISION
2025-11-25 14:16:56 -04:00
Anonymoussaurus
e73021eb20
Merge pull request #1174 from Scott-Nx/main
pkg(com.xiaomi.finddevice): update description
2025-11-24 18:47:47 +01:00
Scott-Nx
d876382ae7
pkg(com.xiaomi.finddevice): update description 2025-11-22 23:03:30 +07:00
Ivy
4651fe6c49
pkg(lg, misc): add packages from LG Stylo 3 Plus 2025-11-21 12:26:02 -03:30
iamanaws
07ac838964 chore: fix and apply copilot suggestions 2025-11-20 21:46:19 -08:00
iamanaws
7c641bf258 chore(lint): fix clippy warnings 2025-11-20 20:52:31 -08:00
iamanaws
e184121f5f fix: detect and handle cross-user app restoration behavior
- Add cross-user behavior detection for unexpected restoration patterns
- Fix user isolation to prevent operations on unintended users
- Handle uninstall→restore, uninstall→uninstall, and restore→restore cases
- Prevent ping-pong restoration loops from OEM multi-user implementations

Related to #507
2025-11-20 20:52:31 -08:00
iamanaws
88997bdccc fix(adb): prevent crashes from non-UTF8 output
Use lossy UTF-8 conversion in ADB operations and add user-friendly error messages for common OEM failures (Samsung Knox, work profiles, permission errors).

Closes #1005
Merged from #1024

Co-authored-by: simplehima <simplehima@users.noreply.github.com>
2025-11-20 20:52:31 -08:00
iamanaws
7a15e18844 feat(pkg-state): add fallback verification and disable on reinstall
- Verify package state after operations via ADB commands
- Fallback to disable when uninstall fails due to reinstallation
- Fallback to uninstall when disable fails due to re-enabling
- Add UI notifications to inform users of fallback actions
- Remove unused ChangePackageState message variant

Fixes #632
2025-11-20 20:52:31 -08:00
Anonymoussaurus
93a5e56813
pkg(com.aura.oobe.samsung): change description (#1165)
* Enhance AppCloud description with more details

Updated the description for AppCloud in uad_lists.json to include additional information about IronSource and its ownership by Unity, along with app analysis links for reference.

* Add Knox Admin Documentation
2025-11-19 17:37:42 +01:00
iamanaws
ebcaf5e31f fix(backup): continue restoration when packages are missing from device
Skip missing packages with warning instead of failing entire restore process.
Fixes #538
2025-11-18 18:17:18 -08:00
Ricardo Fernández Serrata
dbd33f2cdb
Merge pull request #1064 from iamanaws/iced-13
chore(deps): bump iced from 0.12.0 to 0.13.1
2025-11-18 21:20:02 -04:00
shuvashish76
aee17d7f6a
Add Knox Admin Documentation 2025-11-18 03:39:55 +00:00
shuvashish76
be5e421e81
Enhance AppCloud description with more details
Updated the description for AppCloud in uad_lists.json to include additional information about IronSource and its ownership by Unity, along with app analysis links for reference.
2025-11-18 02:17:01 +00:00
Murilo Bob
553656e5fa
Merge pull request #1158 from Universal-Debloater-Alliance/pkg/blackview
pkg(blackview): add packages
2025-11-17 19:21:11 +01:00
iamanaws
7580105910 chore(lint): solve clippy warnings 2025-11-17 07:24:07 -08:00
iamanaws
4ab886490e chore: replace deprecated adb_shell_command with run_adb_action 2025-11-17 07:15:18 -08:00
iamanaws
369cfaa946 chore(deps): bump iced from 0.12.0 to 0.13.1 2025-11-17 07:13:03 -08:00
iamanaws
ef3803eba5 chore: update flake.nix and flake.lock 2025-11-17 07:02:46 -08:00
Anonymoussaurus
e6d672f817
Update uad_lists.json (#1163)
Add three new packages
2025-11-17 11:26:22 +01:00
Jacob L
6fdda4133e
pkg(com.android.bips): update description (#1155)
* (pkg): default print service, disabling can crash settings app in some cirumstances

Updated the description for the com.android.bips entry to include potential crash issues with the Settings app on specific devices.

* Enhance description for com.android.bips entry

Updated the description for the com.android.bips entry to include information about general device slowness.

* Change BIPS removal label from 'Advanced' to 'Expert'
2025-11-16 23:11:11 +01:00
Den1136
3f0f0684e2
Update uad_lists.json
Add three new packages
2025-11-16 20:51:20 +02:00
foot fondler
e430a6d2da
pkg(androidtv, blackview, bluestacks, google, samsung, unisoc, waydroid): add packages (#1154)
* Android TV; Blackview; Google; and Samsung.

* Update uad_lists.json

* Update uad_lists.json

* Change removal and description

com.android.dialer Advanced → Expert
com.google.android.marvin.talkback Recommended → Advanced
com.android.bluetoothmidiservice Recommended → Advanced

* Update uad_lists.json

* Change Google Speech Services to recommended

* Changed some apps from Recommended to Advanced

com.android.keyguard Recommended → Advanced
com.huawei.ohos.inputmethod Recommended → Advanced
com.samsung.android.net.wifi.wifiguider Recommended → Advanced

* Delete empty line

* Added emulator apps to close #1145

* Update uad_lists.json

* Update uad_lists.json

* Update uad_lists.json

* Update uad_lists.json

* Update uad_lists.json

* 16 apps added

* Update uad_lists.json

* Update uad_lists.json

* Update uad_lists.json

* 4 Japanese carrier apps

* Update uad_lists.json

---------

Co-authored-by: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>
2025-11-16 14:07:24 +01:00
Anonymoussaurus
c197541964
Merge pull request #1157 from Universal-Debloater-Alliance/pkg/xiaomi
pkg(com.xiaomi.aicr): change description and removal
2025-11-16 11:54:10 +01:00
QQ喵
6594a15442
pkg(OnePlus) Make it explicit that com.oplus.athena kills apps (#1156)
* Make it explicit that athena kills apps

* Add a reference link
2025-11-16 11:52:54 +01:00
Anonymoussaurus
d29e927856
Merge pull request #1045 from Universal-Debloater-Alliance/ci/fix-lints-tests
ci: fix lints, disable non-determ tests
2025-11-16 11:39:27 +01:00
Muriilo
337d5e5d96 pkg(Xiaomi) Changing package recommended and description 2025-11-14 17:46:46 -03:00
Anonymoussaurus
f0655e7bd6
Merge pull request #1152 from lawson85/patch-2
pkg(com.aura.oobe.deutsche): add package
2025-11-12 20:23:06 +01:00
Anonymoussaurus
07807130ab
Merge branch 'main' into pr/1152 2025-11-12 20:20:16 +01:00
Anonymoussaurus
6eb6df63b3
Merge pull request #1149 from Universal-Debloater-Alliance/pkg/android
pkg(com.android.angle): change description and removal
2025-11-12 20:15:02 +01:00
Anonymoussaurus
877df6b75e
Merge pull request #1148 from Universal-Debloater-Alliance/pkg/seedvault
pkg(com.stevesoltys.seedvault): change description and removal
2025-11-12 20:09:14 +01:00
Anonymoussaurus
7ccb17c886
pkg(Samsung, Telus): add packages from Galaxy S6 (#1141) 2025-11-12 20:08:08 +01:00
Den1136
51455899dd
pkg(com.android.overlay.circletosearch): add package (#1151)
* Update uad_lists.json

Package addition (com.android.overlay.circletosearch)

* Update uad_lists.json

Edited description
2025-11-12 16:29:25 +01:00
Jacob L
bde5f2af70
Add entry for com.aura.oobe.deutsche in uad_lists.json
Added a new entry for 'com.aura.oobe.deutsche' with details about its list, description and removal recommendation.
2025-11-09 13:58:23 +00:00
Muriilo
7d84630e5c pkg(android angle) Updating the description and changing the removal (#857) 2025-11-07 23:24:25 -03:00
Muriilo
2211d1bf97 pkg(seedvault) Adding a better description and changing the removal tag 2025-11-07 23:00:01 -03:00
Jacob L
9e26136cb3
pkg(overlays): fix package name typo and include adb command for additional information (#1143)
* Enhance overlay descriptions with adb command info

Updated descriptions for various overlays to include adb command for additional information.

* Fix JSON formatting in uad_lists.json

Removing extra "

* Fix formatting of description in uad_lists.json

* Fix parse errors

* Fix more parse errors

Updated descriptions to empty strings for various overlays.

* Trying to fix more parse errors

* Fix JSON formatting
2025-11-05 01:47:29 +01:00
Ivy
621c650d84
pkg(Samsung, Telus): add packages from Galaxy S6 2025-11-02 15:07:48 -03:30
Murilo Bob
141c8df7b4
pkg(Samsung) Add packages (#1133)
Co-authored-by: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>
2025-11-02 16:15:25 +01:00
Ivy
8917703189
Merge pull request #1137 from Lost-Entrepreneur439/main
pkg(samsung): add Bell Canada packages
2025-11-02 16:07:57 +01:00
Anonymoussaurus
a1135dd1ce
Merge pull request #1139 from johny579/main
pkg(google, samsung, unisoc): add packages
2025-11-02 16:00:50 +01:00
foot fondler
8db8bcfcb2
Update uad_lists.json 2025-11-02 13:14:11 +00:00
foot fondler
a5508bb4ec
Update uad_lists.json 2025-11-02 13:12:29 +00:00
Anonymoussaurus
78165eed83
Merge pull request #1134 from Universal-Debloater-Alliance/pkg/Russia
pkg(yandex, vk): add Russian packages
2025-11-02 12:29:17 +01:00
Murilo Bob
58606e31dd
pkg(bigme): add packages (#1129)
Co-authored-by: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>
2025-11-02 12:22:23 +01:00
Jacob L
2f3016537f
pkg(Sony, Qualcomm, AOSP): add packages (#1098)
* Added Sony's unlisted and some other OEM/AOSP to uad_lists.json

Added packages from Sony Xperia X, F5121.
Added some Qualcomm, AOSP packages that were unlisted

* Fix missing bracket and . instead of - uad_lists.json

* Remove extra space in uad_lists.json

Hoping to pass the checks. I get error: Error: Duplicate key 'com.sonyericsson.android.camera' when using online json validator

* Remove duplicateuad_lists.json

I think a duplicate caused an issue

* Removed another duplicate from uad_lists.json

"com.sonyericsson.android.camera"

* Changed oem to Oem uad_lists.json

* Update uad_lists.json

Still trying to fix errors

* Attempt to fix Oem vs oem problem in uad_lists.json

All instances of Oem were estored to oem and then Notepad++ located instances of "list": "oem" and changed to "list": "Oem"

* oem -> OEM uad_lists.json

* Trying again to fix oem

* Update removal labels and descriptions to reflect requested changes
2025-11-02 12:20:04 +01:00
Rudxain
f956c5d452
docs(main): rm redundant doc-comment
this fixes `unused-doc-comments` lint warn
2025-10-31 19:21:45 -04:00
Rudxain
15fcd106f7
ci(cargo-check): deny warns 2025-10-31 19:08:35 -04:00
Rudxain
d74a30b194
Revert "ci(cargo): replace check by clippy"
This reverts commit 38554ca2d7.
2025-10-31 18:46:27 -04:00
Rudxain
ab3081b11d
style(gui/views/list): pkg->p rename, to not shadow pkg 2025-10-31 17:41:09 -04:00
Rudxain
fb131df206
ci(adb): use debug_assert to not trigger panic_in_result_fn 2025-10-31 17:41:09 -04:00
Rudxain
0dc61c825f
refactor(adb): comment-out all running (user) stuff 2025-10-31 17:41:08 -04:00
Rudxain
956d64aa64
refactor(gui/views/list): inline delay_hide_copy_confirmation
fn added in 2ca3e05a8e
2025-10-31 17:41:08 -04:00
Rudxain
6a8ac88b67
ci: disable test_save_changes 2025-10-31 17:41:07 -04:00
Rudxain
7477f02f83
refactor: rm PickList (unused) 2025-10-31 17:41:07 -04:00
Rudxain
3fabd4a8d0
refactor(gui/views/list): mark unused var
lint regression at 26afc32b69
2025-10-31 17:41:07 -04:00
Rudxain
760b21da09
ci(clippy): rm some lints
- `exit` isn't bad for apps, only for libs
- `mem_forget` is rarely a mistake, and is trivial to catch
- `string_to_string` is superseded by `implicit_clone`
- `allow_attributes_without_reason` can be catched by reviewers
2025-10-31 17:41:06 -04:00
Rudxain
d383da804d
ci: cargo clippy --fix; cargo fmt 2025-10-31 17:41:06 -04:00
Rudxain
54308e1c58
ci(adb-sync): un-deprecate adb_shell_command; add warn to docs 2025-10-31 17:41:05 -04:00
Rudxain
2729c72c76
ci: rm config.devices.len() assertion 2025-10-31 17:41:05 -04:00
Muriilo
840bc3d408 pkg(Russian) Adding some Russian packages 2025-10-29 18:21:40 -03:00
foot fondler
4cd0936596
pkg: add bloatware (#1127)
* Added more bloatware

* Added more bloatware

* Add commas

* Update uad_lists.json
2025-10-28 11:08:09 +01:00
MintJapan
9c63d20643
Merge pull request #1126 from MintJapan/FeliCa-pkg
pkg(felica, samsung): adds and update FeliCa & Osaifu-Keitai packages
2025-10-27 11:43:12 +01:00
Anonymoussaurus
b5d655550f
Merge pull request #1121 from Iamanaws/uad_lists
pkg(samsung): add packages
2025-10-26 21:44:39 +01:00
iamanaws
7368c5de47 pkg(samsung, oem): add packages
- pkg(com.samsung.android.knox.*): add Knox enterprise/security packages
- pkg(com.samsung.android.app.vex.scanner): add Document Scanner
- pkg(com.samsung.android.motionphoto.app): add Motion Photo
- pkg(com.samsung.android.wifi.resources.wpa2): add WiFi resources
- pkg(com.monotype.android.font.roboto): add Roboto Font
- pkg(com.val.hardware): add hardware diagnostic tool
2025-10-26 13:41:36 -07:00
Anonymoussaurus
3285ddac66
pkg(nuu): add packages (#913)
* pkg(nuu): add packages

* Apply suggestions from code review
2025-10-26 14:16:41 +01:00
Anonymoussaurus
93280a65aa
Merge pull request #1120 from Universal-Debloater-Alliance/pkg/google
pkg(google): add packages
2025-10-26 12:51:56 +01:00
Anonymoussaurus
f91f135c0e
Merge pull request #1122 from hydrospherics/main
pkg(com.coloros.karaoke): improve description
2025-10-26 12:48:58 +01:00
Anthony L.
aa0f168f9d
corrected `com.coloros.karaoke`'s description. 2025-10-26 07:04:30 +08:00
Anthony L.
fd07a012ca
corrected `com.coloros.karaoke`'s description. 2025-10-26 06:58:17 +08:00
Muriilo
d3e833b14e feat(json documentation) Adding documentation about google apps 2025-10-24 21:34:31 -03:00
Anonymoussaurus
c97ed5dcc1
Merge pull request #1118 from Muriilow/main
pkg(oplus): add com.oplus.studycenter
2025-10-24 17:27:31 +02:00
Murilo Bob
af64ee4a12
Merge branch 'Universal-Debloater-Alliance:main' into main 2025-10-24 12:16:07 -03:00
Muriilo
2ce14936fb feat(json documentation) Adding documentation 2025-10-24 12:14:28 -03:00
Anonymoussaurus
1fcdcaa239
Merge pull request #1117 from Muriilow/main
pkg: add many packages
2025-10-24 16:40:37 +02:00
Muriilo
5e9c1c31d3 feat(json documentation) Adding more packages 2025-10-24 10:21:04 -03:00
Muriilo
b8402242cb feat(json documentation) Adding more documentation 2025-10-24 10:10:58 -03:00
Anonymoussaurus
9b64a5d685
Adding packages preinstalled on Inkbook Devices Closes #811 (#1113) 2025-10-22 11:00:33 +02:00
Muriilo
3932ca66f2 Adding packages preinstalled on Inkbook Devices Closes #811 2025-10-18 18:32:32 -03:00
Ferdinand Saurenbach
bc160c5685
Merge pull request #1105 from FSaurenbach/patch-1
pkg(com.google.android.apps.bard): add package
2025-10-10 10:07:41 +02:00
Anonymoussaurus
9c1ab60eeb
Merge pull request #1086 from d22592/main
pkg(samsung, lge, oplus): add packages
2025-09-29 12:22:39 +02:00
Anonymoussaurus
7f76ba0fdc
Merge pull request #1097 from Lost-Entrepreneur439/main
move com.amazon.venezia to advanced
2025-09-29 10:04:24 +02:00
Ivy
e1c77a4668
move com.amazon.venezia to advanced
Due to it breaking the app drawer on Fire TV, I'd put it as advanced, as it renders the device a lot harder to use unless an alternative launcher is installed.
2025-09-28 21:35:34 -02:30
Ricardo Fernández Serrata
88fc302c05
Merge pull request #1063 from Iamanaws/uad_lists
UPDATED: pkg(samsung, cyrus, google): add packages
2025-09-17 15:10:56 -04:00
Anonymoussaurus
125ef7c820
Merge pull request #1092 from NishchayKQ/patch-1
pkg(com.google.android.overlay.gmsconfig.searchselector): change removal and description
2025-09-15 07:58:36 +02:00
NishchayKQ
f29afcba6c
pkg(com.google.android.overlay.gmsconfig.searchselector): change removal and description 2025-09-15 08:16:04 +05:30
d22592
26c7be73bd
Update uad_lists.json 2025-09-11 22:12:07 +00:00
Ricardo Fernández Serrata
72835c67cd
Merge pull request #1074 from ShahrukhJ/patch-1
pkg(Xiaomi): add App Finder
2025-09-07 18:11:24 -04:00
Name
1852d04284
Update uad_lists.json
UnListed Oem App updated (Xaiomi Poco)
2025-09-01 05:11:18 +00:00
iamanaws
d1db1dd4a4 fix entries Advanced -> Expert, fix formatting 2025-08-23 14:45:45 -07:00
Scharada
ff0566ca42 adding format of Google Pixel 8 missing Packages 2025-08-23 14:28:01 -07:00
Scharada
e87e2aa5c7 adding Google Pixel 8 missing Packages 2025-08-23 14:28:01 -07:00
Scharada
5d6b4b6c2e changes and notify that i have the apk shared 2025-08-23 14:28:01 -07:00
Scharada
d52205ed1c add package com.mediatek.security 2025-08-23 14:28:01 -07:00
Scharada
65e926f458 add package mobile.miki 2025-08-23 14:28:01 -07:00
Scharada
999bb00114 add package com.sec.svoice.lang.* 2025-08-23 14:27:57 -07:00
Ricardo Fernández Serrata
374ecba632
Merge pull request #1049 from Universal-Debloater-Alliance/dependabot/cargo/slab-0.4.11
build(deps): bump slab from 0.4.10 to 0.4.11
2025-08-13 15:33:41 -04:00
dependabot[bot]
9812f7f624
build(deps): bump slab from 0.4.10 to 0.4.11
Bumps [slab](https://github.com/tokio-rs/slab) from 0.4.10 to 0.4.11.
- [Release notes](https://github.com/tokio-rs/slab/releases)
- [Changelog](https://github.com/tokio-rs/slab/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/slab/compare/v0.4.10...v0.4.11)

---
updated-dependencies:
- dependency-name: slab
  dependency-version: 0.4.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-13 04:50:08 +00:00
Adhiraj S. Chauhan
826693c79f
Merge pull request #1044 from Universal-Debloater-Alliance/build-script
build(script): use `embed_resource` appropriately
2025-08-10 08:58:16 +05:30
Rudxain
3aa7f56d0c
build: unwrap embed_resource::compile 2025-08-09 20:04:02 -04:00
Rudxain
4eaaca55cf
build: only rerun build.rs if manifest changes 2025-08-09 19:55:38 -04:00
Ricardo Fernández Serrata
fad0ee9630
Merge pull request #1036 from henriquealrs/fix_scroll_crash
fix(list-description-crash): add null action on scroll
2025-08-04 00:43:28 -04:00
Henrique Lustosa
3b7c13dafd Fix lint issue 2025-08-03 19:57:56 -03:00
Henrique Lustosa
26afc32b69 Add null action on scroll to avoid crash 2025-08-03 11:10:09 -03:00
Anonymoussaurus
be03d78ffd
Merge pull request #1033 from alouiadel/feat/add-copy-button
feat(ui): add copy button to copy error messages
2025-08-02 10:03:46 +02:00
Adel Aloui
23ac7ae6ee style: fix formatting issues in list.rs to pass cargo fmt 2025-08-01 15:40:53 +01:00
Adel Aloui
2ca3e05a8e feat(list): add copy confirmation for error messages and improve clipboard handling 2025-08-01 14:17:32 +01:00
Adel Aloui
a20c53a1bc feat(list): add CopyError message and clipboard functionality for error handling 2025-08-01 13:51:38 +01:00
Ricardo Fernández Serrata
4ca22002b5
Merge pull request #1025 from Universal-Debloater-Alliance/refactor/0re
refactor: remove `regex`; parse `UserInfo` from `pm`
2025-07-26 11:04:21 -04:00
Rudxain
207e312741
feat(adb-pm): handle "running" user state 2025-07-22 10:43:55 -04:00
Rudxain
c5799702e2
refactor: rm all regex, and properly parse pm list users 2025-07-22 10:43:55 -04:00
Rudxain
ca0cfd400d
refactor(adb): rm all regex refs in module 2025-07-22 10:43:54 -04:00
Rudxain
5633b077c2
refactor(adb): rm regex dependency from PackageId::new by adding some utils.
and:
- refactor `adb` `version` assertions to call `next` explicitly,
rather than matching on iteration count.
- re-add debug-assertions for `pm list packages`
2025-07-22 10:43:50 -04:00
Ricardo Fernández Serrata
b34492e98c
Merge pull request #949 from Universal-Debloater-Alliance/chore/deps/bump
build(deps): upgrade all dependencies but Iced
2025-07-22 10:42:16 -04:00
Anonymoussaurus
4fc05ab24c
Merge pull request #1019 from Universal-Debloater-Alliance/copilot/fix-1007
pkg(com.google.android.marvin.talkback): add warning about Motorola dynamic volume interference and fix grammar
2025-07-18 17:28:51 +02:00
copilot-swe-agent[bot]
3e57580c1d Update com.google.android.marvin.talkback description with warning and grammar fix
Co-authored-by: AnonymousWP <50231698+AnonymousWP@users.noreply.github.com>
2025-07-18 14:49:33 +00:00
copilot-swe-agent[bot]
474a784df4 Initial plan 2025-07-18 14:39:47 +00:00
Anonymoussaurus
6d93efe94e
Merge pull request #1014 from Scott-Nx/main
pkg(miui, aosp): change description and removal
2025-07-16 00:27:08 +02:00
Scott-Nx
b385b1ea2e
pkg(scope): com.miui.global.packageinstaller 2025-07-15 09:20:37 +07:00
Scott-Nx
df5c051ab5
pkg(scope): com.android.bluetooth 2025-07-15 09:09:58 +07:00
Scott-Nx
ebfbf62e5e
pkg(scope): com.android.emergency 2025-07-15 09:07:15 +07:00
Scott-Nx
161ee6dec2
pkg(scope): com.miui.securitycore 2025-07-15 09:00:13 +07:00
Rudxain
be13905042 chore(deps): upgrade ureq to v3 2025-07-10 10:34:56 +02:00
Rudxain
b778966760 chore(deps): upgrade embed-resource to v3 2025-07-10 10:34:56 +02:00
Rudxain
2069dbf6c3 chore(deps): upgrade dirs to v6 2025-07-10 10:34:56 +02:00
Rudxain
8707cf296e style: cargo fmt 2025-07-10 10:34:56 +02:00
Rudxain
712d61a63c refactor: cargo clippy --fix 2025-07-10 10:34:56 +02:00
Rudxain
a6cbcd9e6e chore(deps): upgrade rfd to 0.15 2025-07-10 10:34:56 +02:00
Rudxain
9d723c6a44 chore(deps): cargo update 2025-07-10 10:34:56 +02:00
Rudxain
94add2dd90 chore(deps): ureq = 2 2025-07-10 10:34:56 +02:00
samuele.ruffino
d471b798c9
feat: make package descriptions selectable and copyable (#1010)
* feat: make package descriptions selectable and copyable

- Replace text widget with text_editor for package descriptions
- Add read-only text_editor that allows text selection and copying
- Implement Clone manually for List struct to handle text_editor::Content
- Add text_editor StyleSheet implementation to match app theme

This improves UX by allowing users to copy package descriptions for
research or sharing purposes while maintaining the same visual appearance.

---------

Co-authored-by: Samuele Domenico Ruffino <sdruffino@it.iliad.com>
2025-07-10 04:04:32 -04:00
Anonymoussaurus
bb1f245bc2
Merge pull request #1011 from Universal-Debloater-Alliance/chore/fix-outdated
chore: fix outdated CI and comments
2025-07-10 09:28:42 +02:00
Rudxain
1fda255b69
ci: rm mold (missed in b86bb3c) 2025-07-10 02:41:46 -04:00
Rudxain
f34e603213
docs: new log-file name
forgot to change this in 2d8d06f
2025-07-10 02:31:13 -04:00
Ricardo Fernández Serrata
4ffdecd123
Merge pull request #1009 from Universal-Debloater-Alliance/build/no-mold
build(cargo): don't use `mold`
2025-07-05 18:16:47 -04:00
Rudxain
7666ef9c85
build(cargo): release opt-lvl 2
I missed this in 45ad468
2025-07-04 15:00:46 -04:00
Rudxain
b86bb3cf69
build(cargo): don't use mold 2025-07-04 14:58:22 -04:00
Ricardo Fernández Serrata
4f25c20072
Merge pull request #1008 from Universal-Debloater-Alliance/build/profile-div
build: split `release` profile into `opt`
2025-07-04 14:17:36 -04:00
Rudxain
45ad468a0e
build: split release profile into opt 2025-07-04 13:51:32 -04:00
Anonymoussaurus
725bfe183c
Merge pull request #1003 from zavocc/coloros-apps-update
pkg(com.coloros.wifibackuprestore): change removal and description
2025-06-28 00:48:37 +08:00
WMCB Tech (marcusz)
d3331b4d45
Update com.oplus.wifibackuprestore description and its removal type 2025-06-27 20:10:52 +08:00
WMCB Tech (marcusz)
11ee8ba214
com.coloros.wifibackuprestore: Change category type to advanced as this can lose the ability to save and restore access points. 2025-06-24 14:46:48 +08:00
Anonymoussaurus
0b37fac065
Merge pull request #1000 from prirai/patch-1
pkg(com.transsion.applock): change description and removal
2025-06-23 12:27:46 +08:00
Priyanshu Kumar Rai
85068a3181
Update uad_lists.json
Removal of this package broke recents for me. Only after installing it back did my recents work. I double checked. Also checked the logcat that indeed applock is linked to the recents as a provider.
2025-06-21 14:15:01 +05:30
Scott
fbe42ef1b2
Merge pull request #997 from Scott-Nx/main
pkg(mediatek): update descriptions
2025-06-19 09:32:54 +08:00
Anonymoussaurus
c7901cbcd0
Merge pull request #995 from NishchayKQ/main
pkg(com.google.android.tungsten.setupwraith): change removal and description
2025-06-17 10:15:12 +08:00
NishchayKQ
b3b195a7e1
pkg(scope): com.google.android.tungsten.setupwraith 2025-06-16 21:11:30 +05:30
Anonymoussaurus
01be1f5e0f
Merge pull request #990 from Universal-Debloater-Alliance/pkg/media-store
pkg(media-storage): add info about indexing/caching
2025-06-11 14:56:49 +08:00
Rudxain
ddf454f885
pkg(media-storage): add info about indexing 2025-06-11 01:34:24 -04:00
Anonymoussaurus
e9a17409dd
pkg(com.motorola.livewallpaper3.prebuilt.*): add Motorola Prebuilt themes
Motorola Prebuilt themes
2025-06-10 14:01:27 +08:00
trustytrojan
6f9e70b647
feat(gui/list): allow searching in descriptions (#977)
Search by description as well as by name

---------

Co-authored-by: Rudxain <76864299+Rudxain@users.noreply.github.com>
2025-06-08 16:12:02 -04:00
Harshit Sethi
ade70a121a
pkg(com.android.avatarpicker): add description of Avatar Picker package (#976)
* pkg(com.android.avatarpicker): Add description of Avatar Picker package

* Fix a minor syntax erorr in json
2025-06-08 17:41:12 +08:00
Sharun
d857bef9b6
fix(crash): index out of bounds exception (#972)
* fix: index out of bounds exception

* cargo fmt

* refactor using get()

* fmt

* potential fix for checkboxes
2025-06-06 10:33:30 +08:00
Anonymoussaurus
8a5a3b7f8f
Merge pull request #984 from Universal-Debloater-Alliance/schbrns/issue929
pkg(scope): com.google.android.safetycenter.resources
2025-06-06 09:14:37 +08:00
Anonymoussaurus
96e5af27f6
Merge pull request #983 from Universal-Debloater-Alliance/schbrns/issue800
pkg(scope): `com.sec.android.app.hwmoduletest` breaks after removing `com.samsung.android.providers.factory` on Samsung Galaxy phones
2025-06-06 09:12:46 +08:00
Anonymoussaurus
a1541b533a
Merge pull request #982 from Universal-Debloater-Alliance/schbrns/issue963
pkg(scope): Remove fearmongering description of com.samsung.android.dqagent
2025-06-06 09:11:10 +08:00
Anonymoussaurus
c0a1f78075
Merge pull request #981 from Universal-Debloater-Alliance/schbrns/issue973
pkg(scope): com.android.apps.turbo - battery settings crash
2025-06-06 09:10:19 +08:00
Anonymoussaurus
ad29532caf
Merge pull request #980 from Universal-Debloater-Alliance/schbrns/issue975
pkg(scope): com.samsung.android.providers.trash Samsung Voice Recorder Crash
2025-06-06 09:09:36 +08:00
Anonymoussaurus
99a5cf8805
Merge pull request #979 from Universal-Debloater-Alliance/schbrns/issue969
pkg(com.oem.euiccpartnerapp): breaks sim manager Moto G 5G 2024
2025-06-06 09:08:27 +08:00
Anonymoussaurus
3a24739a2f
Merge pull request #978 from Universal-Debloater-Alliance/schbrns/issue792
pkg(scope): Issues with SIM Manager, Package Removal, and Performance
2025-06-06 09:07:16 +08:00
schbrns
ffbbe27856 pkg(scope): com.google.android.safetycenter.resources
Fixes #929
2025-06-05 01:37:30 -04:00
schbrns
6f278a6c3a pkg(scope): com.sec.android.app.hwmoduletest breaks after removing com.samsung.android.providers.factory on Samsung Galaxy phones
Fixes #800
2025-06-05 01:33:31 -04:00
schbrns
2a146a1c99 pkg(scope): Remove fearmongering description of com.samsung.android.dqagent
Fixes #963
2025-06-05 01:31:56 -04:00
schbrns
263905b71a pkg(scope): com.android.apps.turbo - battery settings crash
Fixes #973
2025-06-05 01:23:47 -04:00
schbrns
ae1f2931bd pkg(scope): com.samsung.android.providers.trash Samsung Voice Recorder Crash
Fixes #975
2025-06-05 01:21:32 -04:00
schbrns
758e6bcbf1 pkg(com.oem.euiccpartnerapp): breaks sim manager Moto G 5G 2024
Fixes #969
2025-06-05 01:12:54 -04:00
schbrns
10bdbd30cb pkg(scope): Issues with SIM Manager, Package Removal, and Performance
Fixes #792
Fixes #711
2025-06-05 01:08:48 -04:00
Anonymoussaurus
a04104630e
Merge pull request #940 from Universal-Debloater-Alliance/refactor/backup
refactor: `list_available_backup_user` and `CommandType`->`PackageInfo`
2025-05-23 00:27:18 +08:00
Rudxain
59854c4c7c
perf: minor opt (runtime and compile-time) 2025-05-14 23:05:31 -04:00
Rudxain
c0a876da83
refactor: replace CommandType by PackageInfo
https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/issues/760
2025-05-14 23:05:30 -04:00
Rudxain
17a48aae02
style: cargo clippy --fix 2025-05-14 23:05:30 -04:00
Rudxain
a7fd492f5a
refactor(save): more idiomatic list_available_backup_user 2025-05-14 23:05:30 -04:00
Anonymoussaurus
c2614ac892
Merge pull request #966 from antgustech/fix/update-description-oplustrafficmonitor
pkg(com.oplus.trafficmonitor): update description and removal
2025-05-15 09:57:58 +08:00
Adhiraj S. Chauhan
d651a73928
Merge pull request #965 from Lemminkyinen/fix/typos
fix(sync): minor fixes
2025-05-14 23:33:29 +05:30
Antong
5f0c689c95 updated description and removal for com.oplus.trafficmonitor 2025-05-14 15:17:40 +02:00
Anonymoussaurus
6302fbb50b
Merge pull request #964 from AntoninoScordino/main
pkg(com.google.android.pixel.setupwizard): change description and removal
2025-05-14 11:31:01 +08:00
Jarmo Tenhunen
c177fdaac6
fix(sync): remove unnecessary semicolon 2025-05-13 20:47:41 +03:00
Jarmo Tenhunen
cd068e2e47
fix(sync): correct logging typo 2025-05-13 20:46:55 +03:00
Antonino Scordino
07e1c2ea83 pkg(com.google.android.pixel.setupwizard): warn about GPSU breakage
Change-Id: I04c877fa615300bfcd8b024ab030db250084b258
2025-05-13 19:45:10 +02:00
StreetWayfarer
85440777a3
Update uad_lists.json for additional apps
Motorola Prebuilt themes
2025-05-03 02:36:58 +03:00
Anonymoussaurus
25e0ac0cfd
Merge pull request #935 from Universal-Debloater-Alliance/feat/1log
feat(log): single log-file (`const` name)
2025-04-30 12:18:07 +02:00
Rudxain
2d8d06fc3d
feat(log): single log-file (const name) 2025-04-29 18:20:19 -04:00
Anonymoussaurus
1d70755766
Merge pull request #953 from Universal-Debloater-Alliance/ci/cargo-clippy
ci(cargo): improve linting
2025-04-29 21:21:55 +02:00
Rudxain
5c5499bc6b
ci(clippy): deny default warnings
but keep `style` as `warn`
2025-04-29 14:39:45 -04:00
Rudxain
38554ca2d7
ci(cargo): replace check by clippy 2025-04-29 14:28:06 -04:00
Rudxain
ce341df6c3
ci(cargo): rm --all flag 2025-04-29 13:43:57 -04:00
Adhiraj S. Chauhan
1890a214e8
Merge pull request #918 from Universal-Debloater-Alliance/bug/about/panic-adb-version#917
fix(views:about): default text for `adb version`
2025-04-29 12:16:04 +05:30
schbrns
7d3a9947d2
pkg(oem): update descriptions and change removal status for dependencies related to Private space on Pixel devices (#944)
* pkg(oem): update descriptions and change removal status for dependencies related to Private space on Pixel devices

* Update uad_lists.json

Resolve merge conflict.

* Update uad_lists.json
2025-04-27 14:09:02 +02:00
schbrns
a5fb797f9e
Merge pull request #945 from schbrns/schbrns/issue688
pkg(com.android.global): update description and removal behaviour
2025-04-25 13:04:28 +02:00
Anonymoussaurus
19f7ce8187
Merge pull request #946 from AntoninoScordino/main
pkg(com.android.role.notes.enabled): add warning regarding Developer Options
2025-04-24 15:12:02 +02:00
Antonino Scordino
f7e15e9c80 pkg(com.android.role.notes.enabled): add warning regarding Developer Options
Change-Id: Ifbbaa8f3486129dbed464c5818b812ff8259764b
2025-04-24 14:59:04 +02:00
Anonymoussaurus
979df43032
Merge pull request #943 from schbrns/942-pkgscope-comcnnmobileandroidphoneedgepanel
pkg(cnn): remove duplicate
2025-04-23 22:01:39 +02:00
schbrns
830e6883bd pkg(onyx): format JSON 2025-04-23 15:58:35 -04:00
schbrns
cc49ce3c63 pkg(cnn): update edge panel entry and remove outdated parental control description 2025-04-23 15:54:37 -04:00
Anonymoussaurus
3e32074267
Merge pull request #932 from scxtt-nc/main
pkg(com.android.chrome): update description for clarity on functionality
2025-04-14 10:07:15 +02:00
Nathawat
5f2deaa64c
pkg(com.android.chrome): update description to clarify functionality and dependencies 2025-04-14 08:39:01 +07:00
Rudxain
e20b9aef72
fix(views:about): default text for adb version failure
regression introduced here:
3cc2f1d004
2025-03-27 18:46:08 -04:00
Rudxain
6cb12ea91b
perf(ADB): defer version parsing to caller
Change the method signature to
allow the consumer to decide when/if and how to parse the `stdout`
2025-03-27 18:41:40 -04:00
Cameron Jackson
246c2208b0
pkg(miui.systemui.plugin): improve description (#759)
* Reworded as HyperOS devices are now more popular, and losing Quick Settings is more relevant than gaining AOSP volume style.

* Updated to recommended wording

* pkg(miui.systemui.plugin): apply suggestions from code review

---------

Co-authored-by: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>
2025-03-22 11:58:28 +01:00
YoshiiShell
59493b5f9e
pkg(samsung): add packages (#743)
* Packages

* fixes

* fix numba 2

* a

* pkg(samsung): fix formatting and change removals

---------

Co-authored-by: Anonymoussaurus <50231698+AnonymousWP@users.noreply.github.com>
2025-03-22 11:50:39 +01:00
Anonymoussaurus
f0ca2dfe7f
Merge pull request #877 from Universal-Debloater-Alliance/836-pkgscope-comandroidunisoctelephonyserver
pkg(com.android.unisoc.telephony.server): change description and removal
2025-03-18 10:07:14 +01:00
Anonymoussaurus
9bf8c1abe3
pkg(com.android.server.deviceconfig.resources): add package (#826)
* pkg(com.android.server.deviceconfig.resources): add package

* pkg(com.android.server.deviceconfig.resources): apply suggestion from code review

* pkg(com.android.server.deviceconfig.resources): apply suggestion from code review
2025-03-18 10:06:51 +01:00
Anonymoussaurus
6ebd13aaf5
Merge pull request #822 from Universal-Debloater-Alliance/716-update-description-of-pkgcomandroidincallui
pkg(com.android.incallui): change description and removal
2025-03-18 10:06:01 +01:00
Ricardo Fernández Serrata
d562efe813
Merge pull request #868 from Universal-Debloater-Alliance/pkg/gms
pkg: improve GMS and WA desc
2025-03-17 19:18:12 -04:00
Anonymoussaurus
8aede76b86
pkg(com.unisoc.phone): change description 2025-03-01 16:40:35 +01:00
Anonymoussaurus
601fe1f36f
pkg(com.android.unisoc.telephony.server): change description and removal 2025-03-01 16:38:13 +01:00
Rudxain
fdfcfe8c9a
pkg: mention WhatsApp dependency on GMS 2025-02-23 15:02:15 -04:00
Rudxain
63c8a72061
pkg(Google): improve GMS description 2025-02-23 14:57:30 -04:00
Anonymoussaurus
91790f7612
pkg(com.android.incallui): apply suggestion from code review 2025-02-10 20:29:21 +01:00
Anonymoussaurus
2a9b2675d0
pkg(com.android.incallui): change description and removal 2025-02-02 20:55:28 +01:00
63 changed files with 28582 additions and 18784 deletions

View file

@ -1,6 +1,2 @@
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]

View file

@ -6,16 +6,17 @@ body:
- type: markdown
attributes:
value: |
## Consider submitting a Pull Request
We receive many package-related issues and it's hard to keep up. **Consider submitting a Pull Request yourself**—it helps us process changes much faster! See our [guide on how to contribute](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/How-to-contribute) for instructions.
---
## Document each package the best you can
**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
@ -30,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:
@ -37,6 +47,7 @@ body:
description: Your bug report will be closed if you don't follow the checklist below.
options:
- label: I removed all those packages on my phone. If not, why? Explain in the issue.
- label: I'm willing to submit a Pull Request for this change.
- label: This issue is not a duplicate of an existing one.
required: true
- label: I have chosen an appropriate title.

View file

@ -3,6 +3,13 @@ description: Your phone has unexpected issues after debloating
title: "pkg(scope): "
labels: ["package::breakage"]
body:
- type: markdown
attributes:
value: |
## Consider submitting a Pull Request
If you know which package caused the issue and how the documentation should be updated, **consider submitting a Pull Request yourself**—it helps us process changes much faster! See our [guide on how to contribute](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/How-to-contribute) for instructions.
- type: textarea
id: what-happened
attributes:
@ -31,6 +38,7 @@ body:
label: Acknowledgements
description: Your bug report will be closed if you don't follow the checklist below.
options:
- label: I'm willing to submit a Pull Request to fix the package documentation.
- label: This issue is not a duplicate of an existing one.
required: true
- label: I have chosen an appropriate title.

View file

@ -6,16 +6,17 @@ body:
- type: markdown
attributes:
value: |
## Consider submitting a Pull Request
We receive many package documentation updates and it's hard to keep up. **Consider submitting a Pull Request yourself**—it helps us process changes much faster! See our [guide on how to contribute](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/How-to-contribute) for instructions.
---
## Document each package the best you can
**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
@ -33,12 +34,22 @@ 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:
label: Acknowledgements
description: Your bug report will be closed if you don't follow the checklist below.
options:
- label: I'm willing to submit a Pull Request for this change.
- label: This issue is not a duplicate of an existing one.
required: true
- label: I have chosen an appropriate title.

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

@ -18,9 +18,9 @@ jobs:
- build_target: linux
os: ubuntu-22.04
- build_target: macos
os: macos-14
os: macos-15
- build_target: macos-intel
os: macos-13
os: macos-15-intel
- build_target: windows
os: windows-2022
- update_feature: self-update
@ -30,7 +30,6 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: rui314/setup-mold@v1 # faster linker
- uses: actions/cache@v4
with:
path: |
@ -43,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
@ -69,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

@ -21,29 +21,29 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-22.04, windows-2022, macOS-14]
os: [ubuntu-22.04, windows-2022, macos-15]
lint: [check, test, clippy, fmt]
exclude: # https://github.com/community/community/discussions/7835
- os: windows-2022
lint: clippy
- os: windows-2022
lint: fmt
- os: macOS-14
- os: macos-15
lint: clippy
- os: macOS-14
- os: macos-15
lint: fmt
include:
- lint: check
args: " --all-features"
env-flag: "-D warnings"
- lint: test
args: ""
- lint: clippy
args: " --all --all-features"
args: " --all-features -- -D clippy::all -W clippy::style"
- lint: fmt
args: " --all -- --check"
args: " -- --check"
steps:
- uses: actions/checkout@v4
- uses: rui314/setup-mold@v1 # faster linker
- uses: actions/cache@v4
with:
path: |
@ -57,7 +57,8 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy,rustfmt
- run: cargo ${{ matrix.lint }}${{ matrix.args }}
- shell: bash
run: RUSTFLAGS='${{ matrix.env-flag }}' cargo ${{ matrix.lint }}${{ matrix.args }}
coverage:
name: coverage

View file

@ -25,5 +25,10 @@ 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]
> 📦 **Package list updates don't require a new app version**—UAD-ng automatically fetches the latest package definitions on launch (requires internet connection).

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

View file

@ -2,12 +2,42 @@
## For who is this guide?
This guide is meant for users who want to contribute to the codebase of Universal Android Debloater Next Generation, whether that is the application code or the JSON-file for adding packages. To keep all processes streamlined and consistent, we're asking you to stick to this guide whenever contributing.
This guide is meant for users who want to contribute to the codebase of Universal Android Debloater Next Generation, whether that is the application code or [the "database"-file for adding packages](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/How-to-contribute). To keep all processes streamlined and consistent, we're asking you to stick to this guide whenever contributing.
Even though the guide is made for contributors, it's also strongly recommended that the UAD-ng team sticks to these guidelines. After all, we're a prime example.
## What are the guidelines?
### Mass edits
If you've performed massive edits via automation, you must specify what automation you used. For example, if you ran the command:
```sh
sed -i 's/a/b/' resources/assets/uad_lists.json
```
You should copy-paste that cmd to the pull-request description. This rule applies even if you use "proper" cmds such as `jq`, which safely edit structured data. And it also applies to AI, *especially* LLMs.
This is required so that we can review your big patch without reading it. This improves [transparency](https://en.wikipedia.org/wiki/Transparency_(behavior)), which makes you more trust-worthy! Bonus points if you can provide a sequence of cmds that proves your patch contains exactly what you said it does. For the simple `sed` case, something like:
```sh
# alt: `gh pr checkout 0000 && git checkout main`
git remote add fork https://github.com/user/uadng
git fetch fork
# example commit hash from upstream (not fork);
# more reproducible than branch-name
git checkout beefcafe
sed -i 's/a/b/' resources/assets/uad_lists.json
# compare unstaged change with commit from fork
git diff cafebeef
```
Should be enough.
You should be careful with mass-editing anyways. Even if the verification is successful, the effects [might not be what you expect](https://en.wikipedia.org/wiki/Scunthorpe_problem).
Since AI is non-deterministic, the version number is optional, but it's helpful to know how "intelligent" it is. AI edits are non-reproducible, so "verification scripts" are useless.
### Branching strategy
As for our branching strategy, we're using [Trunk-Based Development](https://trunkbaseddevelopment.com/#one-line-summary).
@ -33,3 +63,30 @@ If you want to [back-port](https://en.wikipedia.org/wiki/Backporting) (GitHub's
### Commit messages
As for commits, we prefer using [Conventional Commit Messages](https://gist.github.com/qoomon/5dfcdf8eec66a051ecd85625518cfd13). When working in any of the branches listed above (if there's an existing issue for it), close it using a [closing keyword](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword). For more information regarding Conventional Commit Messages, see <https://www.conventionalcommits.org/en/v1.0.0/> as well.
### Issue labels
When creating issues, they are automatically labeled based on the [issue template](.github/ISSUE_TEMPLATE/) you choose. These labels help categorize work and generate release notes.
| Label | Purpose | Issue template |
| ----- | ------- | -------------- |
| `bug` | Software bugs in UAD-ng | Bug Report |
| `enhancement` | New feature requests | Feature Request |
| `package::addition` | Adding new packages to the debloat list | Add new package(s) |
| `package::breakage` | Issues caused by debloating | Debloat issue report |
| `package::documentation` | Updating package descriptions or recommendations | Update apps description or recommendation |
| `documentation` | Improvements to docs/wiki | UAD-ng documentation |
| `ci` | CI/CD related issues | CI/CD |
| `refactor` | Code refactoring | — (maintainer-assigned) |
| `dependencies` | Dependency updates | — (maintainer-assigned) |
Labels are also used to automatically generate categorized [release notes](.github/release.yml). Each label maps to a changelog section:
| Label(s) | Changelog category |
| -------- | ------------------ |
| `enhancement` | Features |
| `bug` | Fixed |
| `refactor` | Refactors |
| `package::addition`, `package::breakage`, `package::documentation` | Packages/apps |
| `ci`, `dependencies` | Development |
| `documentation` | Documentation |

4505
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,13 @@
[package]
name = "uad-ng"
description = "A cross-platform GUI debloater for android devices"
version = "1.1.2"
[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"
homepage = "https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation"
@ -11,66 +17,47 @@ 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"
regex = "^1.10.2"
chrono = { version = "^0.4", default-features = false, features = ["std", "clock"] }
toml = "^0"
dirs = "^5.0.0"
ureq = { version = "*", features = ["json"] }
dirs = "^6"
ureq = { version = "3", features = ["json"] }
retry = "^2.0.0"
iced = { version = "^0.12.0", features = ["advanced", "image"] }
rfd = "^0.14"
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]
opt-level = "s"
codegen-units = 1
lto = true
strip = true
panic = "abort"
[build-dependencies]
embed-resource = "2.4.2"
[lints.rust]
[workspace.lints.rust]
deprecated_safe = "warn"
[lints.clippy]
[workspace.lints.clippy]
undocumented_unsafe_blocks = "forbid"
exit = "deny"
panic_in_result_fn = "warn"
infinite_loop = "warn"
mem_forget = "warn"
string_to_string = "warn"
format_push_string = "warn"
large_include_file = "warn"
shadow_unrelated = "warn"
struct_field_names = "allow" # annoying
module_name_repetitions = "allow" # annoying
disallowed_types = "deny"
disallowed_methods = "deny"
allow_attributes_without_reason = "warn"
pedantic = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }

View file

@ -1,34 +1,53 @@
# Universal Android Debloater Next Generation
> [!warning]
> **DISCLAIMER**: Use at your own risk. We're not responsible for anything that
could happen to your devices.
> **DISCLAIMER**: Use at your own risk. We're not responsible for anything that could happen to your devices.
<img src="/resources/screenshots/v1.0.2.png" width="850" alt="uad_screenshot">
<div align=center>
<img width=75% alt="screenshot of apps view" src=./resources/screenshots/v1.0.2.png>
</div>
**Check out the issues, and [feel free to contribute!](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/How-to-contribute)**. We're in **HIGH NEED** of [Rust](https://www.rust-lang.org) developers for fixing critical issues, see [this announcement](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/discussions/731) for more information.
**For real-time communication, consider joining our Discord server:**
<a href="https://discord.gg/CzwbMCPEZa">
<img src="./resources/images/icon_clyde_blurple_RGB.png" alt="Icon" width="75">
</a>
**In case you prefer using Matrix (using a Matrix bridge to Discord):**
[<img src="https://matrix.org/images/matrix-logo.svg">](https://matrix.to/#/#uad-ng:matrix.org)
## Summary
This is a detached fork of the [UAD project](https://github.com/0x192/universal-android-debloater), which aims to improve privacy and battery performance by removing unnecessary and obscure system apps.
This can also contribute to improving security by reducing (but not eliminating) [the attack surface](https://en.wikipedia.org/wiki/Attack_surface). Read the [wiki](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki) for more details on getting started. Whilst UAD-ng can remove system apps, it cannot detect or remove potentially malicious system services or drivers baked into the firmware of your device by various vendors; some vendor-specific apps are only UI front-ends to vendor-provided system services, and as such disabling/uninstalling those apps will not stop a service from running. Additional information can be found in package descriptions inside the Universal Android Debloater Next Generation application.
This is a detached fork of the [UAD project](https://github.com/0x192/universal-android-debloater). This aims to improve privacy and efficiency (energy, speed, memory) by removing unnecessary and obscure system apps.
This can also improve security by reducing the [attack surface](https://en.wikipedia.org/wiki/Attack_surface). Read [the wiki](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki) for more details.
## Documentation
For documentation regarding how to use UAD-ng, the FAQ, building from source and how to decompile/extract APKs, see [our Wiki](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki).
Everything about UAD-ng (and related stuff) can be found on the Wiki, such as:
- [Getting started](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Getting-started)
- Features of this app
- [Usage guide](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Usage)
- Suggested Android app replacements
- How to get the cutting-edge version by [building the source-code](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Building-from-source)
- Weird things OEMs do
- [Suggested apps for analyzing APKs](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/How-to-contribute#useful-apps)
## Privacy
UAD-ng does not collect or transmit any user data. The only external connections are `GET` requests to GitHub for fetching the [package list](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/blob/main/resources/assets/uad_lists.json) ([src/core/uad_lists.rs](src/core/uad_lists.rs)) and checking for updates ([src/core/update.rs](src/core/update.rs)).
## Contact
**For real-time communication and support, consider joining our Discord guild:**
[<img width=64em alt="Discord symbol" src=./resources/images/icon_clyde_blurple.svg>](https://discord.gg/CzwbMCPEZa)
**If you prefer using Matrix, we have a bridge to Discord:**
[<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.
- [@mawilms](https://github.com/mawilms) for his LotRO plugin manager ([Lembas](https://github.com/mawilms/lembas)) which helped a lot to understand how to use the [Iced](https://github.com/hecrj/iced) GUI library.
- [@mawilms](https://github.com/mawilms) for his LotRO plugin manager ([Lembas](https://github.com/mawilms/lembas)) which helped a lot to understand how to use the [Iced](https://github.com/iced-rs/iced) GUI library.
- [@casperstorm](https://github.com/casperstorm) for the UI/UX inspiration.

View file

@ -1,9 +1,11 @@
use std::{env, error::Error};
const META: &str = "resources/assets/manifest.rc";
fn main() -> Result<(), Box<dyn Error>> {
if env::var("CARGO_CFG_TARGET_FAMILY")? == "windows" {
embed_resource::compile("resources/assets/manifest.rc", embed_resource::NONE);
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo::rerun-if-changed={META}");
if std::env::var("CARGO_CFG_TARGET_FAMILY")? == "windows" {
embed_resource::compile(META, embed_resource::NONE)
.manifest_optional()
.unwrap();
}
Ok(())
}

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,18 +38,37 @@
//! For comprehensive info about ADB,
//! [see this](https://android.googlesource.com/platform/packages/modules/adb/+/refs/heads/master/docs/)
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::sync::LazyLock;
use std::rc::Rc;
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
pub fn to_trimmed_utf8(v: Vec<u8>) -> String {
String::from_utf8(v)
.expect("ADB should always output valid ASCII (or UTF-8, at least)")
.trim_end()
.to_string()
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()
}
#[cfg(debug_assertions)]
#[must_use]
fn is_version_triple(s: &str) -> bool {
let mut components = s.split('.');
for _ in 0..3 {
if !components
.next()
.is_some_and(|comp| comp.as_bytes().iter().all(u8::is_ascii_digit))
{
return false;
}
}
if components.next().is_some() {
return false;
}
true
}
/// Builder object for an Android Debug Bridge CLI command,
@ -112,7 +131,7 @@ impl ACommand {
.collect())
}
/// `version` sub-command, splitted by lines
/// `version` sub-command
///
/// ## Format
/// This is just a sample,
@ -131,42 +150,35 @@ impl ACommand {
/// Installed as <ANDROID_SDK_HOME>/platform-tools/adb[.exe]
/// Running on <OS/kernel version> (<CPU arch>)
/// ```
pub fn version(mut self) -> Result<Vec<String>, String> {
#[cfg(debug_assertions)]
static TRIPLE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^Android Debug Bridge version \d+.\d+.\d+$")
.unwrap_or_else(|_| unreachable!())
});
#[cfg(debug_assertions)]
static DISTRO: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^Version \d+.\d+.\d+-\S+$").unwrap_or_else(|_| unreachable!())
});
#[expect(clippy::panic_in_result_fn, reason = "Assertions are fine")]
pub fn version(mut self) -> Result<String, String> {
self.0.arg("version");
Ok(self
.run()?
.lines()
.enumerate()
// typically 5 allocs.
// ideally 0, if we didn't use `lines`.
.map(|(i, ln)| {
// DO NOT "REFACTOR" TO `debug_assert`!
// it's not the same!
#[cfg(debug_assertions)]
assert!(match i {
0 => TRIPLE.is_match(ln),
1 => DISTRO.is_match(ln),
2 =>
// missing test for valid path
ln.starts_with("Installed as ")
&& (ln.ends_with("adb") || ln.ends_with("adb.exe")),
// missing test for x86/ARM (both 64b)
3 => ln.starts_with("Running on "),
_ => unreachable!("Expected < 5 lines"),
});
ln.to_string()
})
.collect())
let out = self.run()?;
#[cfg(debug_assertions)]
{
const ADBV: &str = "Android Debug Bridge version ";
const V: &str = "Version ";
let mut lns = out.lines();
assert!(
lns.next()
.is_some_and(|ln| ln.starts_with(ADBV) && is_version_triple(&ln[ADBV.len()..]))
);
assert!(lns.next().is_some_and(|ln| ln.starts_with(V)
&& is_version_triple(&ln[V.len()..ln.find('-').unwrap_or(ln.len())])));
// missing test for valid path
assert!(lns.next().is_some_and(|ln| ln.starts_with("Installed as ")
&& (ln.ends_with("adb") || ln.ends_with("adb.exe"))));
// missing test for x86/ARM (both 64b)
assert!(lns.next().is_some_and(|ln| ln.starts_with("Running on ")));
if lns.next().is_some() {
unreachable!("Expected < 5 lines")
}
}
Ok(out)
}
/// General executor
@ -184,15 +196,15 @@ impl ACommand {
);
match cmd.output() {
Err(e) => {
error!("ADB: {}", e);
error!("ADB: {e}");
Err("Cannot run ADB, likely not found".to_string())
}
Ok(o) => {
let stdout = to_trimmed_utf8(o.stdout);
let stdout = to_trimmed_utf8(&o.stdout);
if o.status.success() {
Ok(stdout)
} else {
let stderr = to_trimmed_utf8(o.stderr);
let stderr = to_trimmed_utf8(&o.stderr);
// ADB does really weird things:
// Some errors are not redirected to `stderr`
let err = if stdout.is_empty() { stderr } else { stdout };
@ -203,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.
///
@ -232,23 +250,53 @@ impl ShellCommand {
self.0.0.arg("reboot");
self.0.run()
}
/// Execute an arbitrary shell action string on the device's default shell.
/// The action string is passed as a single argument to `adb shell` and
/// interpreted by the remote shell (which splits on spaces).
pub fn raw(mut self, action: &str) -> Result<String, String> {
self.0.0.arg(action);
self.0.run()
}
}
#[must_use]
pub const fn is_pkg_component(s: &[u8]) -> bool {
if s.is_empty() {
return false;
}
s[0].is_ascii_alphabetic()
&& if s.len() > 1 {
is_all_w_c(s.split_at(1).1)
} else {
true
}
}
/// String with the invariant of being a valid package-name.
/// See its `new` constructor for more info.
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)]
pub struct PackageId(Box<str>);
/// See [`PackageId::new`] for validation details.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PackageId(Rc<str>);
impl PackageId {
/// Creates a package-ID if it's valid according to
/// [this](https://developer.android.com/build/configure-app-module#set-application-id)
pub fn new(p_id: Box<str>) -> Option<Self> {
static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^[a-zA-Z][a-zA-Z0-9_]*(?:\.[a-zA-Z][a-zA-Z0-9_]*)+$")
.unwrap_or_else(|_| unreachable!())
});
if RE.is_match(p_id.as_ref()) {
Some(Self(p_id))
/// 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>
#[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
.next()
.is_some_and(|comp| is_pkg_component(comp.as_bytes()))
{
return None;
}
}
if components.all(|comp| is_pkg_component(comp.as_bytes())) {
Some(Self(p_id.into()))
} else {
None
}
@ -287,16 +335,16 @@ const PACK_PREFIX: &str = "package:";
pub const PM_CLEAR_PACK: &str = "pm clear";
/// Builder object for an Android Package Manager command.
///
/// [More info](https://developer.android.com/tools/adb#pm)
/// <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(
@ -309,35 +357,111 @@ impl PmCommand {
cmd.args(["list", "packages", "-s"]);
if let Some(s) = f {
cmd.arg(s.to_str());
};
}
if let Some(u) = user_id {
cmd.arg("--user");
cmd.arg(u.to_string());
};
}
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));
String::from(&p_ln[PACK_PREFIX.len()..])
let p = &p_ln[PACK_PREFIX.len()..];
if PackageId::new(p).is_some() {
Some(String::from(p))
} else {
warn!("skipping nonstandard package name: {p:?}");
None
}
})
.collect()
})
}
/// `list users` sub-command.
/// Output isn't parsed, because
/// we don't know if the format is stable across Android versions.
/// `list users` sub-command, deserialized/parsed.
///
/// - <https://source.android.com/docs/devices/admin/multi-user-testing>
/// - <https://stackoverflow.com/questions/37495126/android-get-list-of-users-and-profile-name>
pub fn list_users(mut self) -> Result<String, String> {
pub fn list_users(mut self) -> Result<Box<[UserInfo]>, String> {
self.0.0.0.args(["list", "users"]);
self.0.0.run()
Ok(self
.0
.0
.run()?
.lines()
.skip(1) // omit header
.map(|ln| {
// this could be optimized by making more API-stability assumptions
let ln = ln.trim_ascii_start();
let ln = ln.strip_prefix("UserInfo").unwrap_or(ln).trim_ascii_start();
let ln = ln.strip_prefix('{').unwrap_or(ln).trim_ascii();
//let run;
let ln = if let Some(l) = ln.strip_suffix("running") {
//run = true;
l.trim_ascii_end()
} else {
//run = false;
ln
};
let ln = ln.strip_suffix('}').unwrap_or(ln).trim_ascii_end();
// https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/content/pm/UserInfo.java
// The format looks stable today, but google may change it in future Android versions
// (and very old Androids might differ). Keep parsing defensive.
// Expected shape: "UserInfo{<id>:<name>:<flags>}[ running]"
let mut comps = ln.split(':');
let id = comps
.next()
.expect("There must be at least 1 ':'-separated component")
.parse()
.expect("string assumed to be UID numeral");
//let name = comps
// .next()
// .expect("There must be at least 2 ':'-separated components. 2nd is user-name");
//let flags = u32::from_str_radix(
// comps.next().expect(
// "There must be at least 3 ':'-separated components. 3rd is user bit-flags",
// ),
// 16,
//)
//.expect("string assumed to be hexadecimal bit-flags");
UserInfo {
id,
//name: name.into(),
//flags,
//running: run,
}
})
.collect())
}
}
/// Mirror of AOSP `UserInfo` Java Class, with an extra field
#[derive(Debug, Clone)]
pub struct UserInfo {
id: u16,
//name: Box<str>,
//flags: u32,
//running: bool,
}
impl UserInfo {
#[must_use]
pub const fn get_id(&self) -> u16 {
self.id
}
/*
/// Check if the user was logged-in at the time `pm list users` was invoked
#[must_use]
#[allow(dead_code, reason = "Currently unused by UI; kept for future features")]
pub const fn was_running(&self) -> bool {
self.running
}
*/
}
#[cfg(test)]
mod tests {
use super::*;
@ -356,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);
}
}
@ -373,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,30 +56,33 @@ 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) {
Ok(config) => return config,
Err(e) => error!("Invalid config file: `{}`", e),
Err(e) => error!("Invalid config file: `{e}`"),
},
Err(e) => error!("Failed to read config file: `{}`", e),
Err(e) => error!("Failed to read config file: `{e}`"),
}
error!("Restoring default config file");
let toml = toml::to_string(&Self::default()).unwrap();
@ -109,12 +114,15 @@ mod tests {
fn test_load_configuration_file() {
create_default_config_file();
let config = Config::load_configuration_file();
assert_eq!(config.devices.len(), 0);
assert_eq!(config.general.theme, Theme::default().to_string());
// non-deterministic
//assert_eq!(config.devices.len(), 0);
assert_eq!(config.general.theme, DEFAULT_THEME);
assert!(!config.general.expert_mode);
assert_eq!(config.general.backup_folder, CACHE_DIR.join("backups"));
}
// non-deterministic
/*
#[test]
fn test_save_changes() {
let mut settings = Settings::default();
@ -124,12 +132,13 @@ mod tests {
let config = Config::load_configuration_file();
assert_eq!(config.devices[0].device_id, device_id);
}
*/
#[test]
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,7 @@
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::{
fs,
@ -21,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(),
@ -37,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);
}
@ -52,9 +49,9 @@ pub async fn backup_phone(
let backup_path = &*backup_dir.join(device_id);
if let Err(e) = fs::create_dir_all(backup_path) {
error!("BACKUP: could not create backup dir: {}", e);
error!("BACKUP: could not create backup dir: {e}");
return Err(e.to_string());
};
}
let backup_filename =
format!("{}.json", chrono::Local::now().format("%Y-%m-%d_%H-%M-%S"));
@ -78,24 +75,21 @@ 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) => {
let phone_backup: PhoneBackup =
serde_json::from_str(&data).expect("Unable to parse backup file");
let mut users = vec![];
for u in phone_backup.users {
users.push(User {
id: u.id,
index: 0,
protected: false,
});
}
users
}
Ok(data) => serde_json::from_str::<PhoneBackup>(&data)
.expect("Unable to parse backup file")
.users
.into_iter()
.map(|u| User {
id: u.id,
index: 0,
protected: false,
})
.collect(),
Err(e) => {
error!("[BACKUP]: Selected backup file not found: {}", e);
error!("[BACKUP]: Selected backup file not found: {e}");
vec![]
}
}
@ -103,15 +97,22 @@ pub fn list_available_backup_user(backup: DisplayablePath) -> Vec<User> {
#[derive(Debug)]
pub struct BackupPackage {
pub i_user: usize,
pub index: usize,
pub commands: Vec<String>,
}
#[derive(Debug)]
pub struct RestoreResult {
pub packages: Vec<BackupPackage>,
pub skipped_count: usize,
}
pub fn restore_backup(
selected_device: &Phone,
packages: &[Vec<PackageRow>],
packages: &[Vec<CorePackage>],
settings: &DeviceSettings,
) -> Result<Vec<BackupPackage>, String> {
) -> Result<RestoreResult, String> {
match fs::read_to_string(
settings
.backup
@ -126,24 +127,26 @@ pub fn restore_backup(
serde_json::from_str(&data).expect("Unable to parse backup file");
let mut commands = vec![];
let mut skipped_packages = 0;
for u in phone_backup.users {
let index = match selected_device.user_list.iter().find(|x| x.id == u.id) {
let i_user = match selected_device.user_list.iter().find(|x| x.id == u.id) {
Some(i) => i.index,
None => return Err(format!("user {} doesn't exist", u.id)),
};
for (i, backup_package) in u.packages.iter().enumerate() {
let package: CorePackage = match packages[index]
let package: CorePackage = if let Some(p) = packages[i_user]
.iter()
.find(|x| x.name == backup_package.name)
{
Some(p) => p.into(),
None => {
return Err(format!(
"{} not found for user {}",
backup_package.name, u.id
));
}
p.clone()
} else {
skipped_packages += 1;
warn!(
"{} not found for user {} - skipping package during restore",
backup_package.name, u.id
);
continue;
};
let p_commands = apply_pkg_state_commands(
&package,
@ -156,19 +159,29 @@ pub fn restore_backup(
);
if !p_commands.is_empty() {
commands.push(BackupPackage {
i_user,
index: i,
commands: p_commands,
});
}
}
}
if skipped_packages > 0 {
info!(
"Restore completed with {skipped_packages} packages skipped (not found on device)"
);
}
if !commands.is_empty() {
commands.push(BackupPackage {
i_user: 0,
index: 0,
commands: vec![],
});
}
Ok(commands)
Ok(RestoreResult {
packages: commands,
skipped_count: skipped_packages,
})
}
Err(e) => Err(e.to_string()),
}

663
crates/uad-core/src/sync.rs Normal file
View file

@ -0,0 +1,663 @@
use crate::{
adb::{ACommand as AdbCommand, PM_CLEAR_PACK, PackageId},
uad_lists::PackageState,
};
use log::{error, info};
use retry::{OperationResult, delay::Fixed, retry};
use serde::{Deserialize, Serialize};
/// An Android device, typically a phone
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Phone {
/// Non-market name
pub model: String, // could be `Copy`
/// Android API level version
pub android_sdk: u8,
/// In theory, `len < u16::MAX` _should_ always be `true`.
/// In practice, `len <= u8::MAX`.
pub user_list: Vec<User>,
/// Unique serial identifier
pub adb_id: String, // could be `Copy`
}
impl Default for Phone {
fn default() -> Self {
Self {
model: "fetching devices...".to_string(),
android_sdk: 0,
user_list: vec![],
adb_id: String::default(),
}
}
}
impl std::fmt::Display for Phone {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.model)
}
}
/// `UserInfo` but relevant to UAD
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub struct User {
pub id: u16,
pub index: usize,
pub protected: bool,
}
impl std::fmt::Display for User {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "user {}", self.id)
}
}
/// An enum to contain different variants for errors yielded by ADB.
#[derive(Debug, Clone)]
pub enum AdbError {
Generic(String),
}
/// Run an arbitrary shell action via the typed ADB wrapper.
/// This replaces the deprecated `adb_shell_command`.
///
/// If `serial` is empty, it lets ADB choose the default device.
pub fn run_adb_shell_action<S: AsRef<str>>(
device_serial: S,
action: &str,
) -> Result<String, AdbError> {
let serial = device_serial.as_ref();
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(friendly_msg));
}
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(friendly_msg));
}
Err(AdbError::Generic(err))
}
}
}
/// Convert common OEM-specific ADB error messages into user-friendly explanations.
fn make_friendly_error_message(error_output: &str, action: &str) -> String {
// Common Samsung errors
if error_output.contains("DELETE_FAILED_USER_RESTRICTED") {
return format!(
"Cannot uninstall: This package is restricted by the device manufacturer (Samsung Knox or similar).\n\
Error: {error_output}\n\
Tip: Try disabling the package instead, or check device settings for Knox/security restrictions."
);
}
if error_output.contains("NOT_INSTALLED_FOR_USER") {
return format!(
"Package is not installed for the current user.\n\
Error: {error_output}\n\
Tip: The package may be installed for a different user profile or work profile."
);
}
// Empty package name error
if error_output.contains("Shell cannot change component state for null") {
return format!(
"Invalid package: Empty package name detected.\n\
Error: {error_output}\n\
Tip: Please refresh the package list and try again."
);
}
// Generic permission errors
if error_output.contains("Permission denied")
|| error_output.contains("INSTALL_FAILED_PERMISSION_MODEL_DOWNGRADE")
{
return format!(
"Permission denied: Insufficient privileges to perform this action.\n\
Error: {error_output}\n\
Tip: This may require root access or the package is protected by the system."
);
}
// Work profile / managed device errors
if error_output.contains("DELETE_FAILED_DEVICE_POLICY_MANAGER") {
return format!(
"Cannot modify: Package is managed by device policy (MDM/EMM).\n\
Error: {error_output}\n\
Tip: Contact your IT administrator if this is a work device."
);
}
// Generic failure with context
format!("{action} -> {error_output}")
}
/// 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))
.unwrap_or_default()
}
// Minimum information for processing adb commands
#[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,
}
#[must_use]
pub fn apply_pkg_state_commands(
package: &CorePackage,
wanted_state: PackageState,
selected_user: User,
phone: &Phone,
) -> Vec<String> {
// https://github.com/Universal-Debloater-Alliance/universal-android-debloater/wiki/ADB-reference
// ALWAYS PUT THE COMMAND THAT CHANGES THE PACKAGE STATE FIRST!
let commands = match wanted_state {
PackageState::Enabled => match package.state {
PackageState::Disabled => vec!["pm enable"],
PackageState::Uninstalled => match phone.android_sdk {
i if i >= 23 => vec!["cmd package install-existing"],
21 | 22 => vec!["pm unhide"],
19 | 20 => vec!["pm unblock", PM_CLEAR_PACK],
_ => unreachable!("already prevented by the GUI"),
},
_ => vec![],
},
PackageState::Disabled => match package.state {
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"], // > 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![],
},
PackageState::All => vec![],
}; // this should be a `tinyvec`, as `len <= 4`
let user = supports_multi_user(phone).then_some(selected_user);
request_builder(&commands, &package.name, user)
}
/// 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()
.map(|c| format!("{c}{maybe_user_flag} {package}"))
.collect()
}
/// 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)
.getprop("ro.product.model")
.unwrap_or_else(|err| {
eprintln!("ERROR: {err}");
error!("{err}");
if err.contains("adb: no devices/emulators found") {
"no devices/emulators found".to_string()
} else {
err
}
})
}
/// 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)
.getprop("ro.product.brand")
// `trim` is just-in-case
.map(|s| s.trim().to_string())
.unwrap_or_default()
}
/// Get Android SDK version by querying the
// `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)
.getprop("ro.build.version.sdk")
.map_or(0, |sdk| {
sdk.parse().expect("SDK version numeral must be valid")
})
}
/// Capture the current state of a package across all non-protected users.
/// This is used to detect cross-user behavior by comparing before and after states.
///
/// 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,
target_user_id: u16,
phone: &Phone,
) -> Vec<(u16, PackageState)> {
phone
.user_list
.iter()
.filter(|u| !u.protected && u.id != target_user_id)
.filter_map(|u| {
verify_package_state(package_name, device_serial, Some(u.id)).map(|state| (u.id, state))
})
.collect()
}
/// 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 C: Restore → Restore (package appears on other users)
#[must_use]
pub fn detect_cross_user_behavior(
package_name: &str,
device_serial: &str,
target_user_id: u16,
wanted_state: PackageState,
actual_state: PackageState,
phone: &Phone,
before_states: &[(u16, PackageState)],
) -> Option<String> {
// Only check if operation was successful on target user
if actual_state != wanted_state {
return None;
}
// Only check if we have multiple users
if phone.user_list.len() < 2 {
return None;
}
let after_states =
check_cross_user_package_existence(package_name, device_serial, target_user_id, phone);
match wanted_state {
PackageState::Uninstalled => {
if after_states.is_empty() {
// Case B: Uninstall → Uninstall (check if all users lost package)
let affected_users: Vec<_> = before_states
.iter()
.filter(|(uid, before_state)| {
// Only flag if the package was installed/enabled/disabled before
*before_state != PackageState::Uninstalled
// And is NOT in after_states (doesn't exist in usable state anymore)
&& !after_states.iter().any(|(after_uid, _)| after_uid == uid)
})
.map(|(uid, _)| uid)
.collect();
if affected_users.is_empty() {
None
} else {
let user_list = affected_users
.iter()
.map(|uid| format!("user {uid}"))
.collect::<Vec<_>>()
.join(", ");
Some(format!(
"Detected cross-user uninstall: package was also uninstalled from {user_list} after uninstalling from user {target_user_id}"
))
}
} else {
// Case A: Uninstall → Restore (package appears on other users)
let user_list = after_states
.iter()
.map(|(uid, state)| format!("user {uid} ({state:?})"))
.collect::<Vec<_>>()
.join(", ");
Some(format!(
"Detected cross-user restoration: package exists on {user_list} after uninstalling from user {target_user_id}"
))
}
}
PackageState::Enabled | PackageState::Disabled => {
// Case C: Restore → Restore (package appears on other users)
// Check if a user didn't have the package before (not tracked or package didn't exist).
// Detects packages that appear on users where they didn't exist previously (OEM cross-user restoration).
let was_package_absent_before = |uid: &u16| {
before_states
.iter()
.find(|(before_uid, _)| before_uid == uid)
.is_none_or(|(_, before_state)| *before_state == PackageState::Uninstalled)
};
let newly_appeared: Vec<_> = after_states
.iter()
.filter(|(uid, _after_state)| was_package_absent_before(uid))
.collect();
if newly_appeared.is_empty() {
None
} else {
let user_list = newly_appeared
.iter()
.map(|(uid, state)| format!("user {uid} ({state:?})"))
.collect::<Vec<_>>()
.join(", ");
Some(format!(
"Detected cross-user restoration: package exists on {user_list} after {} from user {target_user_id}",
if wanted_state == PackageState::Enabled {
"enabling"
} else {
"disabling"
}
))
}
}
PackageState::All => None,
}
}
#[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.
/// `true` isn't reliable, you can only trust `false`.
///
/// See:
/// - <https://source.android.com/docs/devices/admin/multi-user#applying_the_overlay>
/// - <https://developer.android.com/reference/android/os/UserManager#supportsMultipleUsers()>
#[must_use]
pub const fn supports_multi_user(dev: &Phone) -> bool {
// >= Lollipop (5.0)
dev.android_sdk >= 21
}
/// Check if a `user_id` is protected on a device by trying
/// to list associated packages.
///
/// If `device_serial` is empty, it lets ADB choose the default device.
pub fn is_protected_user<S: AsRef<str>>(user_id: u16, device_serial: S) -> bool {
AdbCommand::new()
.shell(device_serial)
.pm()
.list_packages_sys(None, Some(user_id))
.is_err()
}
#[must_use]
pub fn list_users_idx_prot(device_serial: &str) -> Vec<User> {
AdbCommand::new()
.shell(device_serial)
.pm()
.list_users()
.map(|out| {
out.into_iter()
.enumerate()
.map(|(i, user)| {
let id = user.get_id();
User {
id,
index: i,
protected: is_protected_user(id, device_serial),
}
})
.collect()
})
.unwrap_or_default()
}
/// This matches serials (`getprop ro.serialno`)
/// that are authorized by the user.
#[must_use]
pub fn get_devices_list() -> Vec<Phone> {
retry(
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![];
if devices.iter().all(|(_, stat)| stat != "device") {
return OperationResult::Retry(vec![]);
}
for device in devices {
let serial = &device.0;
device_list.push(Phone {
model: format!("{} {}", get_device_brand(serial), get_device_model(serial)),
android_sdk: get_android_sdk(serial),
user_list: list_users_idx_prot(serial),
adb_id: serial.clone(),
});
}
OperationResult::Ok(device_list)
}
Err(err) => {
error!("get_devices_list() -> {err}");
let test: Vec<Phone> = vec![];
OperationResult::Retry(test)
}
},
)
.unwrap_or_default()
}
#[must_use]
pub fn initial_load() -> bool {
match AdbCommand::new().devices() {
Ok(_devices) => true,
Err(_err) => false,
}
}
/// 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::adb::{ACommand as AdbCommand, PmListPacksFlag};
// Check if package is enabled
if let Ok(enabled_packages) = AdbCommand::new()
.shell(device_serial)
.pm()
.list_packages_sys(Some(PmListPacksFlag::OnlyEnabled), user_id)
&& enabled_packages.contains(&package_name.to_string())
{
return Some(PackageState::Enabled);
}
// Check if package is disabled
if let Ok(disabled_packages) = AdbCommand::new()
.shell(device_serial)
.pm()
.list_packages_sys(Some(PmListPacksFlag::OnlyDisabled), user_id)
&& disabled_packages.contains(&package_name.to_string())
{
return Some(PackageState::Disabled);
}
// Check if package exists at all (including uninstalled)
if let Ok(all_packages) = AdbCommand::new()
.shell(device_serial)
.pm()
.list_packages_sys(Some(PmListPacksFlag::IncludeUninstalled), user_id)
&& all_packages.contains(&package_name.to_string())
{
return Some(PackageState::Uninstalled);
}
// Package not found at all - it doesn't exist on this device/user
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,
target_user_id: u16,
phone: &Phone,
) -> Vec<(u16, PackageState)> {
let mut other_user_states = Vec::new();
for user in &phone.user_list {
if user.id != target_user_id
&& !user.protected
&& let Some(state) = verify_package_state(package_name, device_serial, Some(user.id))
&& state != PackageState::Uninstalled
{
other_user_states.push((user.id, state));
}
}
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: &CorePackage,
wanted_state: PackageState,
actual_state: PackageState,
user: User,
phone: &Phone,
) -> Result<String, String> {
match (wanted_state, actual_state) {
// Case 1: Tried to uninstall but package was reinstalled -> disable it
(PackageState::Uninstalled, PackageState::Enabled) => {
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 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}"))?;
// 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) => {
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}"))?;
// 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_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
_ => Err(format!(
"No fallback available for wanted state {wanted_state:?} and actual state {actual_state:?}"
)),
}
}

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",
@ -206,23 +209,32 @@ pub fn load_debloat_lists(remote: bool) -> Result<PackageHashMap, PackageHashMap
let mut error = false;
let list: PackageHashMap = if remote {
retry(Fixed::from_millis(1000).take(60), || {
match ureq::get(
&format!("https://raw.githubusercontent.com/Universal-Debloater-Alliance/universal-android-debloater/\
main/resources/assets/{LIST_FNAME}"),
)
match ureq::get(format!(
"https://raw.githubusercontent.com\
/Universal-Debloater-Alliance\
/universal-android-debloater\
/main\
/resources\
/assets\
/{LIST_FNAME}"
))
.call()
{
Ok(data) => {
// TODO: max resp size is 10MB, list is ~1.3MB;
// TODO: https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/discussions/608
#[warn(clippy::expect_used, reason = "this will panic if GH servers rate-limit the user, or many other reasons.")]
let text = data.into_string().expect("response should be Ok type");
Ok(mut data) => {
// https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/discussions/608
let text = data
.body_mut()
.with_config()
.limit(1 << (3 + 10 + 10))
.read_to_string()
.expect("remote list is bigger than 8MiB");
fs::write(cached_uad_lists.clone(), &text).expect("Unable to write file");
let list: PackageHashMap = serde_json::from_str(&text).expect("Unable to parse");
let list: PackageHashMap =
serde_json::from_str(&text).expect("Unable to parse");
OperationResult::Ok(list)
}
Err(e) => {
warn!("Could not load remote debloat list: {}", e);
warn!("Could not load remote debloat list: {e}");
error = true;
OperationResult::Retry(PackageHashMap::new())
}

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,
@ -55,15 +55,14 @@ impl std::fmt::Display for SelfUpdateStatus {
/// Download a file from the internet
#[cfg(feature = "self-update")]
#[allow(clippy::unused_async, reason = "`.call` is equivalent to `.await`")]
pub async fn download_file<T: ToString + Send>(url: T, dest_file: PathBuf) -> Result<(), String> {
let url = url.to_string();
debug!("downloading file from {}", &url);
pub async fn download_file(url: &str, dest_file: PathBuf) -> Result<(), String> {
debug!("downloading file from {url}");
match ureq::get(&url).call() {
Ok(res) => {
match ureq::get(url).call() {
Ok(mut res) => {
let mut file = fs::File::create(dest_file).map_err(|e| e.to_string())?;
if let Err(e) = copy(&mut res.into_reader(), &mut file) {
if let Err(e) = copy(&mut res.body_mut().as_reader(), &mut file) {
return Err(e.to_string());
}
}
@ -77,7 +76,7 @@ pub async fn download_file<T: ToString + Send>(url: T, dest_file: PathBuf) -> Re
/// then returns both the original file name (new version) and temp path (old version)
#[cfg(feature = "self-update")]
pub async fn download_update_to_temp_file(
bin_name: String,
bin_name: &str,
release: Release,
) -> Result<(PathBuf, PathBuf), ()> {
let current_bin_path = std::env::current_exe().map_err(|_| ())?;
@ -110,8 +109,8 @@ pub async fn download_update_to_temp_file(
let archive_path = current_bin_path.parent().ok_or(())?.join(&asset_name);
if let Err(e) = download_file(asset.download_url, archive_path.clone()).await {
error!("Couldn't download {NAME} update: {}", e);
if let Err(e) = download_file(&asset.download_url, archive_path.clone()).await {
error!("Couldn't download {NAME} update: {e}");
return Err(());
}
@ -133,7 +132,7 @@ pub async fn download_update_to_temp_file(
.cloned()
.ok_or(())?;
if let Err(e) = download_file(asset.download_url, download_path.clone()).await {
if let Err(e) = download_file(&asset.download_url, download_path.clone()).await {
error!("Couldn't download {NAME} update: {}", e);
return Err(());
}
@ -147,23 +146,17 @@ pub async fn download_update_to_temp_file(
let mut permissions = fs::metadata(&download_path).map_err(|_| ())?.permissions();
permissions.set_mode(0o755);
if let Err(e) = fs::set_permissions(&download_path, permissions) {
error!("[SelfUpdate] Couldn't set permission to temp file: {}", e);
error!("[SelfUpdate] Couldn't set permission to temp file: {e}");
return Err(());
}
}
if let Err(e) = rename(&current_bin_path, &tmp_path) {
error!(
"[SelfUpdate] Couldn't rename from current to temporary binary path: {}",
e
);
error!("[SelfUpdate] Couldn't rename from current to temporary binary path: {e}");
return Err(());
}
if let Err(e) = rename(&download_path, &current_bin_path) {
error!(
"[SelfUpdate] Couldn't rename from downloaded to current binary path: {}",
e
);
error!("[SelfUpdate] Couldn't rename from downloaded to current binary path: {e}");
return Err(());
}
@ -182,10 +175,10 @@ pub fn get_latest_release() -> Result<Option<Release>, ()> {
pub fn get_latest_release() -> Result<Option<Release>, ()> {
debug!("Checking for {NAME} update");
if let Ok(res) = ureq::get("https://api.github.com/repos/Universal-Debloater-Alliance/universal-android-debloater/releases/latest")
if let Ok(mut res) = ureq::get("https://api.github.com/repos/Universal-Debloater-Alliance/universal-android-debloater/releases/latest")
.call() {
let release: Release = serde_json::from_value(
res.into_json::<serde_json::Value>()
res.body_mut().read_json::<serde_json::Value>()
.map_err(|_| ())?
.clone(),
)
@ -232,7 +225,7 @@ pub fn extract_binary_from_tar(archive_path: &Path, temp_file: &Path) -> io::Res
/// Hardcoded binary names for each compilation target
/// that gets published to the GitHub Release
#[cfg(feature = "self-update")]
pub const fn bin_name() -> &'static str {
pub const BIN_NAME: &str = {
#[cfg(target_os = "windows")]
{
"uad-ng-windows.exe"
@ -252,7 +245,7 @@ pub const fn bin_name() -> &'static str {
{
"uad-ng-linux"
}
}
};
/// Rename a file or directory to a new name, retrying if the operation fails because of permissions
///

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,
@ -17,8 +16,32 @@ use std::{
/// Canonical shortened name of the application
pub const NAME: &str = "UAD-ng";
/// Full name of the application
pub const FULL_NAME: &str = "Universal Android Debloater Next Generation";
pub const EXPORT_FILE_NAME: &str = "selection_export.txt";
/// Returns `true` if `c` matches the regex `\w`
#[inline]
#[must_use]
pub const fn is_w(c: u8) -> bool {
// https://github.com/rust-lang/rust/issues/93279
// https://github.com/rust-lang/rust/issues/83623
(c == b'_') | c.is_ascii_alphanumeric()
}
/// Returns `true` if `s` matches the regex `^\w+$`
#[must_use]
pub const fn is_all_w_c(s: &[u8]) -> bool {
let mut i = 0;
while i < s.len() {
if !is_w(s[i]) {
return false;
}
i += 1;
}
true
}
// Takes a time-stamp parameter,
// for purity and testability.
//
@ -42,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()
@ -68,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) {
@ -94,34 +118,30 @@ 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) {
error!("Can't create directory: {dir:?}");
error!("Can't create directory: {}", dir.display());
panic!("{e}");
};
}
dir
}
/// Open a directory or file with the system's default file manager.
pub fn open_url(dir: PathBuf) {
const OPENER: &str = match std::env::consts::OS.as_bytes() {
b"windows" => "explorer",
@ -132,16 +152,23 @@ pub fn open_url(dir: PathBuf) {
match std::process::Command::new(OPENER).arg(dir).output() {
Ok(o) => {
if !o.status.success() {
// does Windows print UTF-16?
let stderr = String::from_utf8(o.stderr).unwrap().trim_end().to_string();
error!("Can't open the following URL: {}", stderr);
// Use lossy conversion for stderr - some systems (like Windows)
// may output non-UTF8 characters in error messages
let stderr = String::from_utf8_lossy(&o.stderr);
let stderr_trimmed = stderr.trim_end();
if stderr_trimmed.is_empty() {
error!("Can't open URL: command failed with no error message");
} else {
error!("Can't open the following URL: {stderr_trimmed}");
}
}
}
Err(e) => error!("Failed to run command to open the file explorer: {}", e),
Err(e) => error!("Failed to run command to open the file explorer: {e}"),
}
}
#[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(),
@ -149,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;
@ -163,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),
@ -191,10 +214,11 @@ impl fmt::Display for DisplayablePath {
error!("[PATH STEM]: No file stem found");
"[File steam not found]".to_string()
},
|p| match p.to_os_string().into_string() {
Ok(stem) => stem,
Err(e) => {
error!("[PATH ENCODING]: {:?}", e);
|p| {
if let Ok(stem) = p.to_os_string().into_string() {
stem
} else {
error!("[PATH ENCODING]: {}", self.path.display());
"[PATH ENCODING ERROR]".to_string()
}
},
@ -204,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())?;
@ -228,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();
@ -243,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,32 +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::{NAME, string_to_theme};
use iced::advanced::graphics::image::image_rs::ImageFormat;
use iced::font;
use iced::window::icon;
use views::about::{About as AboutView, Message as AboutMessage};
use views::list::{List as AppsView, LoadingState as ListLoadingState, Message as AppsMessage};
use views::settings::{Message as SettingsMessage, Settings as SettingsView};
use widgets::navigation_menu::nav_menu;
use iced::widget::column;
use iced::{
Alignment, Application, Command, Element, Length, Renderer, Settings,
window::Settings as Window,
#[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::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 {
@ -38,11 +38,11 @@ enum View {
#[derive(Default, Clone)]
pub struct UpdateState {
self_update: SelfUpdateState,
uad_list: UadListState,
pub self_update: SelfUpdateState,
pub uad_list: UadListState,
}
#[derive(Default, Clone)]
#[derive(Default)]
pub struct UadGui {
view: View,
apps_view: AppsView,
@ -77,22 +77,28 @@ pub enum Message {
ADBSatisfied(bool),
}
impl Application for UadGui {
type Theme = Theme;
type Executor = iced::executor::Default;
type Message = Message;
type Flags = ();
impl UadGui {
#[allow(
clippy::unused_self,
reason = "required by iced's Application trait interface"
)]
fn title(&self) -> String {
self.selected_device.as_ref().map_or_else(
|| FULL_NAME.to_string(),
|device| format!("{FULL_NAME} - {}", device.model),
)
}
fn new(_flags: ()) -> (Self, Command<Message>) {
fn new() -> (Self, Task<Message>) {
(
Self::default(),
Command::batch([
// Used in crate::gui::widgets::navigation_menu::ICONS. Name is `icomoon`.
font::load(include_bytes!("../../resources/assets/icons.ttf").as_slice())
Task::batch([
// Used in crate::widgets::navigation_menu::ICONS. Name is `icomoon`.
font::load(include_bytes!("../../../resources/assets/icons.ttf").as_slice())
.map(Message::FontLoaded),
Command::perform(initial_load(), Message::ADBSatisfied),
Command::perform(get_devices_list(), Message::LoadDevices),
Command::perform(
Task::perform(async { initial_load() }, Message::ADBSatisfied),
Task::perform(async { get_devices_list() }, Message::LoadDevices),
Task::perform(
async move { get_latest_release() },
Message::GetLatestRelease,
),
@ -100,15 +106,57 @@ impl Application for UadGui {
)
}
fn theme(&self) -> Theme {
string_to_theme(&self.settings_view.general.theme)
#[allow(
clippy::unused_self,
reason = "required by iced's Application trait interface"
)]
fn subscription(&self) -> Subscription<Message> {
if self.selected_device.is_some() {
event::listen_with(Self::keyboard_shortcuts_with_device)
} else {
event::listen_with(Self::keyboard_shortcuts_without_device)
}
}
fn title(&self) -> String {
String::from("Universal Android Debloater Next Generation")
fn keyboard_shortcuts_with_device(
event: iced::Event,
_status: iced::event::Status,
_window: iced::window::Id,
) -> Option<Message> {
Self::keyboard_shortcuts(event, true)
}
#[allow(clippy::too_many_lines)]
fn update(&mut self, msg: Message) -> Command<Message> {
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(
clippy::too_many_lines,
reason = "Root GUI update orchestrates many flows"
)]
fn update(&mut self, msg: Message) -> Task<Message> {
match msg {
Message::LoadDevices(devices_list) => {
self.selected_device = match &self.selected_device {
@ -132,19 +180,19 @@ impl Application for UadGui {
}
Message::AppsPress => {
self.view = View::List;
Command::none()
Task::none()
}
Message::AboutPressed => {
self.view = View::About;
self.update_state.self_update = SelfUpdateState::default();
Command::perform(
Task::perform(
async move { get_latest_release() },
Message::GetLatestRelease,
)
}
Message::SettingsPressed => {
self.view = View::Settings;
Command::none()
Task::none()
}
Message::RefreshButtonPressed => {
self.apps_view = AppsView::default();
@ -154,7 +202,7 @@ impl Application for UadGui {
self.adb_satisfied,
)));
}
Command::perform(get_devices_list(), Message::LoadDevices)
Task::perform(async { get_devices_list() }, Message::LoadDevices)
}
Message::RebootButtonPressed => {
self.apps_view = AppsView::default();
@ -164,7 +212,7 @@ impl Application for UadGui {
};
self.selected_device = None;
self.devices_list = vec![];
Command::perform(
Task::perform(
async { adb::ACommand::new().shell(serial).reboot() },
|_| Message::Nothing,
)
@ -240,18 +288,17 @@ impl Application for UadGui {
{
self.update_state.self_update.status = SelfUpdateStatus::Updating;
self.apps_view.loading_state = ListLoadingState::_UpdatingUad;
let bin_name = bin_name().to_owned();
Command::perform(
download_update_to_temp_file(bin_name, release.clone()),
Task::perform(
download_update_to_temp_file(BIN_NAME, release.clone()),
Message::_NewReleaseDownloaded,
)
} else {
Command::none()
Task::none()
}
#[cfg(not(feature = "self-update"))]
Command::none()
Task::none()
}
AboutMessage::UrlPressed(_) => Command::none(),
AboutMessage::UrlPressed(_) => Task::none(),
}
}
Message::DeviceSelected(s_device) => {
@ -300,15 +347,15 @@ impl Application for UadGui {
{
Ok(_) => {
if let Err(e) = remove_file(cleanup_path) {
error!("Could not remove temp update file: {}", e);
error!("Could not remove temp update file: {e}");
}
std::process::exit(0)
}
Err(error) => {
if let Err(e) = remove_file(cleanup_path) {
error!("Could not remove temp update file: {}", e);
error!("Could not remove temp update file: {e}");
}
error!("Failed to update {NAME}: {}", error);
error!("Failed to update {NAME}: {error}");
}
}
} else {
@ -319,7 +366,7 @@ impl Application for UadGui {
self.update_state.self_update.status = SelfUpdateStatus::Failed;
}
}
Command::none()
Task::none()
}
Message::GetLatestRelease(release) => {
match release {
@ -328,15 +375,15 @@ impl Application for UadGui {
self.update_state.self_update.latest_release = r;
}
Err(()) => self.update_state.self_update.status = SelfUpdateStatus::Failed,
};
Command::none()
}
Task::none()
}
Message::FontLoaded(result) => {
if let Err(error) = result {
error!("Couldn't load font: {error:?}");
}
Command::none()
Task::none()
}
Message::ADBSatisfied(result) => {
self.adb_satisfied = result;
@ -344,11 +391,11 @@ impl Application for UadGui {
self.adb_satisfied,
)))
}
Message::Nothing => Command::none(),
Message::Nothing => Task::none(),
}
}
fn view(&self) -> Element<Self::Message, Self::Theme, Renderer> {
fn view(&self) -> Element<'_, Message, Theme> {
let navigation_container = nav_menu(
&self.devices_list,
self.selected_device.clone(),
@ -374,37 +421,55 @@ impl Application for UadGui {
column![navigation_container, main_container]
.width(Length::Fill)
.align_items(Alignment::Center)
.align_x(Alignment::Center)
.into()
}
fn theme(&self) -> Theme {
string_to_theme(&self.settings_view.general.theme)
}
}
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")
}
};
Self::run(Settings {
id: Some(String::from(NAME)),
window: Window {
iced::application(UadGui::new, UadGui::update, UadGui::view)
.title(UadGui::title)
.theme(UadGui::theme)
.subscription(UadGui::subscription)
.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 {
size: iced::Size {
width: 950.0,
height: 700.0,
},
resizable: true,
decorations: true,
#[cfg(feature = "img")]
icon: icon::from_file_data(logo, Some(ImageFormat::Png)).ok(),
..iced::window::Settings::default()
},
default_text_size: iced::Pixels(16.0),
..Settings::default()
})
})
.run()
}
}

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,16 +25,15 @@ fn main() -> iced::Result {
}
setup_logger().expect("setup logging");
gui::UadGui::start()
UadGui::start()
}
/// Sets up logging to a new file in `CACHE_DIR/UAD`_{time}.log
/// Sets up logging to a new file in `CACHE_DIR"/uadng.log"`
/// Also attaches the terminal on Windows machines
/// '''
/// match `setup_logger().expect("Error` setting up logger")
/// '''
fn setup_logger() -> Result<(), fern::InitError> {
/// Attach Windows terminal, only on Windows
#[cfg(target_os = "windows")]
{
attach_windows_console();
@ -69,20 +63,20 @@ fn setup_logger() -> Result<(), fern::InitError> {
.create(true)
.append(true)
.truncate(false)
.open(CACHE_DIR.join(format!("UAD_{}.log", chrono::Local::now().format("%Y%m%d"))))?;
.open(CACHE_DIR.join("uadng.log"))?;
let file_dispatcher = fern::Dispatch::new()
.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()

776
crates/uad-gui/src/style.rs Normal file
View file

@ -0,0 +1,776 @@
#![allow(
non_snake_case,
clippy::trivially_copy_pass_by_ref,
clippy::wildcard_imports,
reason = "Iced style modules use PascalCase and &Theme; wildcard for local convenience"
)]
use crate::theme::{ColorPalette, Theme};
use iced::widget::{
button, checkbox, container, overlay, pick_list, radio, scrollable, text, text_editor,
text_input,
};
use iced::{Background, Border, Color, Shadow};
// Implement theming catalogs for our custom `Theme` so generic widgets
// like `Button<'_, Message, Theme, Renderer>` compile under iced 0.13.
impl button::Catalog for Theme {
type Class<'a> = button::StyleFn<'a, Theme>;
fn default<'a>() -> <Self as button::Catalog>::Class<'a> {
Box::new(|t: &Theme, s: button::Status| Button::Primary(t, s))
}
fn style(
&self,
class: &<Self as button::Catalog>::Class<'_>,
status: button::Status,
) -> button::Style {
(class)(self, status)
}
}
impl checkbox::Catalog for Theme {
type Class<'a> = checkbox::StyleFn<'a, Theme>;
fn default<'a>() -> <Self as checkbox::Catalog>::Class<'a> {
Box::new(|t: &Theme, s: checkbox::Status| CheckBox::SettingsEnabled(t, s))
}
fn style(
&self,
class: &<Self as checkbox::Catalog>::Class<'_>,
status: checkbox::Status,
) -> checkbox::Style {
(class)(self, status)
}
}
impl container::Catalog for Theme {
type Class<'a> = container::StyleFn<'a, Theme>;
fn default<'a>() -> <Self as container::Catalog>::Class<'a> {
Box::new(|t: &Theme| Container::Background(t))
}
fn style(&self, class: &<Self as container::Catalog>::Class<'_>) -> container::Style {
(class)(self)
}
}
impl scrollable::Catalog for Theme {
type Class<'a> = scrollable::StyleFn<'a, Theme>;
fn default<'a>() -> <Self as scrollable::Catalog>::Class<'a> {
Box::new(|t: &Theme, s: scrollable::Status| Scrollable::Description(t, s))
}
fn style(
&self,
class: &<Self as scrollable::Catalog>::Class<'_>,
status: scrollable::Status,
) -> scrollable::Style {
(class)(self, status)
}
}
impl text::Catalog for Theme {
type Class<'a> = text::StyleFn<'a, Theme>;
fn default<'a>() -> <Self as text::Catalog>::Class<'a> {
Box::new(|t: &Theme| Text::Default(t))
}
fn style(&self, class: &<Self as text::Catalog>::Class<'_>) -> text::Style {
(class)(self)
}
}
// Additional catalogs required by Iced 0.13 generic theming
impl text_input::Catalog for Theme {
type Class<'a> = text_input::StyleFn<'a, Theme>;
fn default<'a>() -> <Self as text_input::Catalog>::Class<'a> {
Box::new(|t: &Theme, s: text_input::Status| {
let p = t.palette();
let active = text_input::Style {
background: Background::Color(p.base.foreground),
border: Border {
color: p.base.foreground,
width: 0.0,
radius: 5.0.into(),
},
icon: Color {
a: 0.5,
..p.normal.primary
},
placeholder: p.normal.surface,
value: p.bright.primary,
selection: p.normal.primary,
};
let focused = text_input::Style {
background: Background::Color(p.base.foreground),
border: Border {
color: Color {
a: 0.5,
..p.normal.primary
},
width: 1.0,
radius: 2.0.into(),
},
icon: Color {
a: 0.5,
..p.normal.primary
},
placeholder: p.normal.surface,
value: p.bright.primary,
selection: p.normal.primary,
};
let disabled = text_input::Style {
background: Background::Color(p.base.background),
border: Border {
color: Color {
a: 0.5,
..p.base.foreground
},
width: 1.0,
radius: 2.0.into(),
},
icon: Color {
a: 0.5,
..p.base.foreground
},
placeholder: p.normal.surface,
value: p.bright.primary,
selection: p.normal.primary,
};
match s {
text_input::Status::Active => active,
text_input::Status::Focused { .. } | text_input::Status::Hovered => focused,
text_input::Status::Disabled => disabled,
}
})
}
fn style(
&self,
class: &<Self as text_input::Catalog>::Class<'_>,
status: text_input::Status,
) -> text_input::Style {
(class)(self, status)
}
}
impl pick_list::Catalog for Theme {
type Class<'a> = pick_list::StyleFn<'a, Theme>;
fn default<'a>() -> <Self as pick_list::Catalog>::Class<'a> {
Box::new(|t: &Theme, s: pick_list::Status| {
let p = t.palette();
let border_color = match s {
pick_list::Status::Hovered => p.normal.primary,
_ => Color {
a: 0.5,
..p.normal.primary
},
};
pick_list::Style {
text_color: p.bright.surface,
placeholder_color: p.bright.surface,
handle_color: p.bright.surface,
background: Background::Color(p.base.background),
border: Border {
color: border_color,
width: 1.0,
radius: 2.0.into(),
},
}
})
}
fn style(
&self,
class: &<Self as pick_list::Catalog>::Class<'_>,
status: pick_list::Status,
) -> pick_list::Style {
(class)(self, status)
}
}
impl overlay::menu::Catalog for Theme {
type Class<'a> = overlay::menu::StyleFn<'a, Theme>;
fn default<'a>() -> <Self as overlay::menu::Catalog>::Class<'a> {
Box::new(|t: &Theme| {
let p = t.palette();
overlay::menu::Style {
text_color: p.bright.surface,
background: p.base.background.into(),
border: Border {
color: p.base.background,
width: 1.0,
radius: 2.0.into(),
},
selected_text_color: p.bright.surface,
selected_background: p.normal.primary.into(),
shadow: Shadow::default(),
}
})
}
fn style(&self, class: &<Self as overlay::menu::Catalog>::Class<'_>) -> overlay::menu::Style {
(class)(self)
}
}
impl radio::Catalog for Theme {
type Class<'a> = radio::StyleFn<'a, Theme>;
fn default<'a>() -> <Self as radio::Catalog>::Class<'a> {
Box::new(|t: &Theme, s: radio::Status| {
let p = t.palette();
let active = radio::Style {
background: Color::TRANSPARENT.into(),
dot_color: p.bright.primary,
border_width: 1.0,
border_color: p.bright.primary,
text_color: None,
};
match s {
radio::Status::Active { .. } => active,
radio::Status::Hovered { .. } => radio::Style {
border_width: 2.0,
..active
},
}
})
}
fn style(
&self,
class: &<Self as radio::Catalog>::Class<'_>,
status: radio::Status,
) -> radio::Style {
(class)(self, status)
}
}
impl text_editor::Catalog for Theme {
type Class<'a> = text_editor::StyleFn<'a, Theme>;
fn default<'a>() -> <Self as text_editor::Catalog>::Class<'a> {
Box::new(|t: &Theme, _s: text_editor::Status| {
let p = t.palette();
text_editor::Style {
background: Background::Color(p.base.foreground),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 0.0.into(),
},
placeholder: p.normal.surface,
value: p.bright.surface,
selection: Color {
a: 0.3,
..p.normal.primary
},
}
})
}
fn style(
&self,
class: &<Self as text_editor::Catalog>::Class<'_>,
status: text_editor::Status,
) -> text_editor::Style {
(class)(self, status)
}
}
// Rule styling for custom Theme (needed by vertical_rule)
impl iced::widget::rule::Catalog for Theme {
type Class<'a> = iced::widget::rule::StyleFn<'a, Theme>;
fn default<'a>() -> <Self as iced::widget::rule::Catalog>::Class<'a> {
Box::new(|t: &Theme| {
let p = t.palette();
iced::widget::rule::Style {
color: p.bright.surface,
radius: 2.0.into(),
fill_mode: iced::widget::rule::FillMode::Full,
snap: true,
}
})
}
fn style(
&self,
class: &<Self as iced::widget::rule::Catalog>::Class<'_>,
) -> iced::widget::rule::Style {
(class)(self)
}
}
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 {
background: Some(Background::Color(p.base.foreground)),
text_color: Some(p.bright.surface),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 5.0.into(),
},
shadow: Shadow::default(),
snap: true,
}
}
#[must_use]
pub fn BorderedFrame(theme: &Theme) -> container::Style {
let p = theme.palette();
container::Style {
background: Some(Background::Color(p.base.foreground)),
text_color: Some(p.bright.surface),
border: Border {
color: p.normal.error,
width: 1.0,
radius: 5.0.into(),
},
shadow: Shadow::default(),
snap: true,
}
}
#[allow(dead_code, reason = "Currently unused in some views")]
#[must_use]
pub fn Tooltip(theme: &Theme) -> container::Style {
let p = theme.palette();
container::Style {
background: Some(Background::Color(p.base.foreground)),
text_color: Some(p.bright.surface),
border: Border {
color: p.normal.primary,
width: 1.0,
radius: 8.0.into(),
},
shadow: Shadow::default(),
snap: true,
}
}
#[must_use]
pub fn Background(theme: &Theme) -> container::Style {
let p = theme.palette();
container::Style {
background: Some(Background::Color(p.base.background)),
text_color: Some(p.bright.surface),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 5.0.into(),
},
shadow: Shadow::default(),
snap: true,
}
}
}
pub mod Button {
use super::*;
#[allow(
dead_code,
reason = "Helper used by multiple styles; may be inlined by compiler"
)]
fn base(border_color: Color) -> button::Style {
button::Style {
background: None,
text_color: Color::WHITE,
border: Border {
color: border_color,
width: 1.0,
radius: 2.0.into(),
},
shadow: Shadow::default(),
snap: true,
}
}
#[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);
if matches!(status, button::Status::Active | button::Status::Pressed) {
style.background = Some(Background::Color(p.base.foreground));
}
style
}
#[allow(
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);
if matches!(status, button::Status::Active | button::Status::Pressed) {
style.background = Some(Background::Color(p.base.foreground));
}
if matches!(status, button::Status::Disabled) {
style.background = Some(Background::Color(Color {
a: 0.05,
..p.normal.primary
}));
style.text_color = p.bright.primary;
}
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);
if matches!(status, button::Status::Active | button::Status::Pressed) {
style.background = Some(Background::Color(p.base.foreground));
}
style
}
#[allow(
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 {
button::Status::Hovered => button::Style {
background: Some(Background::Color(Color {
a: 0.25,
..p.normal.primary
})),
text_color: p.bright.surface,
border: Border {
color: p.base.background,
width: 0.0,
radius: 5.0.into(),
},
shadow: Shadow::default(),
snap: true,
},
_ => button::Style {
background: Some(Background::Color(p.base.foreground)),
text_color: p.bright.surface,
border: Border {
color: p.base.background,
width: 0.0,
radius: 5.0.into(),
},
shadow: Shadow::default(),
snap: true,
},
}
}
#[must_use]
pub fn SelectedPackage(theme: &Theme, _status: button::Status) -> button::Style {
let p = theme.palette();
button::Style {
background: Some(Background::Color(Color {
a: 0.25,
..p.normal.primary
})),
text_color: p.bright.primary,
border: Border {
color: p.normal.primary,
width: 0.0,
radius: 5.0.into(),
},
shadow: Shadow::default(),
snap: true,
}
}
#[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)),
text_color: Color::TRANSPARENT,
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 5.0.into(),
},
shadow: Shadow::default(),
snap: true,
}
}
fn style_active_hover_disabled(
main: Color,
text: Color,
status: button::Status,
) -> button::Style {
match status {
button::Status::Active | button::Status::Pressed => button::Style {
background: Some(Background::Color(main)),
text_color: text,
border: Border {
color: Color { a: 0.5, ..main },
width: 1.0,
radius: 2.0.into(),
},
shadow: Shadow::default(),
snap: true,
},
button::Status::Hovered => button::Style {
background: Some(Background::Color(Color { a: 0.25, ..main })),
text_color: text,
border: Border {
color: Color { a: 0.5, ..main },
width: 1.0,
radius: 2.0.into(),
},
shadow: Shadow::default(),
snap: true,
},
button::Status::Disabled => button::Style {
background: Some(Background::Color(Color { a: 0.05, ..main })),
text_color: Color { a: 0.5, ..text },
border: Border {
color: Color { a: 0.5, ..main },
width: 1.0,
radius: 2.0.into(),
},
shadow: Shadow::default(),
snap: true,
},
}
}
}
pub mod Scrollable {
use super::*;
#[allow(dead_code, reason = "Kept for future custom rails variations")]
fn rails(scroller_color: Color) -> (scrollable::Rail, scrollable::Rail) {
let rail = scrollable::Rail {
background: Some(Background::Color(Color::TRANSPARENT)),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 5.0.into(),
},
scroller: scrollable::Scroller {
background: Background::Color(scroller_color),
border: Border {
color: Color::TRANSPARENT,
width: 1.0,
radius: 5.0.into(),
},
},
};
(rail, rail)
}
fn autoscroll(p: ColorPalette) -> scrollable::AutoScroll {
scrollable::AutoScroll {
background: Background::Color(Color {
a: 0.05,
..p.base.background
}),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 5.0.into(),
},
shadow: Shadow::default(),
icon: p.bright.surface,
}
}
#[must_use]
pub fn Description(theme: &Theme, _status: scrollable::Status) -> scrollable::Style {
let p = theme.palette();
let (v, h) = rails(p.normal.surface);
scrollable::Style {
container: container::Style::default(),
vertical_rail: v,
horizontal_rail: h,
gap: Some(Background::Color(Color::TRANSPARENT)),
auto_scroll: autoscroll(p),
}
}
#[must_use]
pub fn Packages(theme: &Theme, _status: scrollable::Status) -> scrollable::Style {
let p = theme.palette();
let (v, h) = rails(p.base.foreground);
scrollable::Style {
container: container::Style::default(),
vertical_rail: v,
horizontal_rail: h,
gap: Some(Background::Color(Color::TRANSPARENT)),
auto_scroll: autoscroll(p),
}
}
}
pub mod CheckBox {
use super::*;
#[must_use]
pub fn PackageEnabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
let p = theme.palette();
checkbox::Style {
background: Background::Color(p.base.background),
icon_color: p.bright.primary,
border: Border {
color: p.base.background,
width: 1.0,
radius: 5.0.into(),
},
text_color: Some(p.bright.surface),
}
}
#[must_use]
pub fn PackageDisabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
let p = theme.palette();
checkbox::Style {
background: Background::Color(Color {
a: 0.55,
..p.base.background
}),
icon_color: p.bright.primary,
border: Border {
color: p.normal.primary,
width: 1.0,
radius: 5.0.into(),
},
text_color: Some(p.normal.primary),
}
}
#[must_use]
pub fn SettingsEnabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
let p = theme.palette();
checkbox::Style {
background: Background::Color(p.base.background),
icon_color: p.bright.primary,
border: Border {
color: p.bright.primary,
width: 1.0,
radius: 5.0.into(),
},
text_color: Some(p.bright.surface),
}
}
#[must_use]
pub fn SettingsDisabled(theme: &Theme, _status: checkbox::Status) -> checkbox::Style {
let p = theme.palette();
checkbox::Style {
background: Background::Color(p.base.foreground),
icon_color: p.bright.primary,
border: Border {
color: p.normal.primary,
width: 1.0,
radius: 5.0.into(),
},
text_color: Some(p.bright.surface),
}
}
}
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 {
color: Some(p.bright.secondary),
}
}
#[must_use]
pub fn Danger(theme: &Theme) -> text::Style {
let p = theme.palette();
text::Style {
color: Some(p.bright.error),
}
}
#[must_use]
pub fn Commentary(theme: &Theme) -> text::Style {
let p = theme.palette();
text::Style {
color: Some(p.normal.surface),
}
}
#[allow(
dead_code,
reason = "Convenience factory used by dynamic text coloring"
)]
pub fn Color(c: Color) -> impl Fn(&Theme) -> text::Style {
move |_t: &Theme| text::Style { color: Some(c) }
}
}
// Unit tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_palette() {
let palette = Theme::Dark.palette();
assert_ne!(palette.base.background, palette.base.foreground);
assert_ne!(palette.normal.primary, Color::BLACK);
assert_ne!(palette.normal.surface, Color::BLACK);
assert_ne!(palette.bright.primary, Color::BLACK);
// if `LIGHT` then this can be `BLACK`
// https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/pull/730#issuecomment-2525405134
//assert_ne!(palette.bright.surface, Color::BLACK);
assert_ne!(palette.normal.error, Color::BLACK);
assert_ne!(palette.bright.error, Color::BLACK);
}
}

View file

@ -1,4 +1,4 @@
use dark_light;
use iced::theme::{self, Mode, Palette, Style};
use iced::{Color, color};
use std::sync::LazyLock;
@ -37,7 +37,7 @@ pub struct BaseColors {
#[derive(Debug, Clone, Copy)]
pub struct NormalColors {
pub primary: Color,
#[allow(dead_code)]
#[allow(dead_code, reason = "Reserved for future palette updates")]
pub secondary: Color,
pub surface: Color,
pub error: Color,
@ -149,3 +149,71 @@ 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 {
Mode::Light => Self::Light,
Mode::Dark => Self::Dark,
Mode::None => match *OS_COLOR_SCHEME {
dark_light::Mode::Light => Self::Light,
dark_light::Mode::Dark | dark_light::Mode::Unspecified => Self::Dark,
},
}
}
fn mode(&self) -> Mode {
let resolved = match self {
Self::Auto => match *OS_COLOR_SCHEME {
dark_light::Mode::Light => Self::Light,
dark_light::Mode::Dark | dark_light::Mode::Unspecified => Self::Dark,
},
other => *other,
};
match resolved {
Self::Light => Mode::Light,
_ => Mode::Dark,
}
}
fn base(&self) -> Style {
let p = Self::palette(*self);
Style {
background_color: p.base.background,
text_color: p.bright.surface,
}
}
fn palette(&self) -> Option<Palette> {
let p = Self::palette(*self);
Some(Palette {
background: p.base.background,
text: p.bright.surface,
primary: p.normal.primary,
success: p.bright.secondary,
warning: p.normal.surface,
danger: p.normal.error,
})
}
fn name(&self) -> &str {
match self {
Self::Dark => "Dark",
Self::Light => "Light",
Self::Lupin => "Lupin",
Self::Auto => "Auto",
}
}
}

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::{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 {}
@ -23,15 +24,23 @@ pub enum Message {
}
impl About {
#[allow(
clippy::unused_self,
reason = "Trait-like shape required by GUI architecture"
)]
pub fn update(&mut self, msg: Message) {
if let Message::UrlPressed(url) = msg {
open_url(url);
}
// other events are handled by UadGui update()
}
pub fn view(&self, update_state: &UpdateState) -> Element<Message, Theme, Renderer> {
#[allow(
clippy::unused_self,
reason = "Trait-like shape required by GUI architecture"
)]
pub fn view(&self, update_state: &UpdateState) -> Element<'_, Message, Theme, Renderer> {
let about_text = text(format!(
"Universal Android Debloater Next Generation ({NAME}) is a free and open-source community project \naiming at simplifying the removal of pre-installed apps on any Android device."
"{FULL_NAME} ({NAME}) is a free and open-source community project \naiming at simplifying the removal of pre-installed apps on any Android device."
));
let descr_container = container(about_text)
@ -76,13 +85,13 @@ impl About {
let last_self_update_text = text(self_update_text).style(style::Text::Default);
row![uad_version_text, self_update_btn, last_self_update_text,]
.align_items(Alignment::Center)
.align_y(Alignment::Center)
.spacing(10)
.width(550)
};
let uad_list_row = row![uad_list_text, uad_lists_btn, last_update_text,]
.align_items(Alignment::Center)
.align_y(Alignment::Center)
.spacing(10)
.width(550);
@ -94,34 +103,36 @@ impl About {
the numbers will be out of sync!
However, the server will still be the "old" version
until the next start
until it's killed
*/
let adb_version_text = text(
adb::ACommand::new()
.version()
.map_err(|e| error!("{e}"))
.ok()
// 1st line is the relevant one.
// 2nd could be useful, too
.unwrap_or_default()[0]
// there must be some way to avoid this...
.clone(),
)
let adb_version_text = text(match adb::ACommand::new().version() {
Ok(s) => s
.lines()
.nth(0)
.unwrap_or_else(|| unreachable!())
// This allocation is good.
// If it was a ref, the app would hold the entire string
// instead of the relevant slice.
.to_string(),
Err(e) => {
error!("{e}");
"Couldn't fetch ADB version. Is it installed?".into()
// satisfy `match` by inferring the type of the `Ok` arm
}
})
.width(250);
let adb_version_row = row![adb_version_text]
.align_items(Alignment::Center)
.width(550);
let adb_version_row = row![adb_version_text].align_y(Alignment::Center).width(550);
#[cfg(feature = "self-update")]
let update_column = column![uad_list_row, self_update_row, adb_version_row];
#[cfg(not(feature = "self-update"))]
let update_column = column![uad_list_row, adb_version_row];
let update_column = update_column.align_items(Alignment::Center).spacing(10);
let update_column = update_column.align_x(Alignment::Center).spacing(10);
let update_container = container(update_column)
.width(Length::Fill)
.center_x()
.center_x(Length::Fill)
.padding(10)
.style(style::Container::Frame);
@ -145,14 +156,14 @@ impl About {
let row = row![website_btn, wiki_btn, issue_btn, log_btn,].spacing(20);
let content = column![
Space::new(Length::Fill, Length::Shrink),
Space::new().width(Length::Fill).height(Length::Shrink),
descr_container,
update_container,
row,
]
.width(Length::Fill)
.spacing(20)
.align_items(Alignment::Center);
.align_x(Alignment::Center);
container(content)
.width(Length::Fill)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,694 @@
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,
widgets::navigation_menu::ICONS,
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 {
ExportUninstalled,
}
#[derive(Debug, Clone)]
pub struct Settings {
pub general: GeneralSettings,
pub device: DeviceSettings,
is_loading: bool,
modal: Option<PopUpModal>,
}
impl Default for Settings {
fn default() -> Self {
Self {
general: Config::load_configuration_file().general,
device: DeviceSettings::default(),
is_loading: false,
modal: None,
}
}
}
#[derive(Debug, Clone)]
pub enum Message {
LoadDeviceSettings,
ExpertMode(bool),
DisableMode(bool),
MultiUserMode(bool),
ApplyTheme(Theme),
UrlPressed(PathBuf),
BackupSelected(DisplayablePath),
BackupDevice,
RestoreDevice,
RestoringDevice(Result<PackageInfo, AdbError>),
DeviceBackedUp(Result<bool, String>),
ChooseBackUpFolder,
FolderChosen(Result<PathBuf, Error>),
ExportPackages,
PackagesExported(Result<bool, String>),
ModalHide,
}
impl Settings {
pub fn update(
&mut self,
phone: &Phone,
packages: &[Vec<PackageRow>],
nb_running_async_adb_commands: &mut u32,
msg: Message,
selected_user: Option<User>,
) -> Task<Message> {
match msg {
Message::ModalHide => self.handle_modal_hide(),
Message::ExpertMode(toggled) => self.handle_expert_mode(phone, toggled),
Message::DisableMode(toggled) => self.handle_disable_mode(phone, toggled),
Message::MultiUserMode(toggled) => self.handle_multi_user_mode(phone, toggled),
Message::ApplyTheme(theme) => self.handle_apply_theme(phone, theme),
Message::UrlPressed(url) => Self::handle_url_pressed(url),
Message::LoadDeviceSettings => self.handle_load_device_settings(phone),
Message::BackupSelected(d_path) => self.handle_backup_selected(d_path),
Message::BackupDevice => self.handle_backup_device(phone, packages),
Message::DeviceBackedUp(result) => self.handle_device_backed_up(phone, result),
Message::RestoreDevice => {
self.handle_restore_device(phone, packages, nb_running_async_adb_commands)
}
Message::RestoringDevice(_) => Task::none(),
Message::FolderChosen(result) => self.handle_folder_chosen(phone, result),
Message::ChooseBackUpFolder => self.handle_choose_backup_folder(),
Message::ExportPackages => Self::handle_export_packages(selected_user, packages),
Message::PackagesExported(result) => self.handle_packages_exported(result),
}
}
fn handle_modal_hide(&mut self) -> Task<Message> {
self.modal = None;
Task::none()
}
fn handle_expert_mode(&mut self, _phone: &Phone, toggled: bool) -> Task<Message> {
self.general.expert_mode = toggled;
debug!("Config change: {self:?}");
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 supports_disabling(phone) {
self.device.disable_mode = toggled;
debug!("Config change: {self:?}");
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> {
self.device.multi_user_mode = toggled;
debug!("Config change: {self:?}");
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> {
self.general.theme = theme.to_string();
debug!("Config change: {self:?}");
let mut config = Config::load_configuration_file();
config.save_device_settings(self.device.clone(), self.general.clone());
Task::none()
}
fn handle_url_pressed(url: PathBuf) -> Task<Message> {
open_url(url);
Task::none()
}
fn handle_load_device_settings(&mut self, phone: &Phone) -> Task<Message> {
self.load_device_settings(phone);
Task::none()
}
fn load_device_settings(&mut self, phone: &Phone) {
let backups = list_available_backups(&self.general.backup_folder.join(&phone.adb_id));
let backup = BackupSettings {
backups: backups.clone(),
selected: backups.first().cloned(),
users: phone.user_list.clone(),
selected_user: phone.user_list.first().copied(),
backup_state: String::default(),
};
match Config::load_configuration_file()
.devices
.iter()
.find(|d| d.device_id == phone.adb_id)
{
Some(device) => {
self.device.clone_from(device);
self.device.backup = backup;
}
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),
backup,
};
}
}
}
fn handle_backup_selected(&mut self, d_path: DisplayablePath) -> Task<Message> {
self.device.backup.selected = Some(d_path.clone());
self.device.backup.users = list_available_backup_user(d_path);
Task::none()
}
fn handle_backup_device(
&mut self,
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(
async move { backup_phone(user_list, device_id, &packages_owned) },
Message::DeviceBackedUp,
)
}
fn handle_device_backed_up(
&mut self,
phone: &Phone,
result: Result<bool, String>,
) -> Task<Message> {
match result {
Ok(_) => {
info!("[BACKUP] Backup successfully created");
self.device.backup.backups =
list_available_backups(&self.general.backup_folder.join(phone.adb_id.clone()));
self.device.backup.selected = self.device.backup.backups.first().cloned();
}
Err(err) => {
error!("[BACKUP FAILED] Backup creation failed: {err:?}");
}
}
Task::none()
}
fn handle_restore_device(
&mut self,
phone: &Phone,
packages: &[Vec<PackageRow>],
nb_running_async_adb_commands: &mut u32,
) -> Task<Message> {
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;
for p in &restore_result.packages {
let p_info = PackageInfo {
i_user: p.i_user,
index: p.index,
removal: "RESTORE".to_string(),
before_cross_user_states: vec![],
};
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(
async move {
run_adb_shell_action(phone_id, command.as_str())
.map(|_| p_info_clone)
},
Message::RestoringDevice,
));
}
}
if restore_result.skipped_count > 0 {
self.device.backup.backup_state = format!(
"Restore completed with {} packages skipped (not found on device)",
restore_result.skipped_count
);
} else if restore_result.packages.is_empty() {
if get_android_sdk(&phone.adb_id) == 0 {
self.device.backup.backup_state = "Device is not connected".to_string();
} else {
self.device.backup.backup_state =
"Device state is already restored".to_string();
}
} else {
self.device.backup.backup_state = "Restore completed successfully".to_string();
}
info!(
"[RESTORE] Restoring backup {}",
self.device.backup.selected.as_ref().unwrap()
);
Task::batch(commands)
}
Err(e) => {
self.device.backup.backup_state.clone_from(&e);
error!("{} - {}", self.device.backup.selected.as_ref().unwrap(), e);
Task::none()
}
}
}
fn handle_folder_chosen(
&mut self,
phone: &Phone,
result: Result<PathBuf, Error>,
) -> Task<Message> {
self.is_loading = false;
if let Ok(path) = result {
self.general.backup_folder = path;
let mut config = Config::load_configuration_file();
config.save_device_settings(self.device.clone(), self.general.clone());
self.load_device_settings(phone);
}
Task::none()
}
fn handle_choose_backup_folder(&mut self) -> Task<Message> {
if self.is_loading {
Task::none()
} else {
self.is_loading = true;
Task::perform(open_folder(), Message::FolderChosen)
}
}
fn handle_export_packages(
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(
async move { export_packages(selected_user.unwrap_or_default(), &core_packages) },
Message::PackagesExported,
)
}
fn handle_packages_exported(&mut self, result: Result<bool, String>) -> Task<Message> {
match result {
Ok(_) => self.modal = Some(PopUpModal::ExportUninstalled),
Err(err) => error!("Failed to export list of uninstalled packages: {err:?}"),
}
Task::none()
}
pub fn view(
&self,
phone: &Phone,
apps_view: &AppsView,
) -> Element<'_, Message, Theme, Renderer> {
let content = if phone.adb_id.is_empty() {
self.build_no_device_content()
} else {
self.build_device_content(phone, apps_view)
};
if let Some(PopUpModal::ExportUninstalled) = self.modal {
return Self::render_export_modal(content);
}
container(scrollable(content))
.padding(10)
.width(Length::Fill)
.height(Length::Fill)
.into()
}
fn build_no_device_content(&self) -> Element<'_, Message, Theme, Renderer> {
column![
text("Theme").size(26),
self.theme_container(),
text("General").size(26),
self.general_container(),
text("Current device").size(26),
Self::no_device_container(),
text("Backup / Restore").size(26),
Self::no_device_container(),
]
.width(Length::Fill)
.spacing(20)
.into()
}
fn build_device_content(
&self,
phone: &Phone,
apps_view: &AppsView,
) -> Element<'_, Message, Theme, Renderer> {
column![
text("Theme").size(26),
self.theme_container(),
text("General").size(26),
self.general_container(),
text("Current device").size(26),
Self::warning_container(phone),
self.device_specific_container(phone),
text("Backup / Restore").size(26),
self.backup_restore_container(phone, apps_view),
]
.width(Length::Fill)
.spacing(20)
.into()
}
fn theme_container(&self) -> Element<'_, Message, Theme, Renderer> {
let radio_btn_theme = Theme::ALL
.iter()
.fold(row![].spacing(10), |column, option| {
column.push(
radio(
format!("{}", option.clone()),
*option,
Some(string_to_theme(&self.general.theme)),
Message::ApplyTheme,
)
.size(24),
)
});
container(radio_btn_theme)
.padding(10)
.width(Length::Fill)
.height(Length::Shrink)
.style(style::Container::Frame)
.into()
}
fn general_container(&self) -> Element<'_, Message, Theme, Renderer> {
let expert_mode_checkbox = checkbox(self.general.expert_mode)
.label("Allow to uninstall packages marked as \"unsafe\" (I KNOW WHAT I AM DOING)")
.on_toggle(Message::ExpertMode)
.size(20)
.style(style::CheckBox::SettingsEnabled);
let expert_mode_descr =
text("Most unsafe packages are known to bootloop the device if removed.")
.style(style::Text::Commentary);
let choose_backup_descr = text("Note: If you have previous backups, you will need to transfer them manually to newly changed backup folder to be able to use Restore functionality")
.style(style::Text::Commentary);
let choose_backup_btn = button(text("\u{E930}").font(ICONS))
.padding([5, 10])
.on_press(Message::ChooseBackUpFolder)
.style(style::Button::Primary);
let choose_backup_row = row![
choose_backup_btn,
"Choose backup folder",
Space::new().width(Length::Fill).height(Length::Shrink),
"Current folder: ",
text(self.general.backup_folder.to_string_lossy())
]
.spacing(10)
.align_y(Alignment::Center);
container(
column![
expert_mode_checkbox,
expert_mode_descr,
choose_backup_row,
choose_backup_descr,
]
.spacing(10),
)
.padding(10)
.width(Length::Fill)
.height(Length::Shrink)
.style(style::Container::Frame)
.into()
}
fn warning_container(phone: &Phone) -> Element<'static, Message, Theme, Renderer> {
container(
row![
text("The following settings only affect the currently selected device:")
.style(style::Text::Danger),
text(phone.model.clone()),
Space::new().width(Length::Fill).height(Length::Shrink),
text(phone.adb_id.clone()).style(style::Text::Commentary)
]
.spacing(7),
)
.padding(10)
.width(Length::Fill)
.style(style::Container::BorderedFrame)
.into()
}
fn device_specific_container(&self, phone: &Phone) -> Element<'_, Message, Theme, Renderer> {
let multi_user_mode_descr = row![
text("This will not affect the following protected work profile users: ")
.style(style::Text::Commentary),
text(
phone
.user_list
.iter()
.filter(|&u| u.protected)
.map(|u| u.id.to_string())
.collect::<Vec<String>>()
.join(", ")
)
.style(style::Text::Danger)
];
let multi_user_mode_checkbox = checkbox(self.device.multi_user_mode)
.label("Affect all the users of the device (not only the selected user)")
.on_toggle(Message::MultiUserMode)
.size(20)
.style(style::CheckBox::SettingsEnabled);
let disable_checkbox_style = if supports_disabling(phone) {
style::CheckBox::SettingsEnabled
} else {
style::CheckBox::SettingsDisabled
};
let disable_mode_descr =
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))
.on_press(Message::UrlPressed(PathBuf::from(
"https://github.com/Universal-Debloater-Alliance/universal-android-debloater/wiki/FAQ#\
why-is-the-disable-mode-setting-not-available-for-my-device",
)))
.height(22)
.style(style::Button::Unavailable);
let disable_mode_checkbox = checkbox(self.device.disable_mode)
.label("Clear and disable packages instead of uninstalling them")
.on_toggle(Message::DisableMode)
.size(20)
.style(disable_checkbox_style);
let disable_setting_row = if supports_disabling(phone) {
row![
disable_mode_checkbox,
Space::new().width(Length::Fill).height(Length::Shrink),
]
.width(Length::Fill)
} else {
row![
disable_mode_checkbox,
Space::new().width(Length::Fill).height(Length::Shrink),
unavailable_btn,
]
.width(Length::Fill)
};
container(
column![
multi_user_mode_checkbox,
multi_user_mode_descr,
disable_setting_row,
disable_mode_descr,
]
.spacing(10),
)
.padding(10)
.width(Length::Fill)
.height(Length::Shrink)
.style(style::Container::Frame)
.into()
}
fn backup_restore_container(
&self,
phone: &Phone,
apps_view: &AppsView,
) -> Element<'_, Message, Theme, Renderer> {
let backup_pick_list = pick_list(
self.device.backup.backups.clone(),
self.device.backup.selected.clone(),
Message::BackupSelected,
)
.padding(6);
let backup_btn = button_primary(text("Backup").align_x(alignment::Horizontal::Center))
.on_press(Message::BackupDevice)
.width(77);
let restore_btn = |enabled| {
if enabled {
button(text("Restore").align_x(alignment::Horizontal::Center))
.padding([5, 10])
.on_press(Message::RestoreDevice)
.width(77)
} else {
button(
text("No backup")
.align_x(alignment::Horizontal::Center)
.align_y(alignment::Vertical::Center),
)
.padding([5, 10])
.width(77)
}
};
let locate_backup_btn = if self.device.backup.backups.is_empty() {
button_primary("Open backup directory")
} else {
button_primary("Open backup directory").on_press(Message::UrlPressed(
self.general.backup_folder.join(phone.adb_id.clone()),
))
};
let export_btn = button_primary("Export").on_press(Message::ExportPackages);
let backup_row = row![
backup_btn,
"Backup the current state of the phone",
Space::new().width(Length::Fill).height(Length::Shrink),
locate_backup_btn,
]
.spacing(10)
.align_y(Alignment::Center);
let restore_row = if self.device.backup.backups.is_empty() {
row![]
} else {
row![
restore_btn(true),
"Restore the state of the device",
Space::new().width(Length::Fill).height(Length::Shrink),
text(self.device.backup.backup_state.clone()).style(style::Text::Danger),
backup_pick_list,
]
.spacing(10)
.align_y(Alignment::Center)
};
let export_row = row![
export_btn,
"Export uninstalled packages with their description",
Space::new().width(Length::Fill).height(Length::Shrink),
text(format!(
"Selected: user {}",
apps_view.selected_user.unwrap_or_default().id
)),
]
.spacing(10)
.align_y(Alignment::Center);
container(column![backup_row, restore_row, export_row].spacing(10))
.padding(10)
.width(Length::Fill)
.height(Length::Shrink)
.style(style::Container::Frame)
.into()
}
fn no_device_container() -> Element<'static, Message, Theme, Renderer> {
container(text("No device detected").style(style::Text::Danger))
.padding(10)
.width(Length::Fill)
.style(style::Container::BorderedFrame)
.into()
}
fn render_export_modal<'a>(
content: Element<'a, Message, Theme, Renderer>,
) -> Element<'a, Message, Theme, Renderer> {
let title = container(row![text("Success").size(24)].align_y(Alignment::Center))
.width(Length::Fill)
.style(style::Container::Frame)
.padding([10, 0])
.center_y(Length::Shrink)
.center_x(Length::Shrink);
let text_box = row![
text(format!(
"Exported uninstalled packages into file.\nFile is exported in same directory where {NAME} is located."
))
.width(Length::Fill),
]
.padding(20);
let file_row =
row![text(generate_backup_name(chrono::Local::now())).style(style::Text::Commentary)]
.padding(20);
let modal_btn_row = row![
Space::new().width(Length::Fill).height(Length::Shrink),
button(text("Close").width(Length::Shrink))
.width(Length::Shrink)
.on_press(Message::ModalHide),
Space::new().width(Length::Fill).height(Length::Shrink),
];
let ctn = container(column![title, text_box, file_row, modal_btn_row])
.height(Length::Shrink)
.width(500)
.padding(10)
.style(style::Container::Frame);
let padded_content: Element<'a, Message, Theme, Renderer> =
container(content).padding(10).into();
Modal::new(padded_content, ctn)
.on_blur(Message::ModalHide)
.into()
}
}

View file

@ -1,7 +1,7 @@
use iced::advanced::widget::{self, Tree, Widget};
use iced::advanced::{Clipboard, Layout, Shell, layout, overlay, renderer};
use iced::mouse::{self, Cursor};
use iced::{Alignment, Color, Element, Event, Length, Point, Rectangle, Size, advanced, event};
use iced::{Alignment, Color, Element, Event, Length, Point, Rectangle, Size, Vector, advanced};
/// A widget that centers a modal element over some base element
pub struct Modal<'a, Message, Theme, Renderer> {
@ -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),
@ -52,28 +53,28 @@ where
}
fn layout(
&self,
&mut self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.base
.as_widget()
.as_widget_mut()
.layout(&mut tree.children[0], renderer, limits)
}
fn on_event(
fn update(
&mut self,
state: &mut Tree,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor_position: Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
self.base.as_widget_mut().on_event(
) {
self.base.as_widget_mut().update(
&mut state.children[0],
event,
layout,
@ -82,7 +83,7 @@ where
clipboard,
shell,
viewport,
)
);
}
fn draw(
@ -109,9 +110,10 @@ where
fn overlay<'b>(
&'b mut self,
state: &'b mut Tree,
layout: Layout<'_>,
layout: Layout<'b>,
_renderer: &Renderer,
_translation: iced::Vector,
viewport: &Rectangle,
_translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
Some(overlay::Element::new(Box::new(Overlay {
position: layout.position(),
@ -119,6 +121,7 @@ where
tree: &mut state.children[1],
size: layout.bounds().size(),
on_blur: self.on_blur.clone(),
viewport: *viewport,
})))
}
@ -140,14 +143,14 @@ where
}
fn operate(
&self,
&mut self,
state: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
operation: &mut dyn widget::Operation,
) {
self.base
.as_widget()
.as_widget_mut()
.operate(&mut state.children[0], layout, renderer, operation);
}
}
@ -158,6 +161,7 @@ struct Overlay<'a, 'b, Message, Theme, Renderer> {
tree: &'b mut Tree,
size: Size,
on_blur: Option<Message>,
viewport: Rectangle,
}
impl<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
@ -173,42 +177,41 @@ where
let child = self
.content
.as_widget()
.as_widget_mut()
.layout(self.tree, renderer, &limits)
.align(Alignment::Center, Alignment::Center, limits.max());
layout::Node::with_children(self.size, vec![child]).move_to(self.position)
}
fn on_event(
fn update(
&mut self,
event: Event,
event: &Event,
layout: Layout<'_>,
cursor: Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
if let Some(message) = self.on_blur.as_ref() {
if matches!(
) {
if let Some(message) = self.on_blur.as_ref()
&& matches!(
event,
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
) {
if let Some(cursor_position) = cursor.position() {
let content_bounds = layout
.children()
.next()
.expect("Layout must have at least 1 child")
.bounds();
if !content_bounds.contains(cursor_position) {
shell.publish(message.clone());
return event::Status::Captured;
}
}
)
&& let Some(cursor_position) = cursor.position()
{
let content_bounds = layout
.children()
.next()
.expect("Layout must have at least 1 child")
.bounds();
if !content_bounds.contains(cursor_position) {
shell.publish(message.clone());
return;
}
}
self.content.as_widget_mut().on_event(
self.content.as_widget_mut().update(
self.tree,
event,
layout.children().next().unwrap(),
@ -216,8 +219,8 @@ where
renderer,
clipboard,
shell,
&layout.bounds(),
)
&self.viewport,
);
}
fn draw(
@ -254,9 +257,9 @@ where
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation<Message>,
operation: &mut dyn widget::Operation,
) {
self.content.as_widget().operate(
self.content.as_widget_mut().operate(
self.tree,
layout.children().next().unwrap(),
renderer,
@ -268,14 +271,13 @@ where
&self,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
self.content.as_widget().mouse_interaction(
self.tree,
layout.children().next().unwrap(),
cursor,
viewport,
&self.viewport,
renderer,
)
}

View file

@ -1,19 +1,22 @@
use crate::core::helpers::button_primary;
pub use crate::core::sync::Phone;
use crate::core::theme::Theme;
use crate::core::update::{SelfUpdateState, SelfUpdateStatus};
pub use crate::gui::views::about::Message as AboutMessage;
pub use crate::gui::views::list::{List as AppsView, LoadingState as ListLoadingState};
use crate::gui::{Message, style, widgets::text};
use crate::helpers::button_primary;
use crate::theme::Theme;
pub use crate::views::about::Message as AboutMessage;
pub use crate::views::list::{List as AppsView, LoadingState as ListLoadingState};
use crate::{gui::Message, style, widgets::text};
use iced::widget::{Space, button, container, pick_list, row, tooltip};
use iced::{Alignment, Element, Font, Length, Renderer, alignment, font};
pub use uad_core::sync::Phone;
use uad_core::update::{SelfUpdateState, SelfUpdateStatus};
/// resources/assets/icons.ttf, loaded in [`crate::gui::UadGui`]
pub const ICONS: Font = Font {
family: font::Family::Name("icomoon"),
..Font::DEFAULT
};
#[allow(
clippy::too_many_lines,
reason = "Navigation layout composes many widgets inline"
)]
pub fn nav_menu<'a>(
device_list: &'a [Phone],
selected_device: Option<Phone>,
@ -24,16 +27,28 @@ pub fn nav_menu<'a>(
text("\u{E900}")
.font(ICONS)
.width(22)
.horizontal_alignment(alignment::Horizontal::Center),
.align_x(alignment::Horizontal::Center),
)
.on_press(Message::RefreshButtonPressed);
let apps_refresh_tooltip = tooltip(apps_refresh_btn, "Refresh apps", tooltip::Position::Bottom)
.style(style::Container::Tooltip)
.gap(4);
let apps_refresh_tooltip = tooltip(
apps_refresh_btn,
"Refresh apps (Ctrl+Shift+5)",
tooltip::Position::Bottom,
)
.style(style::Container::Tooltip)
.gap(4);
let reboot_btn = button_primary("Reboot").on_press(Message::RebootButtonPressed);
let reboot_btn = tooltip(
reboot_btn,
"Reboot device (Ctrl+Shift+R)",
tooltip::Position::Bottom,
)
.style(style::Container::Tooltip)
.gap(4);
let uad_version_text = if let Some(r) = &self_update_state.latest_release {
match self_update_state.status {
SelfUpdateStatus::Failed => text(format!("Failed to update to {}", r.tag_name)),
@ -60,16 +75,28 @@ pub fn nav_menu<'a>(
let apps_btn = button_primary("Apps").on_press(Message::AppsPress);
let apps_btn = tooltip(apps_btn, "(Ctrl+Shift+A)", tooltip::Position::Bottom)
.style(style::Container::Tooltip)
.gap(4);
let about_btn = button_primary("About").on_press(Message::AboutPressed);
let about_btn = tooltip(about_btn, "(Ctrl+Shift+I)", tooltip::Position::Bottom)
.style(style::Container::Tooltip)
.gap(4);
let settings_btn = button_primary(
text("\u{E994}")
.font(ICONS)
.width(22)
.horizontal_alignment(alignment::Horizontal::Center),
.align_x(alignment::Horizontal::Center),
)
.on_press(Message::SettingsPressed);
let settings_btn = tooltip(settings_btn, "(Ctrl+Shift+S)", tooltip::Position::Bottom)
.style(style::Container::Tooltip)
.gap(4);
let device_list_text = match apps_view.loading_state {
ListLoadingState::FindingPhones => text("Finding connected devices..."),
_ => text("No devices/emulators found"),
@ -80,7 +107,7 @@ pub fn nav_menu<'a>(
reboot_btn,
apps_refresh_tooltip,
pick_list(device_list, Some(phone), Message::DeviceSelected,),
Space::new(Length::Fill, Length::Shrink),
Space::new().width(Length::Fill).height(Length::Shrink),
uad_version_text,
update_btn,
apps_btn,
@ -88,13 +115,13 @@ pub fn nav_menu<'a>(
settings_btn,
]
.width(Length::Fill)
.align_items(Alignment::Center)
.align_y(Alignment::Center)
.spacing(10),
None => row![
reboot_btn,
apps_refresh_tooltip,
device_list_text,
Space::new(Length::Fill, Length::Shrink),
Space::new().width(Length::Fill).height(Length::Shrink),
uad_version_text,
update_btn,
apps_btn,
@ -102,7 +129,7 @@ pub fn nav_menu<'a>(
settings_btn,
]
.width(Length::Fill)
.align_items(Alignment::Center)
.align_y(Alignment::Center)
.spacing(10),
};

View file

@ -1,20 +1,21 @@
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, Command, Element, Length, Renderer, alignment};
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,34 +28,43 @@ 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,
}
}
pub fn update(&mut self, _message: &Message) -> Command<Message> {
Command::none()
#[allow(
clippy::unused_self,
reason = "Consistent component API; may change later"
)]
pub fn update(&mut self, _message: &Message) -> Task<Message> {
Task::none()
}
pub fn view(&self, settings: &Settings, _phone: &Phone) -> Element<Message, Theme, Renderer> {
pub fn view(
&self,
settings: &Settings,
_phone: &Phone,
) -> Element<'_, Message, Theme, Renderer> {
//let trash_svg = format!("{}/resources/assets/trash.svg", env!("CARGO_MANIFEST_DIR"));
//let restore_svg = format!("{}/resources/assets/rotate.svg", env!("CARGO_MANIFEST_DIR"));
let button_style;
let button_style: fn(&Theme, iced::widget::button::Status) -> iced::widget::button::Style;
let action_text;
let action_btn;
let selection_checkbox;
@ -87,24 +97,26 @@ impl PackageRow {
|| self.state != PackageState::Enabled
|| settings.general.expert_mode
{
selection_checkbox = checkbox("", self.selected)
selection_checkbox = checkbox(self.selected)
.on_toggle(Message::ToggleSelection)
.size(20)
.style(style::CheckBox::PackageEnabled);
action_btn = button(
text(action_text)
.horizontal_alignment(alignment::Horizontal::Center)
.align_x(alignment::Horizontal::Center)
.width(100),
)
.on_press(Message::ActionPressed);
} else {
selection_checkbox = checkbox("", self.selected)
selection_checkbox = checkbox(self.selected)
.on_toggle(Message::ToggleSelection)
.size(20)
.style(style::CheckBox::PackageDisabled);
action_btn = button(
text(action_text)
.horizontal_alignment(alignment::Horizontal::Center)
.align_x(alignment::Horizontal::Center)
.width(100),
);
}
@ -116,7 +128,8 @@ impl PackageRow {
text(&self.name).width(Length::FillPortion(8)),
action_btn.style(button_style)
]
.align_items(Alignment::Center)
.spacing(8)
.align_y(Alignment::Center)
)
.padding(8)
.style(if self.current {
@ -126,9 +139,62 @@ impl PackageRow {
})
.width(Length::Fill)
.on_press(Message::PackagePressed),
Space::with_width(15)
Space::new().width(Length::Fixed(15.0))
]
.align_items(Alignment::Center)
.align_y(Alignment::Center)
.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

@ -0,0 +1,13 @@
#![allow(
clippy::disallowed_types,
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, Theme> {
widget::Text::new(text).shaping(iced::widget::text::Shaping::Advanced)
}

50
flake.lock generated
View file

@ -1,55 +1,25 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 0,
"narHash": "sha256-YN/Ciidm+A0fmJPWlHBGvVkcarYWSC+s3NTPk/P+q3c=",
"path": "/nix/store/amxd2p02wx78nyaa4bkb0hjvgwhz1dq7-source",
"type": "path"
"lastModified": 1776949667,
"narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View file

@ -1,37 +1,69 @@
{
description = "devshell for uad-ng";
inputs.flake-utils.url = "github:numtide/flake-utils";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem
(system:
let pkgs = nixpkgs.legacyPackages.${system}; in
{
devShells.default = pkgs.mkShell {
packages = with pkgs;
outputs =
{ nixpkgs, ... }:
let
forSystems =
f:
nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed (
system: f (import nixpkgs { inherit system; })
);
in
{
formatter = forSystems (pkgs: pkgs.nixfmt-tree);
devShells = forSystems (
pkgs:
let
inherit (pkgs) lib stdenv;
runtimeLibs = [
pkgs.fontconfig
pkgs.freetype
]
++ lib.optionals stdenv.isLinux (
with pkgs;
[
rustc
cargo
clang
pkg-config
mold
android-tools
];
libglvnd
libX11
libXcursor
libXi
libXrandr
libxkbcommon
wayland
]
);
in
{
default = pkgs.mkShell (
{
nativeBuildInputs = with pkgs; [
clang
pkg-config
];
buildInputs =
with pkgs;
[
rustc
cargo
rustfmt
rust-analyzer
clippy
android-tools
]
++ runtimeLibs;
LD_LIBRARY_PATH = "${nixpkgs.lib.makeLibraryPath [
pkgs.fontconfig
pkgs.freetype
pkgs.libglvnd
pkgs.xorg.libX11
pkgs.xorg.libXcursor
pkgs.xorg.libXi
pkgs.xorg.libXrandr
pkgs.libxkbcommon
pkgs.wayland
]}";
LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
};
RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}/lib/rustlib/src/rust";
LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
}
// lib.optionalAttrs stdenv.isLinux { LD_LIBRARY_PATH = lib.makeLibraryPath runtimeLibs; }
// lib.optionalAttrs stdenv.isDarwin {
DYLD_FALLBACK_LIBRARY_PATH = lib.makeLibraryPath runtimeLibs;
}
);
}
);
};
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="Discord-Logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 126.644 96"><defs><style>.cls-1{fill:#5865f2;}</style></defs><path id="Discord-Symbol-Blurple" class="cls-1" d="M81.15,0c-1.2376,2.1973-2.3489,4.4704-3.3591,6.794-9.5975-1.4396-19.3718-1.4396-28.9945,0-.985-2.3236-2.1216-4.5967-3.3591-6.794-9.0166,1.5407-17.8059,4.2431-26.1405,8.0568C2.779,32.5304-1.6914,56.3725.5312,79.8863c9.6732,7.1476,20.5083,12.603,32.0505,16.0884,2.6014-3.4854,4.8998-7.1981,6.8698-11.0623-3.738-1.3891-7.3497-3.1318-10.8098-5.1523.9092-.6567,1.7932-1.3386,2.6519-1.9953,20.281,9.547,43.7696,9.547,64.0758,0,.8587.7072,1.7427,1.3891,2.6519,1.9953-3.4601,2.0457-7.0718,3.7632-10.835,5.1776,1.97,3.8642,4.2683,7.5769,6.8698,11.0623,11.5419-3.4854,22.3769-8.9156,32.0509-16.0631,2.626-27.2771-4.496-50.9172-18.817-71.8548C98.9811,4.2684,90.1918,1.5659,81.1752.0505l-.0252-.0505ZM42.2802,65.4144c-6.2383,0-11.4159-5.6575-11.4159-12.6535s4.9755-12.6788,11.3907-12.6788,11.5169,5.708,11.4159,12.6788c-.101,6.9708-5.026,12.6535-11.3907,12.6535ZM84.3576,65.4144c-6.2637,0-11.3907-5.6575-11.3907-12.6535s4.9755-12.6788,11.3907-12.6788,11.4917,5.708,11.3906,12.6788c-.101,6.9708-5.026,12.6535-11.3906,12.6535Z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View file

@ -1,14 +0,0 @@
use crate::core::theme::Theme;
use crate::gui::style;
use iced::widget::button;
use iced::widget::button::Button;
use iced::{Element, Renderer};
/// Wrapper function for `iced::widget::button` with padding and style applied
pub fn button_primary<'a, Message>(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Button<'a, Message, Theme, Renderer> {
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;

View file

@ -1,369 +0,0 @@
use crate::core::{
adb::{ACommand as AdbCommand, PM_CLEAR_PACK, to_trimmed_utf8},
uad_lists::PackageState,
};
use crate::gui::{views::list::PackageInfo, widgets::package_row::PackageRow};
use regex::Regex;
use retry::{OperationResult, delay::Fixed, retry};
use serde::{Deserialize, Serialize};
use std::process::Command;
use std::sync::LazyLock;
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
/// An Android device, typically a phone
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Phone {
/// Non-market name
pub model: String, // could be `Copy`
/// Android API level version
pub android_sdk: u8,
/// In theory, `len < u16::MAX` _should_ always be `true`.
/// In practice, `len <= u8::MAX`.
pub user_list: Vec<User>,
/// Unique serial identifier
pub adb_id: String, // could be `Copy`
}
impl Default for Phone {
fn default() -> Self {
Self {
model: "fetching devices...".to_string(),
android_sdk: 0,
user_list: vec![],
adb_id: String::default(),
}
}
}
impl std::fmt::Display for Phone {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.model)
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub struct User {
pub id: u16,
pub index: usize,
pub protected: bool,
}
impl std::fmt::Display for User {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "user {}", self.id)
}
}
#[derive(Debug, Clone)]
pub enum CommandType {
PackageManager(PackageInfo),
}
/// An enum to contain different variants for errors yielded by ADB.
#[derive(Debug, Clone)]
pub enum AdbError {
Generic(String),
}
/// Runs an **arbitrary command** on the device's default `sh` implementation.
/// Typically MKSH, but could be Ash.
/// [More info](https://chromium.googlesource.com/aosp/platform/system/core/+/refs/heads/upstream/shell_and_utilities).
///
/// If `serial` is empty, it lets ADB choose the default device.
#[deprecated = "Use [`adb::ACommand::shell`] with `async` blocks instead"]
pub async fn adb_shell_command<S: AsRef<str>>(
device_serial: S,
action: String,
command_type: CommandType,
) -> Result<CommandType, AdbError> {
let serial = device_serial.as_ref();
let label = match &command_type {
CommandType::PackageManager(p) => &p.removal,
};
let mut cmd = Command::new("adb");
if !serial.is_empty() {
cmd.args(["-s", serial]);
};
cmd.arg("shell");
// this works because `sh` splits spaces
cmd.arg(&action);
#[cfg(target_os = "windows")]
let cmd = cmd.creation_flags(0x0800_0000); // do not open a cmd window
match match cmd.output() {
Err(e) => {
error!("ADB: {}", e);
Err("Cannot run ADB, likely not found".to_string())
}
Ok(o) => {
let stdout = to_trimmed_utf8(o.stdout);
if o.status.success() {
Ok(stdout)
} else {
let stderr = to_trimmed_utf8(o.stderr);
// ADB does really weird things. Some errors are not redirected to stderr
let err = if stdout.is_empty() { stderr } else { stdout };
Err(err)
}
}
} {
Ok(o) => {
// On old devices, adb commands can return the `0` exit code even if there
// is an error. On Android 4.4, ADB doesn't check if the package exists.
// It does not return any error if you try to `pm block` a non-existent package.
// Some commands are even killed by ADB before finishing and UAD-ng can't catch
// the output.
if ["Error", "Failure"].iter().any(|&e| o.contains(e)) {
return Err(AdbError::Generic(format!("[{label}] {action} -> {o}")));
}
info!("[{label}] {action} -> {o}");
Ok(command_type)
}
Err(err) => {
if !err.contains("[not installed for") {
return Err(AdbError::Generic(format!("[{label}] {action} -> {err}")));
}
Err(AdbError::Generic(err))
}
}
}
/// If `None`, returns an empty String, not " --user 0"
pub fn user_flag(user_id: Option<User>) -> String {
user_id
.map(|user| format!(" --user {}", user.id))
.unwrap_or_default()
}
// Minimum information for processing adb commands
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub struct CorePackage {
pub name: String,
pub state: PackageState,
}
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,
}
}
}
pub fn apply_pkg_state_commands(
package: &CorePackage,
wanted_state: PackageState,
selected_user: User,
phone: &Phone,
) -> Vec<String> {
// https://github.com/Universal-Debloater-Alliance/universal-android-debloater/wiki/ADB-reference
// ALWAYS PUT THE COMMAND THAT CHANGES THE PACKAGE STATE FIRST!
let commands = match wanted_state {
PackageState::Enabled => match package.state {
PackageState::Disabled => vec!["pm enable"],
PackageState::Uninstalled => match phone.android_sdk {
i if i >= 23 => vec!["cmd package install-existing"],
21 | 22 => vec!["pm unhide"],
19 | 20 => vec!["pm unblock", PM_CLEAR_PACK],
_ => unreachable!("already prevented by the GUI"),
},
_ => 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![],
},
_ => 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)
_ => vec!["pm block", PM_CLEAR_PACK], // Disable mode is unavailable on older devices because the specific ADB commands need root
},
_ => vec![],
},
PackageState::All => vec![],
}; // this should be a `tinyvec`, as `len <= 4`
let user = supports_multi_user(phone).then_some(selected_user);
request_builder(&commands, &package.name, user)
}
/// 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`.
pub fn request_builder(commands: &[&str], package: &str, user: Option<User>) -> Vec<String> {
let maybe_user_flag = user_flag(user);
commands
.iter()
.map(|c| format!("{c}{maybe_user_flag} {package}"))
.collect()
}
/// Get the model by querying the `ro.product.model` property.
///
/// If `serial` is empty, it lets ADB choose the default device.
pub fn get_device_model(serial: &str) -> String {
AdbCommand::new()
.shell(serial)
.getprop("ro.product.model")
.unwrap_or_else(|err| {
eprintln!("ERROR: {err}");
error!("{err}");
if err.contains("adb: no devices/emulators found") {
"no devices/emulators found".to_string()
} else {
err
}
})
}
/// Get the brand by querying the `ro.product.brand` property.
///
/// If `serial` is empty, it lets ADB choose the default device.
pub fn get_device_brand(serial: &str) -> String {
AdbCommand::new()
.shell(serial)
.getprop("ro.product.brand")
// `trim` is just-in-case
.map(|s| s.trim().to_string())
.unwrap_or_default()
}
/// Get Android SDK version by querying the
// `ro.build.version.sdk` property or defaulting to 0.
///
/// If `device_serial` is empty, it lets ADB choose the default device.
pub fn get_android_sdk(device_serial: &str) -> u8 {
AdbCommand::new()
.shell(device_serial)
.getprop("ro.build.version.sdk")
.map_or(0, |sdk| {
sdk.parse().expect("SDK version numeral must be valid")
})
}
/// Minimum inclusive Android SDK version
/// that supports multi-user mode.
/// Lollipop 5.0
pub const MULTI_USER_SDK: u8 = 21;
/// Check if it might support multi-user mode,
/// by simply comparing SDK version.
/// `true` isn't reliable, you can only trust `false`.
///
/// See:
/// - <https://source.android.com/docs/devices/admin/multi-user#applying_the_overlay>
/// - <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
}
/// Check if a `user_id` is protected on a device by trying
/// to list associated packages.
///
/// If `device_serial` is empty, it lets ADB choose the default device.
pub fn is_protected_user<S: AsRef<str>>(user_id: u16, device_serial: S) -> bool {
AdbCommand::new()
.shell(device_serial)
.pm()
.list_packages_sys(None, Some(user_id))
.is_err()
}
/// `pm list users` parsed into a vector with extra info
pub fn list_users_parsed(device_serial: &str) -> Vec<User> {
// this could be thread-local (no lock overhead),
// but then each thread would compile its own clone of the regex,
// I guess?
static RE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\{([0-9]+)").unwrap_or_else(|_| unreachable!()));
AdbCommand::new()
.shell(device_serial)
.pm()
.list_users()
.map(|out| {
RE.find_iter(&out)
.enumerate()
.map(|(i, user)| {
let u = user.as_str()[1..]
.parse()
.unwrap_or_else(|_| unreachable!("User ID must always be a valid `u16`"));
User {
id: u,
index: i,
protected: is_protected_user(u, device_serial),
}
})
.collect()
})
.unwrap_or_default()
}
/// This matches serials (`getprop ro.serialno`)
/// that are authorized by the user.
pub async fn get_devices_list() -> Vec<Phone> {
retry(
Fixed::from_millis(500).take(if cfg!(debug_assertions) { 3 } else { 120 }),
|| match AdbCommand::new().devices() {
Ok(devices) => {
let mut device_list: Vec<Phone> = vec![];
if devices.iter().all(|(_, stat)| stat != "device") {
return OperationResult::Retry(vec![]);
}
for device in devices {
let serial = &device.0;
device_list.push(Phone {
model: format!("{} {}", get_device_brand(serial), get_device_model(serial)),
android_sdk: get_android_sdk(serial),
user_list: list_users_parsed(serial),
adb_id: serial.to_string(),
});
}
OperationResult::Ok(device_list)
}
Err(err) => {
error!("get_device_list() -> {}", err);
let test: Vec<Phone> = vec![];
OperationResult::Retry(test)
}
},
)
.unwrap_or_default()
}
pub async fn initial_load() -> bool {
match AdbCommand::new().devices() {
Ok(_devices) => true,
Err(_err) => false,
}
}

View file

@ -1,608 +0,0 @@
use crate::core::theme::Theme;
use iced::overlay::menu;
use iced::widget::{
button, checkbox, container, pick_list, radio, rule, scrollable, text, text_input,
};
use iced::{Background, Border, Color, Shadow, application};
#[derive(Default, Debug, Clone, Copy)]
pub enum Application {
#[default]
Default,
}
impl application::StyleSheet for Theme {
type Style = Application;
fn appearance(&self, _style: &Self::Style) -> application::Appearance {
let p = self.palette();
application::Appearance {
background_color: p.base.background,
text_color: p.bright.surface,
}
}
}
#[derive(Default, Debug, Clone, Copy)]
pub enum Container {
#[default]
Invisible,
Frame,
BorderedFrame,
Tooltip,
Background,
}
impl container::StyleSheet for Theme {
type Style = Container;
fn appearance(&self, style: &Self::Style) -> container::Appearance {
let pal = self.palette();
match style {
Container::Invisible => container::Appearance::default(),
Container::Frame => container::Appearance {
background: Some(Background::Color(pal.base.foreground)),
text_color: Some(pal.bright.surface),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 5.0.into(),
},
..container::Appearance::default()
},
Container::BorderedFrame => container::Appearance {
background: Some(Background::Color(pal.base.foreground)),
text_color: Some(pal.bright.surface),
border: Border {
color: pal.normal.error,
width: 1.0,
radius: 5.0.into(),
},
shadow: Shadow::default(),
},
Container::Tooltip => container::Appearance {
background: Some(Background::Color(pal.base.foreground)),
text_color: Some(pal.bright.surface),
border: Border {
color: pal.normal.primary,
width: 1.0,
radius: 8.0.into(),
},
shadow: Shadow::default(),
},
Container::Background => container::Appearance {
background: Some(Background::Color(pal.base.background)),
text_color: Some(pal.bright.surface),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 5.0.into(),
},
..container::Appearance::default()
},
}
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub enum Button {
#[default]
Primary,
Unavailable,
SelfUpdate,
UninstallPackage,
RestorePackage,
NormalPackage,
SelectedPackage,
Hidden,
}
impl button::StyleSheet for Theme {
type Style = Button;
fn active(&self, style: &Self::Style) -> button::Appearance {
let p = self.palette();
let appearance = button::Appearance {
border: Border {
color: p.normal.primary,
width: 1.0,
radius: 2.0.into(),
},
..button::Appearance::default()
};
let active_appearance = |bg: Option<Color>, mc| button::Appearance {
background: Some(Background::Color(bg.unwrap_or(p.base.foreground))),
border: Border {
color: Color { a: 0.5, ..mc },
width: 1.0,
radius: 2.0.into(),
},
text_color: mc,
..appearance
};
match style {
Button::Primary | Button::SelfUpdate => active_appearance(None, p.bright.primary),
Button::RestorePackage => active_appearance(None, p.bright.secondary),
Button::NormalPackage => button::Appearance {
background: Some(Background::Color(p.base.foreground)),
text_color: p.bright.surface,
border: Border {
color: p.base.background,
width: 0.0,
radius: 5.0.into(),
},
..appearance
},
Button::SelectedPackage => button::Appearance {
background: Some(Background::Color(Color {
a: 0.25,
..p.normal.primary
})),
text_color: p.bright.primary,
border: Border {
color: p.normal.primary,
width: 0.0,
radius: 5.0.into(),
},
..appearance
},
Button::Unavailable | Button::UninstallPackage => {
active_appearance(None, p.bright.error)
}
Button::Hidden => button::Appearance {
background: Some(Background::Color(Color::TRANSPARENT)),
text_color: Color::TRANSPARENT,
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 5.0.into(),
},
..appearance
},
}
}
fn hovered(&self, style: &Self::Style) -> button::Appearance {
let active = self.active(style);
let p = self.palette();
let hover_appearance = |bg, tc: Option<Color>| button::Appearance {
background: Some(Background::Color(Color { a: 0.25, ..bg })),
text_color: tc.unwrap_or(bg),
..active
};
match style {
Button::Primary | Button::SelfUpdate => hover_appearance(p.bright.primary, None),
Button::NormalPackage => hover_appearance(p.normal.primary, Some(p.bright.surface)),
Button::SelectedPackage => hover_appearance(p.normal.primary, None),
Button::RestorePackage => hover_appearance(p.bright.secondary, None),
Button::Unavailable | Button::UninstallPackage => {
hover_appearance(p.bright.error, None)
}
Button::Hidden => hover_appearance(Color::TRANSPARENT, None),
}
}
fn pressed(&self, style: &Self::Style) -> button::Appearance {
self.active(style)
}
fn disabled(&self, style: &Self::Style) -> button::Appearance {
let active = self.active(style);
let p = self.palette();
let disabled_appearance = |bg, tc: Option<Color>| button::Appearance {
background: Some(Background::Color(Color { a: 0.05, ..bg })),
text_color: Color {
a: 0.50,
..tc.unwrap_or(bg)
},
..active
};
match style {
Button::RestorePackage => disabled_appearance(p.normal.primary, Some(p.bright.primary)),
Button::UninstallPackage => disabled_appearance(p.bright.error, None),
Button::Primary => disabled_appearance(p.bright.primary, Some(p.bright.primary)),
_ => active,
}
}
}
#[derive(Default, Debug, Clone, Copy)]
pub enum Scrollable {
#[default]
Description,
Packages,
}
impl scrollable::StyleSheet for Theme {
type Style = Scrollable;
fn active(&self, style: &Self::Style) -> scrollable::Appearance {
let from_appearance = |c: Color| scrollable::Appearance {
container: container::Appearance::default(),
gap: Some(Background::Color(Color::TRANSPARENT)),
scrollbar: scrollable::Scrollbar {
background: Some(Background::Color(Color::TRANSPARENT)),
border: Border {
color: Color::TRANSPARENT,
width: 0.0,
radius: 5.0.into(),
},
scroller: scrollable::Scroller {
color: c,
border: Border {
color: Color::TRANSPARENT,
width: 1.0,
radius: 5.0.into(),
},
},
},
};
let p = self.palette();
match style {
Scrollable::Description => from_appearance(p.normal.surface),
Scrollable::Packages => from_appearance(p.base.foreground),
}
}
fn hovered(&self, style: &Self::Style, _mouse_over_scrollbar: bool) -> scrollable::Appearance {
scrollable::Appearance {
scrollbar: self.active(style).scrollbar,
..self.active(style)
}
}
fn dragging(&self, style: &Self::Style) -> scrollable::Appearance {
let hovered = self.hovered(style, true);
scrollable::Appearance {
scrollbar: hovered.scrollbar,
..hovered
}
}
}
#[derive(Default, Debug, Clone, Copy)]
pub enum CheckBox {
#[default]
PackageEnabled,
PackageDisabled,
SettingsEnabled,
SettingsDisabled,
}
impl checkbox::StyleSheet for Theme {
type Style = CheckBox;
fn active(&self, style: &Self::Style, _is_checked: bool) -> checkbox::Appearance {
let pal = self.palette();
match style {
CheckBox::PackageEnabled => checkbox::Appearance {
background: Background::Color(pal.base.background),
icon_color: pal.bright.primary,
border: Border {
color: pal.base.background,
width: 1.0,
radius: 5.0.into(),
},
text_color: Some(pal.bright.surface),
},
CheckBox::PackageDisabled => checkbox::Appearance {
background: Background::Color(Color {
a: 0.55,
..pal.base.background
}),
icon_color: pal.bright.primary,
border: Border {
color: pal.normal.primary,
width: 1.0,
radius: 5.0.into(),
},
text_color: Some(pal.normal.primary),
},
CheckBox::SettingsEnabled => checkbox::Appearance {
background: Background::Color(pal.base.background),
icon_color: pal.bright.primary,
border: Border {
color: pal.bright.primary,
width: 1.0,
radius: 5.0.into(),
},
text_color: Some(pal.bright.surface),
},
CheckBox::SettingsDisabled => checkbox::Appearance {
background: Background::Color(pal.base.foreground),
icon_color: pal.bright.primary,
border: Border {
color: pal.normal.primary,
width: 1.0,
radius: 5.0.into(),
},
text_color: Some(pal.bright.surface),
},
}
}
fn hovered(&self, style: &Self::Style, is_checked: bool) -> checkbox::Appearance {
let p = self.palette();
let from_appearance = || checkbox::Appearance {
background: Background::Color(p.base.foreground),
icon_color: p.bright.primary,
border: Border {
color: p.bright.primary,
width: 2.0,
radius: 5.0.into(),
},
text_color: Some(p.bright.surface),
};
match style {
CheckBox::PackageEnabled | CheckBox::SettingsEnabled => from_appearance(),
CheckBox::PackageDisabled | CheckBox::SettingsDisabled => {
self.active(style, is_checked)
}
}
}
}
#[derive(Default, Debug, Clone, Copy)]
pub enum TextInput {
#[default]
Default,
}
impl text_input::StyleSheet for Theme {
type Style = TextInput;
fn active(&self, _style: &Self::Style) -> text_input::Appearance {
let p = self.palette();
text_input::Appearance {
background: Background::Color(p.base.foreground),
border: Border {
color: p.base.foreground,
width: 0.0,
radius: 5.0.into(),
},
icon_color: Color {
a: 0.5,
..p.normal.primary
},
}
}
fn focused(&self, _style: &Self::Style) -> text_input::Appearance {
let p = self.palette();
text_input::Appearance {
background: Background::Color(p.base.foreground),
border: Border {
color: Color {
a: 0.5,
..p.normal.primary
},
width: 1.0,
radius: 2.0.into(),
},
icon_color: Color {
a: 0.5,
..p.normal.primary
},
}
}
fn disabled_color(
&self,
_: &<Self as iced::widget::text_input::StyleSheet>::Style,
) -> iced::Color {
todo!()
}
fn disabled(&self, _style: &Self::Style) -> text_input::Appearance {
let p = self.palette();
text_input::Appearance {
background: Background::Color(p.base.background),
border: Border {
color: Color {
a: 0.5,
..p.base.foreground
},
width: 1.0,
radius: 2.0.into(),
},
icon_color: Color {
a: 0.5,
..p.base.foreground
},
}
}
fn placeholder_color(&self, _style: &Self::Style) -> Color {
self.palette().normal.surface
}
fn value_color(&self, _style: &Self::Style) -> Color {
self.palette().bright.primary
}
fn selection_color(&self, _style: &Self::Style) -> Color {
self.palette().normal.primary
}
/// Produces the style of an hovered text input.
fn hovered(&self, style: &Self::Style) -> text_input::Appearance {
self.focused(style)
}
}
#[derive(Default, Debug, Clone, Copy)]
pub enum PickList {
#[default]
Default,
}
impl menu::StyleSheet for Theme {
type Style = ();
fn appearance(&self, _style: &Self::Style) -> menu::Appearance {
let p = self.palette();
menu::Appearance {
text_color: p.bright.surface,
background: p.base.background.into(),
border: Border {
color: p.base.background,
width: 1.0,
radius: 2.0.into(),
},
selected_text_color: p.bright.surface,
selected_background: p.normal.primary.into(),
}
}
}
impl pick_list::StyleSheet for Theme {
type Style = ();
fn active(&self, _style: &()) -> pick_list::Appearance {
let p = self.palette();
pick_list::Appearance {
text_color: p.bright.surface,
background: p.base.background.into(),
border: Border {
color: Color {
a: 0.5,
..p.normal.primary
},
width: 1.0,
radius: 2.0.into(),
},
handle_color: p.bright.surface,
placeholder_color: p.bright.surface,
}
}
fn hovered(&self, style: &()) -> pick_list::Appearance {
let active = self.active(style);
pick_list::Appearance {
border: Border {
color: self.palette().normal.primary,
width: 1.0,
radius: 2.0.into(),
},
..active
}
}
}
#[derive(Default, Clone, Copy)]
pub enum Text {
#[default]
Default,
Ok,
Danger,
Commentary,
Color(Color),
}
impl From<Color> for Text {
fn from(color: Color) -> Self {
Self::Color(color)
}
}
impl text::StyleSheet for Theme {
type Style = Text;
fn appearance(&self, style: Self::Style) -> text::Appearance {
let p = self.palette();
match style {
Text::Default => text::Appearance::default(),
Text::Ok => text::Appearance {
color: Some(p.bright.secondary),
},
Text::Danger => text::Appearance {
color: Some(p.bright.error),
},
Text::Commentary => text::Appearance {
color: Some(p.normal.surface),
},
Text::Color(c) => text::Appearance { color: Some(c) },
}
}
}
impl radio::StyleSheet for Theme {
type Style = ();
fn active(&self, _style: &Self::Style, _is_selected: bool) -> radio::Appearance {
let p = self.palette();
radio::Appearance {
background: Color::TRANSPARENT.into(),
dot_color: p.bright.primary,
border_width: 1.0,
border_color: p.bright.primary,
text_color: None,
}
}
fn hovered(&self, style: &Self::Style, _is_selected: bool) -> radio::Appearance {
let p = self.palette();
let active = self.active(style, true);
radio::Appearance {
dot_color: p.bright.primary,
border_color: p.bright.primary,
border_width: 2.0,
..active
}
}
}
#[derive(Default, Clone, Copy)]
pub enum Rule {
#[default]
Default,
}
impl rule::StyleSheet for Theme {
type Style = Rule;
fn appearance(&self, style: &Self::Style) -> rule::Appearance {
match style {
Rule::Default => rule::Appearance {
color: self.palette().bright.surface,
width: 2,
radius: 2.0.into(),
fill_mode: rule::FillMode::Full,
},
}
}
}
// Unit tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_palette() {
let palette = Theme::default().palette();
assert_ne!(palette.base.background, palette.base.foreground);
assert_ne!(palette.normal.primary, Color::BLACK);
assert_ne!(palette.normal.surface, Color::BLACK);
assert_ne!(palette.bright.primary, Color::BLACK);
// if `LIGHT` then this can be `BLACK`
// https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/pull/730#issuecomment-2525405134
//assert_ne!(palette.bright.surface, Color::BLACK);
assert_ne!(palette.normal.error, Color::BLACK);
assert_ne!(palette.bright.error, Color::BLACK);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,577 +0,0 @@
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, CommandType, Phone, User, adb_shell_command, get_android_sdk, supports_multi_user,
},
theme::Theme,
utils::{
DisplayablePath, Error, NAME, export_packages, generate_backup_name, open_folder, open_url,
string_to_theme,
},
};
use crate::gui::{
style,
views::list::{List as AppsView, PackageInfo},
widgets::modal::Modal,
widgets::navigation_menu::ICONS,
widgets::package_row::PackageRow,
widgets::text,
};
use iced::widget::{Space, button, checkbox, column, container, pick_list, radio, row, scrollable};
use iced::{Alignment, Element, Length, Renderer, alignment};
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub enum PopUpModal {
ExportUninstalled,
}
#[derive(Debug, Clone)]
pub struct Settings {
pub general: GeneralSettings,
pub device: DeviceSettings,
is_loading: bool,
modal: Option<PopUpModal>,
}
impl Default for Settings {
fn default() -> Self {
Self {
general: Config::load_configuration_file().general,
device: DeviceSettings::default(),
is_loading: false,
modal: None,
}
}
}
#[derive(Debug, Clone)]
pub enum Message {
LoadDeviceSettings,
ExpertMode(bool),
DisableMode(bool),
MultiUserMode(bool),
ApplyTheme(Theme),
UrlPressed(PathBuf),
BackupSelected(DisplayablePath),
BackupDevice,
RestoreDevice,
RestoringDevice(Result<CommandType, AdbError>),
DeviceBackedUp(Result<bool, String>),
ChooseBackUpFolder,
FolderChosen(Result<PathBuf, Error>),
ExportPackages,
PackagesExported(Result<bool, String>),
ModalHide,
}
impl Settings {
#[allow(clippy::too_many_lines)]
pub fn update(
&mut self,
phone: &Phone,
packages: &[Vec<PackageRow>],
nb_running_async_adb_commands: &mut u32,
msg: Message,
selected_user: Option<User>,
) -> iced::Command<Message> {
match msg {
Message::ModalHide => {
self.modal = None;
iced::Command::none()
}
Message::ExpertMode(toggled) => {
self.general.expert_mode = toggled;
debug!("Config change: {:?}", self);
Config::save_changes(self, &phone.adb_id);
iced::Command::none()
}
Message::DisableMode(toggled) => {
if phone.android_sdk >= 23 {
self.device.disable_mode = toggled;
debug!("Config change: {:?}", self);
Config::save_changes(self, &phone.adb_id);
}
iced::Command::none()
}
Message::MultiUserMode(toggled) => {
self.device.multi_user_mode = toggled;
debug!("Config change: {:?}", self);
Config::save_changes(self, &phone.adb_id);
iced::Command::none()
}
Message::ApplyTheme(theme) => {
self.general.theme = theme.to_string();
debug!("Config change: {:?}", self);
Config::save_changes(self, &phone.adb_id);
iced::Command::none()
}
Message::UrlPressed(url) => {
open_url(url);
iced::Command::none()
}
Message::LoadDeviceSettings => {
let backups =
list_available_backups(&self.general.backup_folder.join(&phone.adb_id));
let backup = BackupSettings {
backups: backups.clone(),
selected: backups.first().cloned(),
users: phone.user_list.clone(),
selected_user: phone.user_list.first().copied(),
backup_state: String::default(),
};
match Config::load_configuration_file()
.devices
.iter()
.find(|d| d.device_id == phone.adb_id)
{
Some(device) => {
self.device.clone_from(device);
self.device.backup = backup;
}
None => {
self.device = DeviceSettings {
device_id: phone.adb_id.clone(),
multi_user_mode: supports_multi_user(phone),
disable_mode: false,
backup,
}
}
};
iced::Command::none()
}
Message::BackupSelected(d_path) => {
self.device.backup.selected = Some(d_path.clone());
self.device.backup.users = list_available_backup_user(d_path);
iced::Command::none()
}
Message::BackupDevice => iced::Command::perform(
backup_phone(
phone.user_list.clone(),
self.device.device_id.clone(),
packages.to_vec(),
),
Message::DeviceBackedUp,
),
Message::DeviceBackedUp(is_backed_up) => {
match is_backed_up {
Ok(_) => {
info!("[BACKUP] Backup successfully created");
self.device.backup.backups = list_available_backups(
&self.general.backup_folder.join(phone.adb_id.clone()),
);
self.device.backup.selected = self.device.backup.backups.first().cloned();
}
Err(err) => {
error!("[BACKUP FAILED] Backup creation failed: {:?}", err);
}
}
iced::Command::none()
}
Message::RestoreDevice => match restore_backup(phone, packages, &self.device) {
Ok(r_packages) => {
let mut commands = vec![];
*nb_running_async_adb_commands = 0;
for p in &r_packages {
let p_info = PackageInfo {
i_user: 0,
index: p.index,
removal: "RESTORE".to_string(),
};
for command in p.commands.clone() {
*nb_running_async_adb_commands += 1;
commands.push(iced::Command::perform(
// WARNING: THIS IS INSECURE!
// If a user "A" restores a backup from a malicious user "B",
// then B could run arbitrary high-privilege cmds,
// on A's Android device!
adb_shell_command(
phone.adb_id.clone(),
command,
CommandType::PackageManager(p_info.clone()),
),
Message::RestoringDevice,
));
}
}
if r_packages.is_empty() {
if get_android_sdk(&phone.adb_id) == 0 {
self.device.backup.backup_state = "Device is not connected".to_string();
} else {
self.device.backup.backup_state =
"Device state is already restored".to_string();
}
}
info!(
"[RESTORE] Restoring backup {}",
self.device.backup.selected.as_ref().unwrap()
);
iced::Command::batch(commands)
}
Err(e) => {
self.device.backup.backup_state.clone_from(&e);
error!("{} - {}", self.device.backup.selected.as_ref().unwrap(), e);
iced::Command::none()
}
},
// Trigger an action in mod.rs (Message::SettingsAction(msg))
Message::RestoringDevice(_) => iced::Command::none(),
Message::FolderChosen(result) => {
self.is_loading = false;
if let Ok(path) = result {
self.general.backup_folder = path;
Config::save_changes(self, &phone.adb_id);
#[expect(unused_must_use, reason = "side-effect")]
{
self.update(
phone,
packages,
nb_running_async_adb_commands,
Message::LoadDeviceSettings,
selected_user,
);
}
}
iced::Command::none()
}
Message::ChooseBackUpFolder => {
if self.is_loading {
iced::Command::none()
} else {
self.is_loading = true;
iced::Command::perform(open_folder(), Message::FolderChosen)
}
}
Message::ExportPackages => iced::Command::perform(
export_packages(selected_user.unwrap_or_default(), packages.to_vec()),
Message::PackagesExported,
),
Message::PackagesExported(exported) => {
match exported {
Ok(_) => self.modal = Some(PopUpModal::ExportUninstalled),
Err(err) => error!("Failed to export list of uninstalled packages: {:?}", err),
}
iced::Command::none()
}
}
}
#[allow(clippy::too_many_lines)]
pub fn view(&self, phone: &Phone, apps_view: &AppsView) -> Element<Message, Theme, Renderer> {
let radio_btn_theme = Theme::ALL
.iter()
.fold(row![].spacing(10), |column, option| {
column.push(
radio(
format!("{}", option.clone()),
*option,
Some(string_to_theme(&self.general.theme)),
Message::ApplyTheme,
)
.size(24),
)
});
let theme_ctn = container(radio_btn_theme)
.padding(10)
.width(Length::Fill)
.height(Length::Shrink)
.style(style::Container::Frame);
let expert_mode_checkbox = checkbox(
"Allow to uninstall packages marked as \"unsafe\" (I KNOW WHAT I AM DOING)",
self.general.expert_mode,
)
.on_toggle(Message::ExpertMode)
.style(style::CheckBox::SettingsEnabled);
let expert_mode_descr =
text("Most unsafe packages are known to bootloop the device if removed.")
.style(style::Text::Commentary);
let choose_backup_descr = text("Note: If you have previous backups, you will need to transfer them manually to newly changed backup folder to be able to use Restore functionality")
.style(style::Text::Commentary);
let choose_backup_btn = button(text("\u{E930}").font(ICONS))
.padding([5, 10])
.on_press(Message::ChooseBackUpFolder)
.style(style::Button::Primary);
let choose_backup_row = row![
choose_backup_btn,
"Choose backup folder",
Space::new(Length::Fill, Length::Shrink),
"Current folder: ",
text(self.general.backup_folder.to_string_lossy())
]
.spacing(10)
.align_items(Alignment::Center);
let general_ctn = container(
column![
expert_mode_checkbox,
expert_mode_descr,
choose_backup_row,
choose_backup_descr,
]
.spacing(10),
)
.padding(10)
.width(Length::Fill)
.height(Length::Shrink)
.style(style::Container::Frame);
let warning_ctn = container(
row![
text("The following settings only affect the currently selected device:")
.style(style::Text::Danger),
text(phone.model.clone()),
Space::new(Length::Fill, Length::Shrink),
text(phone.adb_id.clone()).style(style::Text::Commentary)
]
.spacing(7),
)
.padding(10)
.width(Length::Fill)
.style(style::Container::BorderedFrame);
let multi_user_mode_descr = row![
text("This will not affect the following protected work profile users: ")
.style(style::Text::Commentary),
text(
phone
.user_list
.iter()
.filter(|&u| u.protected)
.map(|u| u.id.to_string())
.collect::<Vec<String>>()
.join(", ")
)
.style(style::Text::Danger)
];
let multi_user_mode_checkbox = checkbox(
"Affect all the users of the device (not only the selected user)",
self.device.multi_user_mode,
)
.on_toggle(Message::MultiUserMode)
.style(style::CheckBox::SettingsEnabled);
let disable_checkbox_style = if phone.android_sdk >= 23 {
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")
.style(style::Text::Commentary);
let unavailable_btn = button(text("Unavailable").size(14))
.on_press(Message::UrlPressed(PathBuf::from(
"https://github.com/Universal-Debloater-Alliance/universal-android-debloater/wiki/FAQ#\
why-is-the-disable-mode-setting-not-available-for-my-device",
)))
.height(22)
.style(style::Button::Unavailable);
// Disabling package without root isn't really possible before Android Oreo (8.0)
// see https://github.com/Universal-Debloater-Alliance/universal-android-debloater/wiki/ADB-reference
let disable_mode_checkbox = checkbox(
"Clear and disable packages instead of uninstalling them",
self.device.disable_mode,
)
.on_toggle(Message::DisableMode)
.style(disable_checkbox_style);
let disable_setting_row = if phone.android_sdk >= 23 {
row![
disable_mode_checkbox,
Space::new(Length::Fill, Length::Shrink),
]
.width(Length::Fill)
} else {
row![
disable_mode_checkbox,
Space::new(Length::Fill, Length::Shrink),
unavailable_btn,
]
.width(Length::Fill)
};
let device_specific_ctn = container(
column![
multi_user_mode_checkbox,
multi_user_mode_descr,
disable_setting_row,
disable_mode_descr,
]
.spacing(10),
)
.padding(10)
.width(Length::Fill)
.height(Length::Shrink)
.style(style::Container::Frame);
let backup_pick_list = pick_list(
self.device.backup.backups.clone(),
self.device.backup.selected.clone(),
Message::BackupSelected,
)
.padding(6);
let backup_btn =
button_primary(text("Backup").horizontal_alignment(alignment::Horizontal::Center))
.on_press(Message::BackupDevice)
.width(77);
let restore_btn = |enabled| {
if enabled {
button(text("Restore").horizontal_alignment(alignment::Horizontal::Center))
.padding([5, 10])
.on_press(Message::RestoreDevice)
.width(77)
} else {
button(
text("No backup")
.horizontal_alignment(alignment::Horizontal::Center)
.vertical_alignment(alignment::Vertical::Center),
)
.padding([5, 10])
.width(77)
}
};
let locate_backup_btn = if self.device.backup.backups.is_empty() {
button_primary("Open backup directory")
} else {
button_primary("Open backup directory").on_press(Message::UrlPressed(
self.general.backup_folder.join(phone.adb_id.clone()),
))
};
let export_btn = button_primary("Export").on_press(Message::ExportPackages);
let backup_row = row![
backup_btn,
"Backup the current state of the phone",
Space::new(Length::Fill, Length::Shrink),
locate_backup_btn,
]
.spacing(10)
.align_items(Alignment::Center);
let restore_row = if self.device.backup.backups.is_empty() {
row![]
} else {
row![
restore_btn(true),
"Restore the state of the device",
Space::new(Length::Fill, Length::Shrink),
text(self.device.backup.backup_state.clone()).style(style::Text::Danger),
backup_pick_list,
]
.spacing(10)
.align_items(Alignment::Center)
};
let no_device_ctn = || {
container(text("No device detected").style(style::Text::Danger))
.padding(10)
.width(Length::Fill)
.style(style::Container::BorderedFrame)
};
let content = if phone.adb_id.is_empty() {
column![
text("Theme").size(26),
theme_ctn,
text("General").size(26),
general_ctn,
text("Current device").size(26),
no_device_ctn(),
text("Backup / Restore").size(26),
no_device_ctn(),
]
.width(Length::Fill)
.spacing(20)
} else {
let export_row = row![
export_btn,
"Export uninstalled packages with their description",
Space::new(Length::Fill, Length::Shrink),
text(format!(
"Selected: user {}",
apps_view.selected_user.unwrap_or_default().id
)),
]
.spacing(10)
.align_items(Alignment::Center);
let backup_restore_ctn =
container(column![backup_row, restore_row, export_row].spacing(10))
.padding(10)
.width(Length::Fill)
.height(Length::Shrink)
.style(style::Container::Frame);
column![
text("Theme").size(26),
theme_ctn,
text("General").size(26),
general_ctn,
text("Current device").size(26),
warning_ctn,
device_specific_ctn,
text("Backup / Restore").size(26),
backup_restore_ctn,
]
.width(Length::Fill)
.spacing(20)
};
if let Some(PopUpModal::ExportUninstalled) = self.modal {
let title = container(row![text("Success").size(24)].align_items(Alignment::Center))
.width(Length::Fill)
.style(style::Container::Frame)
.padding([10, 0, 10, 0])
.center_y()
.center_x();
let text_box = row![
text(format!("Exported uninstalled packages into file.\nFile is exported in same directory where {NAME} is located.")).width(Length::Fill),
].padding(20);
let file_row = row![
text(generate_backup_name(chrono::Local::now())).style(style::Text::Commentary)
]
.padding(20);
let modal_btn_row = row![
Space::new(Length::Fill, Length::Shrink),
button(text("Close").width(Length::Shrink))
.width(Length::Shrink)
.on_press(Message::ModalHide),
Space::new(Length::Fill, Length::Shrink),
];
let ctn = container(column![title, text_box, file_row, modal_btn_row])
.height(Length::Shrink)
.width(500)
.padding(10)
.style(style::Container::Frame);
return Modal::new(content.padding(10), ctn)
.on_blur(Message::ModalHide)
.into();
}
container(scrollable(content))
.padding(10)
.width(Length::Fill)
.height(Length::Fill)
.into()
}
}

View file

@ -1,17 +0,0 @@
#![allow(
clippy::disallowed_types,
reason = "this is the replacement that enforces advanced shaping for disallowed [`iced::widget::Text`]"
)]
use std::borrow::Cow;
use iced::widget::Text;
// Creates a new Text widget with advanced shaping.
pub fn text<'a, Theme, Renderer>(text: impl Into<Cow<'a, str>>) -> Text<'a, Theme, Renderer>
where
Theme: iced::widget::text::StyleSheet,
Renderer: iced::advanced::text::Renderer,
{
Text::new(text).shaping(iced::widget::text::Shaping::Advanced)
}