From 6c851d111217647a99cf13c841de03eaa151d49f Mon Sep 17 00:00:00 2001 From: iamanaws <78835633+Iamanaws@users.noreply.github.com> Date: Tue, 2 Dec 2025 22:28:29 -0800 Subject: [PATCH 01/16] feat(adb): add built-in Rust ADB backend --- Cargo.lock | 538 ++++++++++++++++++++++++--- Cargo.toml | 1 + crates/uad-cli/src/commands.rs | 76 ++-- crates/uad-cli/src/device.rs | 8 +- crates/uad-cli/src/main.rs | 50 ++- crates/uad-cli/src/output.rs | 12 +- crates/uad-cli/src/repl.rs | 43 ++- crates/uad-core/Cargo.toml | 1 + crates/uad-core/src/adb.rs | 502 +++++++++++++++---------- crates/uad-core/src/config.rs | 5 + crates/uad-core/src/sync.rs | 101 +++-- crates/uad-gui/src/gui.rs | 29 +- crates/uad-gui/src/views/about.rs | 34 +- crates/uad-gui/src/views/list.rs | 64 ++-- crates/uad-gui/src/views/settings.rs | 84 ++++- flake.lock | 6 +- 16 files changed, 1190 insertions(+), 364 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e40c813..5c53a1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,31 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" +[[package]] +name = "adb_client" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4974ac06396fce4424d9d2193f80ea5a177d6e2ddde6214ff7c14aaedd2a3104" +dependencies = [ + "base64", + "byteorder", + "chrono", + "image", + "log", + "num-bigint-dig", + "num-traits", + "num_enum", + "quick-protobuf", + "rand 0.10.1", + "rcgen", + "regex", + "rsa", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.18", +] + [[package]] name = "adler2" version = "2.0.1" @@ -37,6 +62,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "aligned" version = "0.4.3" @@ -227,6 +261,45 @@ dependencies = [ "zbus", ] +[[package]] +name = "asn1-rs" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -473,7 +546,7 @@ dependencies = [ "anyhow", "arrayvec", "log", - "nom", + "nom 8.0.0", "num-rational", "v_frame", ] @@ -493,13 +566,19 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] [[package]] @@ -508,6 +587,15 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bit_field" version = "0.10.3" @@ -541,6 +629,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.5.1" @@ -604,6 +701,12 @@ dependencies = [ "syn", ] +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "byteorder-lite" version = "0.1.0" @@ -704,10 +807,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] -name = "chrono" -version = "0.4.43" +name = "chacha20" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "num-traits", @@ -854,6 +968,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "cookie" version = "0.18.1" @@ -986,6 +1106,24 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1039,6 +1177,16 @@ dependencies = [ "wgpu", ] +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "csv" version = "1.4.0" @@ -1086,6 +1234,37 @@ dependencies = [ "winreg 0.52.0", ] +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.5.6" @@ -1095,6 +1274,17 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", +] + [[package]] name = "dirs" version = "6.0.0" @@ -1597,6 +1787,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "gethostname" version = "1.1.0" @@ -1639,6 +1839,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -2177,9 +2378,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.9" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", @@ -2195,8 +2396,8 @@ dependencies = [ "rayon", "rgb", "tiff", - "zune-core 0.5.1", - "zune-jpeg 0.5.12", + "zune-core", + "zune-jpeg", ] [[package]] @@ -2351,6 +2552,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "leb128fmt" @@ -2457,9 +2661,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" dependencies = [ "value-bag", ] @@ -2537,6 +2741,12 @@ dependencies = [ "paste", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2560,9 +2770,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.11" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" dependencies = [ "num-traits", "pxfm", @@ -2682,6 +2892,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nom" version = "8.0.0" @@ -2707,6 +2927,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "serde", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.2.0" @@ -2733,6 +2970,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -2766,9 +3014,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -2776,9 +3024,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -3145,6 +3393,15 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -3242,6 +3499,25 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3291,6 +3567,27 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -3446,6 +3743,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-protobuf" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f" +dependencies = [ + "byteorder", +] + [[package]] name = "quick-xml" version = "0.38.4" @@ -3501,6 +3807,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.1", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -3539,6 +3856,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "range-alloc" version = "0.1.4" @@ -3588,9 +3911,9 @@ dependencies = [ [[package]] name = "ravif" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" dependencies = [ "avif-serialize", "imgref", @@ -3627,6 +3950,20 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + [[package]] name = "read-fonts" version = "0.35.0" @@ -3676,6 +4013,35 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + [[package]] name = "renderdoc-sys" version = "1.1.0" @@ -3744,6 +4110,26 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -3765,6 +4151,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "rustix" version = "0.38.44" @@ -3793,9 +4188,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "log", "once_cell", @@ -3808,9 +4203,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "zeroize", ] @@ -3969,6 +4364,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3985,6 +4391,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.8" @@ -4144,6 +4560,12 @@ dependencies = [ "x11rb", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -4153,6 +4575,16 @@ dependencies = [ "bitflags 2.10.0", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -4306,16 +4738,16 @@ dependencies = [ [[package]] name = "tiff" -version = "0.10.3" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" dependencies = [ "fax", "flate2", "half", "quick-error", "weezl", - "zune-jpeg 0.4.21", + "zune-jpeg", ] [[package]] @@ -4532,6 +4964,12 @@ dependencies = [ "core_maths", ] +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + [[package]] name = "uad-cli" version = "1.2.0" @@ -4556,6 +4994,7 @@ dependencies = [ name = "uad-core" version = "1.2.0" dependencies = [ + "adb_client", "chrono", "csv", "dirs", @@ -5119,7 +5558,7 @@ checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7" dependencies = [ "arrayvec", "bit-set", - "bit-vec", + "bit-vec 0.8.0", "bitflags 2.10.0", "bytemuck", "cfg_aliases 0.2.1", @@ -5891,6 +6330,24 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + [[package]] name = "xattr" version = "1.6.1" @@ -5938,6 +6395,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec 0.9.1", + "time", +] + [[package]] name = "yazi" version = "0.2.1" @@ -6120,12 +6587,6 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - [[package]] name = "zune-core" version = "0.5.1" @@ -6141,22 +6602,13 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "zune-jpeg" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" -dependencies = [ - "zune-core 0.4.12", -] - [[package]] name = "zune-jpeg" version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" dependencies = [ - "zune-core 0.5.1", + "zune-core", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0464938..9104739 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ edition = "2024" [workspace.dependencies] # Core dependencies +adb_client = "3.2.2" serde = { version = "^1.0", features = ["derive"] } serde_json = "^1.0" log = "^0.4" diff --git a/crates/uad-cli/src/commands.rs b/crates/uad-cli/src/commands.rs index 62286bd..ff16469 100644 --- a/crates/uad-cli/src/commands.rs +++ b/crates/uad-cli/src/commands.rs @@ -2,10 +2,10 @@ 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::adb::{ACommand, AdbBackend, PmListPacksFlag}; use uad_core::sync::{ CorePackage, Phone, User, apply_pkg_state_commands, get_devices_list, get_package_state, - run_adb_shell_action, + run_adb_shell_action_with_backend, }; use uad_core::uad_lists::{Package, PackageState, Removal, UadList, load_debloat_lists}; use uad_core::utils::{matches_search, truncate_description}; @@ -15,9 +15,12 @@ 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> { - println_or_exit!("Scanning for connected devices..."); - let devices = get_devices_list(); +pub fn list_devices(backend: AdbBackend) -> Result<(), Box> { + println_or_exit!( + "Scanning for connected devices (using {} backend)...", + backend + ); + let devices = get_devices_list(backend); if devices.is_empty() { return Err(NO_DEVICES_FOUND.into()); @@ -58,28 +61,24 @@ impl PackageListContext { 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) { @@ -104,8 +103,18 @@ pub struct DisplayConfig { pub show_removal: bool, } +pub(crate) fn resolve_pm_flag(state_filter: Option) -> Option { + match state_filter { + Some(StateFilter::Enabled | StateFilter::Disabled) => { + state_filter.and_then(StateFilter::to_pm_flag) + } + _ => Some(PmListPacksFlag::IncludeUninstalled), + } +} + /// List packages on a device with filtering pub fn list_packages( + backend: AdbBackend, device: Option, state_filter: Option, removal_filter: Option, @@ -113,7 +122,7 @@ pub fn list_packages( search: Option, user_id: Option, ) -> Result<(), Box> { - let target_device = get_target_device(device)?; + let target_device = get_target_device(backend, device)?; let uad_lists = load_debloat_lists(false).unwrap_or_else(|lists| lists); println_or_exit!( @@ -129,8 +138,8 @@ pub fn list_packages( search, }; - let pm_flag = state_filter.and_then(StateFilter::to_pm_flag); - let system_packages = ACommand::new() + let pm_flag = resolve_pm_flag(state_filter); + let system_packages = ACommand::with_backend(backend) .shell(&target_device.adb_id) .pm() .list_packages_sys(pm_flag, user_id)?; @@ -141,6 +150,7 @@ pub fn list_packages( &target_device.adb_id, user_id, &context, + backend, )?; if displayed_count == 0 { @@ -159,18 +169,19 @@ pub fn display_package_list( device_serial: &str, user_id: Option, context: &PackageListContext, + backend: AdbBackend, ) -> Result> { let config = context.display_config(); let mut displayed_count = 0; - let enabled_packages: HashSet = ACommand::new() + let enabled_packages: HashSet = ACommand::with_backend(backend) .shell(device_serial) .pm() .list_packages_sys(Some(PmListPacksFlag::OnlyEnabled), user_id) .unwrap_or_default() .into_iter() .collect(); - let disabled_packages: HashSet = ACommand::new() + let disabled_packages: HashSet = ACommand::with_backend(backend) .shell(device_serial) .pm() .list_packages_sys(Some(PmListPacksFlag::OnlyDisabled), user_id) @@ -241,6 +252,7 @@ pub fn display_package_entry( /// Change the state of one or more packages pub fn change_package_state( + backend: AdbBackend, packages: &[String], device: Option, user_id: Option, @@ -252,7 +264,7 @@ pub fn change_package_state( return Err("No packages specified".into()); } - let target_device = get_target_device(device)?; + let target_device = get_target_device(backend, device)?; let user = get_user(&target_device, user_id)?; let uad_lists = load_debloat_lists(false).unwrap_or_else(|lists| lists); @@ -275,6 +287,7 @@ pub fn change_package_state( user, target_state, dry_run, + backend, &uad_lists, )?; println!(); @@ -296,6 +309,7 @@ fn process_package_state_change( user: User, target_state: PackageState, dry_run: bool, + backend: AdbBackend, uad_lists: &HashMap, ) -> Result<(), Box> { let current_state = get_package_state(&device.adb_id, pkg_name, Some(user.id)) @@ -337,6 +351,7 @@ fn process_package_state_change( user, device, &commands, + backend, " ", )?; } @@ -345,6 +360,10 @@ fn process_package_state_change( } /// Execute commands and verify package state with fallback +#[allow( + clippy::too_many_arguments, + reason = "Keeps the shared fallback helper simple across CLI and REPL callers" +)] pub fn execute_with_fallback( package: &str, target_state: PackageState, @@ -352,15 +371,14 @@ pub fn execute_with_fallback( user: User, device: &Phone, commands: &[String], + backend: AdbBackend, indent: &str, ) -> Result<(), Box> { - // 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()) { + match run_adb_shell_action_with_backend(&device.adb_id, cmd.as_str(), backend) { Ok(_) => println!("{}✓ {}", indent, cmd), Err(e) => { return Err(format!("{indent}✗ Failed to execute `{cmd}`: {e:?}").into()); @@ -368,7 +386,6 @@ pub fn execute_with_fallback( } } - // 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, @@ -387,7 +404,6 @@ pub fn execute_with_fallback( 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) { @@ -397,7 +413,6 @@ pub fn execute_with_fallback( } } - // 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, @@ -417,6 +432,7 @@ pub fn execute_with_fallback( /// Show detailed information about a package pub fn show_package_info( + backend: AdbBackend, package: &str, device: Option, user_id: Option, @@ -436,7 +452,7 @@ pub fn show_package_info( } if let Some(device_id) = device { - let target_device = get_target_device(Some(device_id))?; + let target_device = get_target_device(backend, Some(device_id))?; let user = get_user(&target_device, user_id)?; println!("Device: {} ({})", target_device.model, target_device.adb_id); println!("User: {}", user.id); @@ -468,3 +484,19 @@ pub fn generate_completions(shell: Shell) { let name = cmd.get_name().to_string(); generate(shell, &mut cmd, name, &mut std::io::stdout()); } + +/// Show ADB backend and version information +pub fn show_adb_info(backend: AdbBackend) -> Result<(), Box> { + println!("ADB Backend: {}\n", backend); + + match ACommand::with_backend(backend).version() { + Ok(version) => { + println!("{version}"); + Ok(()) + } + Err(e) => { + eprintln!("Failed to get ADB version: {e}"); + Err(e.into()) + } + } +} diff --git a/crates/uad-cli/src/device.rs b/crates/uad-cli/src/device.rs index 54fee8c..5c9d366 100644 --- a/crates/uad-cli/src/device.rs +++ b/crates/uad-cli/src/device.rs @@ -1,11 +1,15 @@ +use uad_core::adb::AdbBackend; 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) -> Result> { - let devices = get_devices_list(); +pub fn get_target_device( + backend: AdbBackend, + device: Option, +) -> Result> { + let devices = get_devices_list(backend); if devices.is_empty() { return Err(NO_DEVICES_FOUND.into()); diff --git a/crates/uad-cli/src/main.rs b/crates/uad-cli/src/main.rs index f473e28..83b3143 100644 --- a/crates/uad-cli/src/main.rs +++ b/crates/uad-cli/src/main.rs @@ -4,12 +4,14 @@ clippy::uninlined_format_args, clippy::map_unwrap_or, clippy::unnecessary_wraps, + clippy::exit, reason = "Suppress non-critical pedantic/style lints to keep build green" )] -use clap::{Parser, Subcommand}; +use clap::{Parser, Subcommand, ValueEnum}; use clap_complete::Shell; use std::process::ExitCode; +use uad_core::adb::AdbBackend; use uad_core::uad_lists::PackageState; mod commands; @@ -20,12 +22,40 @@ mod repl; use filters::{ListFilter, RemovalFilter, StateFilter}; +/// CLI-compatible ADB backend selection +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +pub enum AdbBackendArg { + /// Built-in ADB implementation (no external dependencies) + Builtin, + /// Use system-installed adb binary + System, +} + +impl From for AdbBackend { + fn from(arg: AdbBackendArg) -> Self { + match arg { + AdbBackendArg::Builtin => AdbBackend::Builtin, + AdbBackendArg::System => AdbBackend::System, + } + } +} + #[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 { + /// ADB backend to use: system (default, uses adb binary) or builtin (no dependencies) + #[arg( + short = 'B', + long = "backend", + value_enum, + global = true, + default_value = "system" + )] + backend: AdbBackendArg, + #[command(subcommand)] command: Commands, } @@ -136,6 +166,9 @@ enum Commands { /// Update UAD package lists from remote repository Update, + /// Show ADB backend and version information + Adb, + /// Generate shell completion script Completions { /// Shell to generate completions for @@ -169,10 +202,11 @@ async fn main() -> ExitCode { fn run() -> Result<(), Box> { let cli = Cli::parse(); + let backend: AdbBackend = cli.backend.into(); match cli.command { Commands::Devices => { - commands::list_devices()?; + commands::list_devices(backend)?; } Commands::List { device, @@ -182,7 +216,7 @@ fn run() -> Result<(), Box> { search, user, } => { - commands::list_packages(device, state, removal, list, search, user)?; + commands::list_packages(backend, device, state, removal, list, search, user)?; } Commands::Uninstall { packages, @@ -191,6 +225,7 @@ fn run() -> Result<(), Box> { dry_run, } => { commands::change_package_state( + backend, &packages, device, user, @@ -206,6 +241,7 @@ fn run() -> Result<(), Box> { dry_run, } => { commands::change_package_state( + backend, &packages, device, user, @@ -221,6 +257,7 @@ fn run() -> Result<(), Box> { dry_run, } => { commands::change_package_state( + backend, &packages, device, user, @@ -234,16 +271,19 @@ fn run() -> Result<(), Box> { device, user, } => { - commands::show_package_info(&package, device, user)?; + commands::show_package_info(backend, &package, device, user)?; } Commands::Update => { commands::update_lists()?; } + Commands::Adb => { + commands::show_adb_info(backend)?; + } Commands::Completions { shell } => { commands::generate_completions(shell); } Commands::Repl { device, user } => { - repl::repl_mode(device, user)?; + repl::repl_mode(backend, device, user)?; } } diff --git a/crates/uad-cli/src/output.rs b/crates/uad-cli/src/output.rs index cff4724..ae62a74 100644 --- a/crates/uad-cli/src/output.rs +++ b/crates/uad-cli/src/output.rs @@ -3,16 +3,22 @@ #[macro_export] macro_rules! println_or_exit { () => { - let _ = writeln!(std::io::stdout()); + if writeln!(std::io::stdout()).is_err() { + std::process::exit(0); + } }; ($($arg:tt)*) => { - let _ = writeln!(std::io::stdout(), $($arg)*); + if writeln!(std::io::stdout(), $($arg)*).is_err() { + std::process::exit(0); + } }; } #[macro_export] macro_rules! print_or_exit { ($($arg:tt)*) => { - let _ = write!(std::io::stdout(), $($arg)*); + if write!(std::io::stdout(), $($arg)*).is_err() { + std::process::exit(0); + } }; } diff --git a/crates/uad-cli/src/repl.rs b/crates/uad-cli/src/repl.rs index eeee0c6..47f75ee 100644 --- a/crates/uad-cli/src/repl.rs +++ b/crates/uad-cli/src/repl.rs @@ -2,7 +2,7 @@ use rustyline::DefaultEditor; use rustyline::error::ReadlineError; use std::collections::HashMap; use std::io::Write; -use uad_core::adb::ACommand; +use uad_core::adb::{ACommand, AdbBackend}; 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}; @@ -18,13 +18,15 @@ enum ReplAction { /// Start interactive REPL mode pub fn repl_mode( + backend: AdbBackend, device: Option, user_id: Option, ) -> Result<(), Box> { println!("Universal Android Debloater - Interactive Mode"); + println!("Using {} ADB backend", backend); println!("Type 'help' for available commands, 'exit' or 'quit' to leave\n"); - let target_device = get_target_device(device)?; + let target_device = get_target_device(backend, device)?; let user = get_user(&target_device, user_id)?; println!( @@ -46,7 +48,15 @@ pub fn repl_mode( let readline = rl.readline("uad> "); match readline { Ok(line) => { - match handle_repl_line(&line, &mut rl, &target_device, user, user_id, &uad_lists) { + match handle_repl_line( + &line, + &mut rl, + &target_device, + user, + user_id, + &uad_lists, + backend, + ) { Ok(ReplAction::Continue) => {} Ok(ReplAction::Exit) => break, Err(e) => { @@ -88,6 +98,7 @@ fn handle_repl_line( user: User, user_id: Option, uad_lists: &HashMap, + backend: AdbBackend, ) -> Result> { let line = line.trim(); if line.is_empty() { @@ -108,7 +119,7 @@ fn handle_repl_line( return Ok(ReplAction::Exit); } "list" | "ls" => { - handle_list_command(&parts[1..], device, user_id, uad_lists)?; + handle_list_command(&parts[1..], device, user_id, uad_lists, backend)?; } "info" => { handle_info_command(&parts[1..], device, user.id, uad_lists)?; @@ -121,6 +132,7 @@ fn handle_repl_line( PackageState::Uninstalled, "Uninstalling", uad_lists, + backend, )?; } "enable" | "restore" => { @@ -131,6 +143,7 @@ fn handle_repl_line( PackageState::Enabled, "Enabling", uad_lists, + backend, )?; } "disable" => { @@ -141,12 +154,13 @@ fn handle_repl_line( PackageState::Disabled, "Disabling", uad_lists, + backend, )?; } "device" => { println!( - "Device: {} ({}), Android SDK: {}, User: {}", - device.model, device.adb_id, device.android_sdk, user.id + "Device: {} ({}), Android SDK: {}, User: {}, Backend: {}", + device.model, device.adb_id, device.android_sdk, user.id, backend ); } "clear" => { @@ -216,11 +230,12 @@ fn handle_list_command( device: &Phone, user_id: Option, uad_lists: &HashMap, + backend: AdbBackend, ) -> Result<(), Box> { let parsed = ReplListArgs::parse(args)?; let pm_flag = parsed.state_filter.and_then(StateFilter::to_pm_flag); - let system_packages = ACommand::new() + let system_packages = ACommand::with_backend(backend) .shell(&device.adb_id) .pm() .list_packages_sys(pm_flag, user_id)?; @@ -238,6 +253,7 @@ fn handle_list_command( &device.adb_id, user_id, &context, + backend, )?; if displayed_count == 0 { @@ -287,6 +303,7 @@ fn handle_state_change_command( target_state: PackageState, action_name: &str, uad_lists: &HashMap, + backend: AdbBackend, ) -> Result<(), Box> { if args.is_empty() { eprintln!( @@ -297,7 +314,15 @@ fn handle_state_change_command( } for pkg_name in args { - process_package_change(pkg_name, device, user, target_state, action_name, uad_lists)?; + process_package_change( + pkg_name, + device, + user, + target_state, + action_name, + uad_lists, + backend, + )?; } Ok(()) @@ -311,6 +336,7 @@ fn process_package_change( target_state: PackageState, action_name: &str, uad_lists: &HashMap, + backend: AdbBackend, ) -> Result<(), Box> { let current_state = get_package_state(&device.adb_id, pkg_name, Some(user.id)) .ok_or("Package not found on device")?; @@ -346,6 +372,7 @@ fn process_package_change( user, device, &commands, + backend, " ", ) } diff --git a/crates/uad-core/Cargo.toml b/crates/uad-core/Cargo.toml index f7d3a43..16e1dbd 100644 --- a/crates/uad-core/Cargo.toml +++ b/crates/uad-core/Cargo.toml @@ -15,6 +15,7 @@ default = [] self-update = ["dep:flate2", "dep:tar"] [dependencies] +adb_client.workspace = true serde.workspace = true serde_json.workspace = true log.workspace = true diff --git a/crates/uad-core/src/adb.rs b/crates/uad-core/src/adb.rs index e7d223c..b6f2b3f 100644 --- a/crates/uad-core/src/adb.rs +++ b/crates/uad-core/src/adb.rs @@ -4,8 +4,7 @@ //! //! Following the design philosophy of most of Rust `std`, //! `*Command` are intended to be "thin wrappers" (low-overhead abstractions) -//! around the ADB CLI or `adb_client` -//! ([in the future](https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/issues/700) ), +//! around `adb_client` or the system ADB CLI, //! which implies: //! - no "magic" //! - no custom commands @@ -33,11 +32,24 @@ //! please, **PLEASE** refrain from falling-back to any `Command`-like API. //! Rather, please extend these APIs in a consistent way. //! +//! ## Backend Selection +//! +//! This module supports two ADB backends: +//! - **Builtin** (`adb_client`): Pure Rust implementation, no external dependencies +//! - **System**: Uses the system-installed `adb` binary +//! +//! Use [`ACommand::with_backend`] to select a specific backend, +//! or [`ACommand::new`] to use the default (System backend). +//! //! Thank you! ❤️ //! //! For comprehensive info about ADB, //! [see this](https://android.googlesource.com/platform/packages/modules/adb/+/refs/heads/master/docs/) +use adb_client::{ADBDeviceExt, server::ADBServer}; +use serde::{Deserialize, Serialize}; +use std::fmt::Write as _; +use std::io::Cursor; use std::rc::Rc; #[cfg(target_os = "windows")] @@ -53,25 +65,83 @@ pub fn to_trimmed_utf8(v: &[u8]) -> String { String::from_utf8_lossy(v).trim_end().to_string() } +/// ADB backend selection. +/// +/// - **Builtin**: Uses the `adb_client` crate (pure Rust, no external dependencies) +/// - **System**: Uses the system-installed `adb` binary +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +pub enum AdbBackend { + /// Built-in ADB implementation via `adb_client` crate. + /// The application can communicate with devices without needing `adb` installed. + Builtin, + /// Uses the system-installed `adb` binary. + /// This is the default to preserve existing behavior. + /// Requires `adb` to be available in PATH. + /// Useful if you prefer using your own ADB installation or need specific ADB features. + #[default] + System, +} + +impl AdbBackend { + /// Returns all available backend variants for UI enumeration + pub const ALL: [Self; 2] = [Self::Builtin, Self::System]; +} + +impl std::fmt::Display for AdbBackend { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Builtin => write!(f, "Builtin"), + Self::System => write!(f, "System (adb)"), + } + } +} + #[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 + let mut parts = s.split('.'); + let is_digits = |c: &str| !c.is_empty() && c.bytes().all(|b| b.is_ascii_digit()); + parts.next().is_some_and(is_digits) + && parts.next().is_some_and(is_digits) + && parts.next().is_some_and(is_digits) + && parts.next().is_none() } -/// Builder object for an Android Debug Bridge CLI command, +#[cfg(debug_assertions)] +fn assert_adb_version_output_format(out: &str) { + 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") + } +} + +/// Internal state for `ACommand` - tracks the device serial and backend to use +#[derive(Debug)] +struct ACommandState { + device_serial: Option, + backend: AdbBackend, +} + +/// Builder object for an Android Debug Bridge command, /// using the type-state and new-type patterns. /// /// This is not intended to model the entire ADB API. @@ -79,12 +149,22 @@ fn is_version_triple(s: &str) -> bool { /// /// [More info here](https://developer.android.com/tools/adb) #[derive(Debug)] -pub struct ACommand(std::process::Command); +pub struct ACommand(ACommandState); + impl ACommand { - /// `adb` command builder + /// `adb` command builder with the default backend. #[must_use] pub fn new() -> Self { - Self(std::process::Command::new("adb")) + Self::with_backend(AdbBackend::default()) + } + + /// `adb` command builder with a specific backend + #[must_use] + pub fn with_backend(backend: AdbBackend) -> Self { + Self(ACommandState { + device_serial: None, + backend, + }) } /// `shell` sub-command builder. @@ -94,9 +174,8 @@ impl ACommand { pub fn shell>(mut self, device_serial: S) -> ShellCommand { let serial = device_serial.as_ref(); if !serial.is_empty() { - self.0.args(["-s", serial]); + self.0.device_serial = Some(serial.to_string()); } - self.0.arg("shell"); ShellCommand(self) } @@ -108,109 +187,189 @@ impl ACommand { /// Status can be (but not limited to): /// - "unauthorized" /// - "device" - pub fn devices(mut self) -> Result, String> { - self.0.arg("devices"); - Ok(self - .run()? - .lines() - .skip(1) // header - .map(|dev_stat| { - let tab_idx = dev_stat - // OS-specific? - .find('\t') - // True on Linux, - // no matter if ADB is piped or connected to terminal - .expect("There must be 1 tab after serial"); - ( - // serial - dev_stat[..tab_idx].to_string(), - // status - dev_stat[(tab_idx + 1)..].to_string(), - ) - }) - .collect()) + pub fn devices(self) -> Result, String> { + match self.0.backend { + AdbBackend::Builtin => Self::devices_builtin(), + AdbBackend::System => Self::devices_system(), + } } - /// `version` sub-command + /// Returns version information from the ADB server/binary. /// - /// ## Format - /// This is just a sample, - /// we don't know which guarantees are stable (yet): + /// ## Builtin backend + /// Returns the ADB server protocol version: + /// ```txt + /// ADB Server Version: 1.0.41 + /// ``` + /// + /// ## System backend + /// Returns the full `adb version` output (format may vary by installation): /// ```txt /// Android Debug Bridge version 1.0.41 - /// Version 34.0.5-debian - /// Installed as /usr/lib/android-sdk/platform-tools/adb - /// Running on Linux 6.12.12-amd64 (x86_64) + /// Version 35.0.2-android-tools + /// Installed as /usr/bin/adb + /// Running on Linux 6.18 (x86_64) /// ``` - /// - /// The expected format should be like: - /// ```txt - /// Android Debug Bridge version .. - /// Version ..- - /// Installed as /platform-tools/adb[.exe] - /// Running on () - /// ``` - #[expect(clippy::panic_in_result_fn, reason = "Assertions are fine")] - pub fn version(mut self) -> Result { - self.0.arg("version"); - let out = self.run()?; + pub fn version(self) -> Result { + match self.0.backend { + AdbBackend::Builtin => Self::version_builtin(), + AdbBackend::System => Self::version_system(), + } + } - #[cfg(debug_assertions)] - { - const ADBV: &str = "Android Debug Bridge version "; - const V: &str = "Version "; + // ========== Builtin backend implementation (adb_client) ========== - 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") + /// Get ADB server version using the builtin `adb_client` + fn version_builtin() -> Result { + let mut server = ADBServer::default(); + match server.version() { + Ok(version) => Ok(format!("ADB Server Version: {version}")), + Err(e) => { + error!("Failed to get ADB server version: {e}"); + Err(format!("Cannot get ADB server version: {e}")) } } + } + + /// List devices using the builtin `adb_client` + fn devices_builtin() -> Result, String> { + let mut server = ADBServer::default(); + server + .devices() + .map(|device_list| { + device_list + .into_iter() + .map(|dev| (dev.identifier, dev.state.to_string())) + .collect() + }) + .map_err(|e| { + error!("ADB: {e}"); + format!("Cannot connect to ADB server: {e}") + }) + } + + /// Execute a shell command via `adb_client` (builtin backend) + fn run_shell_command_builtin(&self, shell_command: &str) -> Result { + let mut server = ADBServer::default(); + + // Validate device availability and serial + if let Some(ref serial) = self.0.device_serial { + let device_list = server + .devices() + .map_err(|e| format!("Cannot get device list: {e}"))?; + + if !device_list.iter().any(|d| d.identifier == *serial) { + let available = device_list + .iter() + .map(|d| d.identifier.as_str()) + .collect::>() + .join(", "); + return Err(format!( + "Device '{serial}' not found. Available: {available}" + )); + } + } + + let mut device = server + .get_device() + .map_err(|e| format!("Cannot connect to device: {e}"))?; + + if shell_command.trim().is_empty() { + return Err("Empty shell command".into()); + } + + info!("Ran command: adb shell {shell_command}"); + + let mut buffer = Vec::new(); + let _exit_code = device + .shell_command(&shell_command, Some(&mut Cursor::new(&mut buffer)), None) + .map_err(|e| { + error!("ADB shell command failed: {e}"); + format!("Shell command failed: {e}") + })?; + + Ok(String::from_utf8_lossy(&buffer).trim_end().to_string()) + } + + // ========== System backend implementation (adb binary) ========== + + /// Get ADB version using the system `adb` binary + fn version_system() -> Result { + let mut cmd = std::process::Command::new("adb"); + cmd.arg("version"); + let out = Self::run_system_command(cmd)?; + + #[cfg(debug_assertions)] + assert_adb_version_output_format(&out); Ok(out) } - /// General executor - fn run(self) -> Result { - let mut cmd = self.0; + /// List devices using the system `adb` binary + fn devices_system() -> Result, String> { + let mut cmd = std::process::Command::new("adb"); + cmd.arg("devices"); + Ok(Self::run_system_command(cmd)? + .lines() + .skip(1) // header + .filter_map(|line| { + let (serial, status) = line.split_once('\t')?; + Some((serial.to_string(), status.to_string())) + }) + .collect()) + } + + /// Execute a shell command via system `adb` binary + fn run_shell_command_system(&self, shell_command: &str) -> Result { + let mut cmd = std::process::Command::new("adb"); + + if let Some(ref serial) = self.0.device_serial { + cmd.args(["-s", serial]); + } + + cmd.arg("shell"); + cmd.arg(shell_command); + + info!("Ran command: adb shell {}", shell_command); + Self::run_system_command(cmd) + } + + /// General system command executor for adb binary + fn run_system_command(mut cmd: std::process::Command) -> Result { #[cfg(target_os = "windows")] let cmd = cmd.creation_flags(0x0800_0000); // do not open a cmd window info!( "Ran command: adb {}", cmd.get_args() - .map(|s| s.to_str().unwrap_or_else(|| unreachable!())) + .filter_map(|s| s.to_str()) .collect::>() .join(" ") ); - 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) - } - } + + let output = cmd.output().map_err(|e| { + error!("ADB: {e}"); + "Cannot run ADB, likely not found".to_string() + })?; + + let stdout = to_trimmed_utf8(&output.stdout); + if output.status.success() { + Ok(stdout) + } else { + // ADB sometimes outputs errors to stdout instead of stderr + Err(if stdout.is_empty() { + to_trimmed_utf8(&output.stderr) + } else { + stdout + }) + } + } + + /// Execute a shell command using the configured backend + fn run_shell_command(&self, shell_command: &str) -> Result { + match self.0.backend { + AdbBackend::Builtin => self.run_shell_command_builtin(shell_command), + AdbBackend::System => self.run_shell_command_system(shell_command), } } } @@ -227,12 +386,13 @@ impl Default for ACommand { /// [More info](https://chromium.googlesource.com/aosp/platform/system/core/+/refs/heads/upstream/shell_and_utilities). #[derive(Debug)] pub struct ShellCommand(ACommand); + impl ShellCommand { /// `pm` command builder - pub fn pm(mut self) -> PmCommand { - self.0.0.arg("pm"); + pub fn pm(self) -> PmCommand { PmCommand(self) } + /// Query a device property value, by its key. /// These can be of any type: /// - `boolean` @@ -241,36 +401,26 @@ impl ShellCommand { /// - etc... /// /// So to avoid lossy conversions, we return strs - pub fn getprop(mut self, key: &str) -> Result { - self.0.0.args(["getprop", key]); - self.0.run() + pub fn getprop(self, key: &str) -> Result { + self.0.run_shell_command(&format!("getprop {key}")) } + /// Reboots device - pub fn reboot(mut self) -> Result { - self.0.0.arg("reboot"); - self.0.run() + pub fn reboot(self) -> Result { + self.0.run_shell_command("reboot") } /// 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 { - self.0.0.arg(action); - self.0.run() + pub fn raw(self, action: &str) -> Result { + self.0.run_shell_command(action) } } #[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 - } + !s.is_empty() && s[0].is_ascii_alphabetic() && (s.len() == 1 || is_all_w_c(s.split_at(1).1)) } /// String with the invariant of being a valid package-name. @@ -314,8 +464,7 @@ pub enum PmListPacksFlag { OnlyDisabled, } impl PmListPacksFlag { - // is there a trait for this? - fn to_str(self) -> &'static str { + const fn as_str(self) -> &'static str { match self { Self::IncludeUninstalled => "-u", Self::OnlyEnabled => "-e", @@ -323,10 +472,10 @@ impl PmListPacksFlag { } } } -#[expect(clippy::to_string_trait_impl, reason = "This is not user-facing")] -impl ToString for PmListPacksFlag { - fn to_string(&self) -> String { - self.to_str().to_string() + +impl std::fmt::Display for PmListPacksFlag { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) } } @@ -348,29 +497,25 @@ impl PmCommand { /// - isn't sorted /// - duplicates never _seem_ to happen, but don't assume uniqueness pub fn list_packages_sys( - mut self, - f: Option, + self, + flag: Option, user_id: Option, ) -> Result, String> { - let cmd = &mut self.0.0.0; - - cmd.args(["list", "packages", "-s"]); - if let Some(s) = f { - cmd.arg(s.to_str()); + let mut command = String::from("pm list packages -s"); + if let Some(f) = flag { + write!(&mut command, " {f}").ok(); } - if let Some(u) = user_id { - cmd.arg("--user"); - cmd.arg(u.to_string()); + if let Some(uid) = user_id { + write!(&mut command, " --user {uid}").ok(); } - self.0.0.run().map(|pack_ls| { - pack_ls + self.0.raw(&command).map(|output| { + output .lines() - .map(|p_ln| { - debug_assert!(p_ln.starts_with(PACK_PREFIX)); - let p = &p_ln[PACK_PREFIX.len()..]; - debug_assert!(PackageId::new(p).is_some()); - String::from(p) + .filter_map(|line| { + let pkg = line.strip_prefix(PACK_PREFIX)?; + debug_assert!(PackageId::new(pkg).is_some()); + Some(pkg.to_string()) }) .collect() }) @@ -380,56 +525,22 @@ impl PmCommand { /// /// - /// - - pub fn list_users(mut self) -> Result, String> { - self.0.0.0.args(["list", "users"]); + pub fn list_users(self) -> Result, String> { + // Expected shape: "UserInfo{::}[ running]" + // https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/content/pm/UserInfo.java Ok(self .0 - .0 - .run()? + .raw("pm list users")? .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{::}[ 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, - } + .filter_map(|line| { + // Defensive parsing: strip known prefixes/suffixes, extract ID + let s = line.trim_ascii(); + let s = s.strip_prefix("UserInfo{").unwrap_or(s); + let s = s.strip_suffix("running").unwrap_or(s).trim_ascii_end(); + let s = s.strip_suffix('}').unwrap_or(s); + let id = s.split(':').next()?.parse().ok()?; + Some(UserInfo { id }) }) .collect()) } @@ -496,4 +607,15 @@ mod tests { assert_ne!(PackageId::new(p_id), None); } } + + #[test] + fn backend_display() { + assert_eq!(AdbBackend::Builtin.to_string(), "Builtin"); + assert_eq!(AdbBackend::System.to_string(), "System (adb)"); + } + + #[test] + fn backend_default_is_system() { + assert_eq!(AdbBackend::default(), AdbBackend::System); + } } diff --git a/crates/uad-core/src/config.rs b/crates/uad-core/src/config.rs index 5522b16..4539261 100644 --- a/crates/uad-core/src/config.rs +++ b/crates/uad-core/src/config.rs @@ -1,5 +1,6 @@ use crate::CACHE_DIR; use crate::CONFIG_DIR; +use crate::adb::AdbBackend; use crate::sync::User; use crate::utils::DisplayablePath; use log::error; @@ -22,6 +23,9 @@ pub struct GeneralSettings { pub theme: String, pub expert_mode: bool, pub backup_folder: PathBuf, + /// ADB backend to use: Builtin (no external dependencies) or System (uses installed adb) + #[serde(default)] + pub adb_backend: AdbBackend, } #[derive(Default, Debug, Clone)] @@ -49,6 +53,7 @@ impl Default for GeneralSettings { theme: DEFAULT_THEME.to_string(), expert_mode: false, backup_folder: CACHE_DIR.join("backups"), + adb_backend: AdbBackend::default(), } } } diff --git a/crates/uad-core/src/sync.rs b/crates/uad-core/src/sync.rs index 9bdd7cc..5a1ca55 100644 --- a/crates/uad-core/src/sync.rs +++ b/crates/uad-core/src/sync.rs @@ -1,5 +1,5 @@ use crate::{ - adb::{ACommand as AdbCommand, PM_CLEAR_PACK, PackageId}, + adb::{ACommand as AdbCommand, AdbBackend, PM_CLEAR_PACK, PackageId}, uad_lists::PackageState, }; use log::{error, info}; @@ -61,13 +61,25 @@ pub enum AdbError { /// This replaces the deprecated `adb_shell_command`. /// /// If `serial` is empty, it lets ADB choose the default device. +/// Uses the default ADB backend. pub fn run_adb_shell_action>( device_serial: S, action: &str, +) -> Result { + run_adb_shell_action_with_backend(device_serial, action, AdbBackend::default()) +} + +/// Run an arbitrary shell action via the typed ADB wrapper with a specific backend. +/// +/// If `serial` is empty, it lets ADB choose the default device. +pub fn run_adb_shell_action_with_backend>( + device_serial: S, + action: &str, + backend: AdbBackend, ) -> Result { let serial = device_serial.as_ref(); - match AdbCommand::new().shell(serial).raw(action) { + match AdbCommand::with_backend(backend).shell(serial).raw(action) { Ok(o) => { if ["Error", "Failure"].iter().any(|&e| o.contains(e)) { let friendly_msg = make_friendly_error_message(&o, action); @@ -213,7 +225,7 @@ pub fn request_builder(commands: &[&str], package: &str, user: Option) -> // 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_some() { + if PackageId::new(package).is_none() { error!("request_builder: refusing invalid package name: {package:?}"); return Vec::new(); } @@ -224,23 +236,48 @@ pub fn request_builder(commands: &[&str], package: &str, user: Option) -> .collect() } +#[must_use] +fn sanitize_device_prop(raw: &str) -> String { + raw.chars() + .filter(|c| !c.is_control()) + .collect::() + .trim() + .to_string() +} + +#[must_use] +fn parse_sdk_version(raw: &str) -> Option { + let sanitized = sanitize_device_prop(raw); + let digits: String = sanitized + .chars() + .skip_while(|c| !c.is_ascii_digit()) + .take_while(char::is_ascii_digit) + .collect(); + + if digits.is_empty() { + None + } else { + digits.parse().ok() + } +} + /// 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| { + match AdbCommand::new().shell(serial).getprop("ro.product.model") { + Ok(model) => sanitize_device_prop(&model), + Err(err) => { eprintln!("ERROR: {err}"); error!("{err}"); if err.contains("adb: no devices/emulators found") { "no devices/emulators found".to_string() } else { - err + sanitize_device_prop(&err) } - }) + } + } } /// Get the brand by querying the `ro.product.brand` property. @@ -251,8 +288,7 @@ 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()) + .map(|s| sanitize_device_prop(&s)) .unwrap_or_default() } @@ -266,7 +302,14 @@ pub fn get_android_sdk(device_serial: &str) -> u8 { .shell(device_serial) .getprop("ro.build.version.sdk") .map_or(0, |sdk| { - sdk.parse().expect("SDK version numeral must be valid") + if let Some(v) = parse_sdk_version(&sdk) { + v + } else { + error!( + "Invalid SDK version {sdk:?} for device '{device_serial}'. Falling back to 0" + ); + 0 + } }) } @@ -452,31 +495,39 @@ pub fn list_users_idx_prot(device_serial: &str) -> Vec { .unwrap_or_default() } -/// This matches serials (`getprop ro.serialno`) -/// that are authorized by the user. +/// Get list of connected devices using a specific ADB backend. #[must_use] -pub fn get_devices_list() -> Vec { +pub fn get_devices_list(backend: AdbBackend) -> Vec { retry( Fixed::from_millis(500).take(if cfg!(debug_assertions) { 3 } else { 10 }), - || match AdbCommand::new().devices() { + || match AdbCommand::with_backend(backend).devices() { Ok(devices) => { let mut device_list: Vec = vec![]; if devices.iter().all(|(_, stat)| stat != "device") { return OperationResult::Retry(vec![]); } - for device in devices { - let serial = &device.0; + for (serial, status) in devices { + if status != "device" { + continue; + } 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(), + 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, }); } + if device_list.is_empty() { + return OperationResult::Retry(vec![]); + } OperationResult::Ok(device_list) } Err(err) => { - error!("get_devices_list() -> {err}"); + error!("get_devices_list(backend) -> {err}"); let test: Vec = vec![]; OperationResult::Retry(test) } @@ -486,8 +537,8 @@ pub fn get_devices_list() -> Vec { } #[must_use] -pub fn initial_load() -> bool { - match AdbCommand::new().devices() { +pub fn initial_load(backend: AdbBackend) -> bool { + match AdbCommand::with_backend(backend).devices() { Ok(_devices) => true, Err(_err) => false, } diff --git a/crates/uad-gui/src/gui.rs b/crates/uad-gui/src/gui.rs index 925aedf..75994f1 100644 --- a/crates/uad-gui/src/gui.rs +++ b/crates/uad-gui/src/gui.rs @@ -83,21 +83,27 @@ impl UadGui { 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), - ) + if self.selected_device.is_some() { + format!("{FULL_NAME} - device connected") + } else { + FULL_NAME.to_string() + } } fn new() -> (Self, Task) { + let app = Self::default(); + let backend = app.settings_view.general.adb_backend; ( - Self::default(), + app, 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), - Task::perform(async { initial_load() }, Message::ADBSatisfied), - Task::perform(async { get_devices_list() }, Message::LoadDevices), + Task::perform(async move { initial_load(backend) }, Message::ADBSatisfied), + Task::perform( + async move { get_devices_list(backend) }, + Message::LoadDevices, + ), Task::perform( async move { get_latest_release() }, Message::GetLatestRelease, @@ -196,16 +202,21 @@ impl UadGui { } Message::RefreshButtonPressed => { self.apps_view = AppsView::default(); + let backend = self.settings_view.general.adb_backend; #[expect(unused_must_use, reason = "side-effect")] { self.update(Message::AppsAction(AppsMessage::ADBSatisfied( self.adb_satisfied, ))); } - Task::perform(async { get_devices_list() }, Message::LoadDevices) + Task::perform( + async move { get_devices_list(backend) }, + Message::LoadDevices, + ) } Message::RebootButtonPressed => { self.apps_view = AppsView::default(); + let backend = self.settings_view.general.adb_backend; let serial = match &self.selected_device { Some(d) => d.adb_id.clone(), _ => String::default(), @@ -213,7 +224,7 @@ impl UadGui { self.selected_device = None; self.devices_list = vec![]; Task::perform( - async { adb::ACommand::new().shell(serial).reboot() }, + async move { adb::ACommand::with_backend(backend).shell(serial).reboot() }, |_| Message::Nothing, ) } diff --git a/crates/uad-gui/src/views/about.rs b/crates/uad-gui/src/views/about.rs index 42cb723..93d81df 100644 --- a/crates/uad-gui/src/views/about.rs +++ b/crates/uad-gui/src/views/about.rs @@ -3,10 +3,8 @@ 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}; @@ -95,38 +93,10 @@ impl About { .spacing(10) .width(550); - /* - There's no need to fetch this info every time the view is updated, - we could cache it in a `static` `LazyLock`. - - But what if the system updates ADB while the app is running? - the numbers will be out of sync! - - However, the server will still be the "old" version - until it's killed - */ - 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_y(Alignment::Center).width(550); - #[cfg(feature = "self-update")] - let update_column = column![uad_list_row, self_update_row, adb_version_row]; + let update_column = column![uad_list_row, self_update_row]; #[cfg(not(feature = "self-update"))] - let update_column = column![uad_list_row, adb_version_row]; + let update_column = column![uad_list_row]; let update_column = update_column.align_x(Alignment::Center).spacing(10); diff --git a/crates/uad-gui/src/views/list.rs b/crates/uad-gui/src/views/list.rs index a9792dd..8701e92 100644 --- a/crates/uad-gui/src/views/list.rs +++ b/crates/uad-gui/src/views/list.rs @@ -5,7 +5,7 @@ use crate::widgets::navigation_menu::ICONS; use log::{error, info, warn}; use std::path::PathBuf; use uad_core::config::DeviceSettings; -use uad_core::sync::{AdbError, Phone, User, apply_pkg_state_commands}; +use uad_core::sync::{AdbError, CorePackage, Phone, User, apply_pkg_state_commands}; use uad_core::uad_lists::{ Opposite, PackageHashMap, PackageState, Removal, UadList, UadListState, load_debloat_lists, }; @@ -130,6 +130,33 @@ impl From for SummaryEntry { } impl List { + #[inline] + fn current_user_index(&self) -> usize { + self.selected_user.unwrap_or_default().index + } + + #[inline] + fn refilter(&mut self) -> Task { + Self::filter_package_lists(self); + Task::none() + } + + #[inline] + fn map_core_to_row(uad_list: &PackageHashMap, core: &CorePackage) -> PackageRow { + let list = uad_list + .get(&core.name) + .map_or(UadList::Unlisted, |p| p.list); + PackageRow::new( + &core.name, + &core.description, + core.removal, + core.state, + list, + false, + false, + ) + } + pub fn update( &mut self, settings: &mut Settings, @@ -216,7 +243,7 @@ impl List { waiting_view("Finding connected devices...", None, style::Text::Default) } else { waiting_view( - "ADB is not installed on your system, install ADB and relaunch application.", + "No device connection detected. Connect a device with USB debugging enabled and authorized, then relaunch.", Some(button("Read on how to get started.") .on_press(Message::GoToUrl(PathBuf::from( "https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Getting-started", @@ -329,12 +356,13 @@ impl List { settings: &Settings, selected_device: &Phone, ) -> Element<'_, Message, Theme, Renderer> { + let i_user = self.current_user_index(); let packages = self .filtered_packages .iter() .fold(column![].spacing(6), |col, &i| { col.push( - self.phone_packages[self.selected_user.unwrap_or_default().index][i] + self.phone_packages[i_user][i] .view(settings, selected_device) .map(move |msg| Message::List(i, msg)), ) @@ -748,8 +776,7 @@ impl List { .selected_removal .expect("removal recommendation must be selected"); - self.filtered_packages = self.phone_packages - [self.selected_user.expect("User must be selected").index] + self.filtered_packages = self.phone_packages[self.current_user_index()] .iter() // we must filter the indices associated with pack-rows, // that's why `enumerate` is before `filter`. @@ -776,7 +803,7 @@ impl List { vec![ fetch_packages(&uad_list, serial, None) .into_iter() - .map(PackageRow::from) + .map(|core| Self::map_core_to_row(&uad_list, &core)) .collect(), ] } else { @@ -785,7 +812,7 @@ impl List { .map(|user| { fetch_packages(&uad_list, serial, Some(user.id)) .into_iter() - .map(PackageRow::from) + .map(|core| Self::map_core_to_row(&uad_list, &core)) .collect() }) .collect() @@ -841,7 +868,7 @@ impl List { } fn on_restoring_device(&mut self, output: Result) -> Task { - let i_user = self.selected_user.unwrap_or_default().index; + let i_user = self.current_user_index(); if let Ok(p) = output { self.loading_state = LoadingState::RestoringDevice(self.phone_packages[i_user][p.index].name.clone()); @@ -886,7 +913,7 @@ impl List { } fn on_apply_filters(&mut self, packages: Vec>) -> Task { - let i_user = self.selected_user.unwrap_or_default().index; + let i_user = self.current_user_index(); self.phone_packages = packages; self.filtered_packages = (0..self.phone_packages[i_user].len()).collect(); self.selected_removal = Some(Removal::Recommended); @@ -906,7 +933,7 @@ impl List { selected_device: &mut Phone, list_update_state: &mut UadListState, ) -> Task { - let i_user = self.selected_user.unwrap_or_default().index; + let i_user = self.current_user_index(); for i in self.filtered_packages.clone() { if self.phone_packages[i_user][i].selected != selected { #[expect(unused_must_use, reason = "side-effect")] @@ -924,26 +951,22 @@ impl List { fn on_search_input_changed(&mut self, letter: String) -> Task { self.input_value = letter; - Self::filter_package_lists(self); - Task::none() + self.refilter() } fn on_list_selected(&mut self, list: UadList) -> Task { self.selected_list = Some(list); - Self::filter_package_lists(self); - Task::none() + self.refilter() } fn on_package_state_selected(&mut self, package_state: PackageState) -> Task { self.selected_package_state = Some(package_state); - Self::filter_package_lists(self); - Task::none() + self.refilter() } fn on_removal_selected(&mut self, removal: Removal) -> Task { self.selected_removal = Some(removal); - Self::filter_package_lists(self); - Task::none() + self.refilter() } fn on_list_row( @@ -953,7 +976,7 @@ impl List { settings: &Settings, selected_device: &mut Phone, ) -> Task { - let i_user = self.selected_user.unwrap_or_default().index; + let i_user = self.current_user_index(); #[expect(unused_must_use, reason = "side-effect")] { self.phone_packages[i_user][i_package] @@ -1031,8 +1054,7 @@ impl List { self.selected_user = Some(user); self.fallback_notifications.clear(); self.filtered_packages = (0..self.phone_packages[user.index].len()).collect(); - Self::filter_package_lists(self); - Task::none() + self.refilter() } #[allow( diff --git a/crates/uad-gui/src/views/settings.rs b/crates/uad-gui/src/views/settings.rs index c19b999..8a850a5 100644 --- a/crates/uad-gui/src/views/settings.rs +++ b/crates/uad-gui/src/views/settings.rs @@ -15,6 +15,7 @@ use iced::{Alignment, Element, Length, Renderer, Task, alignment}; use log::{debug, error, info}; use std::path::PathBuf; use uad_core::{ + adb::{ACommand, AdbBackend}, config::{BackupSettings, Config, DeviceSettings, GeneralSettings}, save::{backup_phone, list_available_backup_user, list_available_backups, restore_backup}, sync::{ @@ -35,15 +36,23 @@ pub struct Settings { pub device: DeviceSettings, is_loading: bool, modal: Option, + /// Cached ADB version string for display + adb_version: String, } impl Default for Settings { fn default() -> Self { + let general = Config::load_configuration_file().general; + // Fetch initial ADB version + let adb_version = ACommand::with_backend(general.adb_backend) + .version() + .unwrap_or_else(|e| format!("Error: {e}")); Self { - general: Config::load_configuration_file().general, + general, device: DeviceSettings::default(), is_loading: false, modal: None, + adb_version, } } } @@ -55,6 +64,8 @@ pub enum Message { DisableMode(bool), MultiUserMode(bool), ApplyTheme(Theme), + ApplyAdbBackend(AdbBackend), + AdbVersionFetched(Result), UrlPressed(PathBuf), BackupSelected(DisplayablePath), BackupDevice, @@ -83,6 +94,8 @@ impl Settings { 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::ApplyAdbBackend(backend) => self.handle_apply_adb_backend(backend), + Message::AdbVersionFetched(result) => self.handle_adb_version_fetched(result), 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), @@ -138,6 +151,28 @@ impl Settings { Task::none() } + fn handle_apply_adb_backend(&mut self, backend: AdbBackend) -> Task { + self.general.adb_backend = backend; + self.adb_version = "Fetching...".to_string(); + debug!("Config change: {self:?}"); + let mut config = Config::load_configuration_file(); + config.save_device_settings(self.device.clone(), self.general.clone()); + info!("ADB backend changed to: {backend}"); + // Fetch version asynchronously + Task::perform( + async move { ACommand::with_backend(backend).version() }, + Message::AdbVersionFetched, + ) + } + + fn handle_adb_version_fetched(&mut self, result: Result) -> Task { + self.adb_version = match result { + Ok(version) => version, + Err(e) => format!("Error: {e}"), + }; + Task::none() + } + fn handle_url_pressed(url: PathBuf) -> Task { open_url(url); Task::none() @@ -360,6 +395,8 @@ impl Settings { self.theme_container(), text("General").size(26), self.general_container(), + text("ADB").size(26), + self.adb_container(), text("Current device").size(26), Self::no_device_container(), text("Backup / Restore").size(26), @@ -380,6 +417,8 @@ impl Settings { self.theme_container(), text("General").size(26), self.general_container(), + text("ADB").size(26), + self.adb_container(), text("Current device").size(26), Self::warning_container(phone), self.device_specific_container(phone), @@ -459,6 +498,49 @@ impl Settings { .into() } + fn adb_container(&self) -> Element<'_, Message, Theme, Renderer> { + let backend_label = text("Backend:").size(16); + let radio_btn_backend = + AdbBackend::ALL + .iter() + .fold(row![].spacing(10), |row_content, option| { + row_content.push( + radio( + format!("{option}"), + *option, + Some(self.general.adb_backend), + Message::ApplyAdbBackend, + ) + .size(20), + ) + }); + + let backend_row = row![backend_label, radio_btn_backend] + .spacing(10) + .align_y(Alignment::Center); + + let backend_descr = text( + "Builtin: Uses embedded ADB (no external dependencies). System: Uses your installed adb binary.", + ) + .style(style::Text::Commentary); + + let version_row = row![ + text("Version: ").size(14), + text(&self.adb_version) + .size(14) + .style(style::Text::Commentary), + ] + .spacing(5) + .align_y(Alignment::Center); + + container(column![backend_row, backend_descr, version_row,].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![ diff --git a/flake.lock b/flake.lock index b3b5542..d212ee9 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1776949667, - "narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=", + "lastModified": 1781607440, + "narHash": "sha256-rxO+uc/KFbSJp+pgyXRuAX6QlG9hJdnt0BXpEQRXY+U=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30", + "rev": "3e41b24abd260e8f71dbe2f5737d24122f972158", "type": "github" }, "original": { From 528ef586dabe1117fbcea2b0823e8871010b00f4 Mon Sep 17 00:00:00 2001 From: Angel J <78835633+iamanaws@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:57:11 -0700 Subject: [PATCH 02/16] feat(adb): gate built-in backend behind default feature --- crates/uad-cli/Cargo.toml | 6 +++++- crates/uad-cli/src/main.rs | 4 +++- crates/uad-core/Cargo.toml | 5 +++-- crates/uad-core/src/adb.rs | 18 +++++++++++++++++- crates/uad-gui/Cargo.toml | 9 +++++---- crates/uad-gui/src/views/settings.rs | 9 +++++---- 6 files changed, 38 insertions(+), 13 deletions(-) diff --git a/crates/uad-cli/Cargo.toml b/crates/uad-cli/Cargo.toml index 6045f95..9759dcf 100644 --- a/crates/uad-cli/Cargo.toml +++ b/crates/uad-cli/Cargo.toml @@ -14,8 +14,12 @@ edition.workspace = true name = "uad" path = "src/main.rs" +[features] +default = ["builtin-adb"] +builtin-adb = ["uad-core/builtin-adb"] + [dependencies] -uad-core = { path = "../uad-core" } +uad-core = { path = "../uad-core", default-features = false } clap.workspace = true clap_complete = "4.4" tokio.workspace = true diff --git a/crates/uad-cli/src/main.rs b/crates/uad-cli/src/main.rs index 83b3143..560baa2 100644 --- a/crates/uad-cli/src/main.rs +++ b/crates/uad-cli/src/main.rs @@ -26,6 +26,7 @@ use filters::{ListFilter, RemovalFilter, StateFilter}; #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] pub enum AdbBackendArg { /// Built-in ADB implementation (no external dependencies) + #[cfg(feature = "builtin-adb")] Builtin, /// Use system-installed adb binary System, @@ -34,6 +35,7 @@ pub enum AdbBackendArg { impl From for AdbBackend { fn from(arg: AdbBackendArg) -> Self { match arg { + #[cfg(feature = "builtin-adb")] AdbBackendArg::Builtin => AdbBackend::Builtin, AdbBackendArg::System => AdbBackend::System, } @@ -46,7 +48,7 @@ impl From for AdbBackend { #[command(version)] #[command(propagate_version = true)] pub struct Cli { - /// ADB backend to use: system (default, uses adb binary) or builtin (no dependencies) + /// ADB backend to use: system (default, uses adb binary) or builtin if enabled #[arg( short = 'B', long = "backend", diff --git a/crates/uad-core/Cargo.toml b/crates/uad-core/Cargo.toml index 16e1dbd..4bd07fd 100644 --- a/crates/uad-core/Cargo.toml +++ b/crates/uad-core/Cargo.toml @@ -11,11 +11,12 @@ categories = ["command-line-utilities"] edition.workspace = true [features] -default = [] +default = ["builtin-adb"] +builtin-adb = ["dep:adb_client"] self-update = ["dep:flate2", "dep:tar"] [dependencies] -adb_client.workspace = true +adb_client = { workspace = true, optional = true } serde.workspace = true serde_json.workspace = true log.workspace = true diff --git a/crates/uad-core/src/adb.rs b/crates/uad-core/src/adb.rs index b6f2b3f..a03ef94 100644 --- a/crates/uad-core/src/adb.rs +++ b/crates/uad-core/src/adb.rs @@ -4,7 +4,7 @@ //! //! Following the design philosophy of most of Rust `std`, //! `*Command` are intended to be "thin wrappers" (low-overhead abstractions) -//! around `adb_client` or the system ADB CLI, +//! around the optional `adb_client` backend or the system ADB CLI, //! which implies: //! - no "magic" //! - no custom commands @@ -46,9 +46,11 @@ //! For comprehensive info about ADB, //! [see this](https://android.googlesource.com/platform/packages/modules/adb/+/refs/heads/master/docs/) +#[cfg(feature = "builtin-adb")] use adb_client::{ADBDeviceExt, server::ADBServer}; use serde::{Deserialize, Serialize}; use std::fmt::Write as _; +#[cfg(feature = "builtin-adb")] use std::io::Cursor; use std::rc::Rc; @@ -73,6 +75,7 @@ pub fn to_trimmed_utf8(v: &[u8]) -> String { pub enum AdbBackend { /// Built-in ADB implementation via `adb_client` crate. /// The application can communicate with devices without needing `adb` installed. + #[cfg(feature = "builtin-adb")] Builtin, /// Uses the system-installed `adb` binary. /// This is the default to preserve existing behavior. @@ -84,12 +87,18 @@ pub enum AdbBackend { impl AdbBackend { /// Returns all available backend variants for UI enumeration + #[cfg(feature = "builtin-adb")] pub const ALL: [Self; 2] = [Self::Builtin, Self::System]; + + /// Returns all available backend variants for UI enumeration + #[cfg(not(feature = "builtin-adb"))] + pub const ALL: [Self; 1] = [Self::System]; } impl std::fmt::Display for AdbBackend { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { + #[cfg(feature = "builtin-adb")] Self::Builtin => write!(f, "Builtin"), Self::System => write!(f, "System (adb)"), } @@ -189,6 +198,7 @@ impl ACommand { /// - "device" pub fn devices(self) -> Result, String> { match self.0.backend { + #[cfg(feature = "builtin-adb")] AdbBackend::Builtin => Self::devices_builtin(), AdbBackend::System => Self::devices_system(), } @@ -212,6 +222,7 @@ impl ACommand { /// ``` pub fn version(self) -> Result { match self.0.backend { + #[cfg(feature = "builtin-adb")] AdbBackend::Builtin => Self::version_builtin(), AdbBackend::System => Self::version_system(), } @@ -220,6 +231,7 @@ impl ACommand { // ========== Builtin backend implementation (adb_client) ========== /// Get ADB server version using the builtin `adb_client` + #[cfg(feature = "builtin-adb")] fn version_builtin() -> Result { let mut server = ADBServer::default(); match server.version() { @@ -232,6 +244,7 @@ impl ACommand { } /// List devices using the builtin `adb_client` + #[cfg(feature = "builtin-adb")] fn devices_builtin() -> Result, String> { let mut server = ADBServer::default(); server @@ -249,6 +262,7 @@ impl ACommand { } /// Execute a shell command via `adb_client` (builtin backend) + #[cfg(feature = "builtin-adb")] fn run_shell_command_builtin(&self, shell_command: &str) -> Result { let mut server = ADBServer::default(); @@ -368,6 +382,7 @@ impl ACommand { /// Execute a shell command using the configured backend fn run_shell_command(&self, shell_command: &str) -> Result { match self.0.backend { + #[cfg(feature = "builtin-adb")] AdbBackend::Builtin => self.run_shell_command_builtin(shell_command), AdbBackend::System => self.run_shell_command_system(shell_command), } @@ -610,6 +625,7 @@ mod tests { #[test] fn backend_display() { + #[cfg(feature = "builtin-adb")] assert_eq!(AdbBackend::Builtin.to_string(), "Builtin"); assert_eq!(AdbBackend::System.to_string(), "System (adb)"); } diff --git a/crates/uad-gui/Cargo.toml b/crates/uad-gui/Cargo.toml index 4a50636..0ef7f42 100644 --- a/crates/uad-gui/Cargo.toml +++ b/crates/uad-gui/Cargo.toml @@ -15,14 +15,15 @@ name = "uad-ng" path = "src/main.rs" [features] -default = ["wgpu", "self-update", "img"] -wgpu = [] # Iced/wgpu is default -self-update = ["flate2", "tar"] +default = ["wgpu", "self-update", "img", "builtin-adb"] +builtin-adb = ["uad-core/builtin-adb"] +self-update = ["flate2", "tar", "uad-core/self-update"] no-self-update = [] +wgpu = [] # Iced/wgpu is default img = ["image", "iced/image"] [dependencies] -uad-core = { path = "../uad-core", features = ["self-update"] } +uad-core = { path = "../uad-core", default-features = false } iced.workspace = true image = { workspace = true, optional = true } rfd.workspace = true diff --git a/crates/uad-gui/src/views/settings.rs b/crates/uad-gui/src/views/settings.rs index 8a850a5..5760c04 100644 --- a/crates/uad-gui/src/views/settings.rs +++ b/crates/uad-gui/src/views/settings.rs @@ -519,10 +519,11 @@ impl Settings { .spacing(10) .align_y(Alignment::Center); - let backend_descr = text( - "Builtin: Uses embedded ADB (no external dependencies). System: Uses your installed adb binary.", - ) - .style(style::Text::Commentary); + #[cfg(feature = "builtin-adb")] + let backend_description = "System: Uses your installed adb binary. Builtin: Uses embedded ADB (no external dependencies)."; + #[cfg(not(feature = "builtin-adb"))] + let backend_description = "System: Uses your installed adb binary."; + let backend_descr = text(backend_description).style(style::Text::Commentary); let version_row = row![ text("Version: ").size(14), From 93c83863a6b4861c29bad0afc80879387d029c5f Mon Sep 17 00:00:00 2001 From: Wakelock <263392226+Wakelock@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:28:22 +0000 Subject: [PATCH 03/16] Update uad_lists.json --- resources/assets/uad_lists.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/resources/assets/uad_lists.json b/resources/assets/uad_lists.json index 76744c5..d0b66aa 100644 --- a/resources/assets/uad_lists.json +++ b/resources/assets/uad_lists.json @@ -41220,7 +41220,7 @@ }, "com.blackview.filetrans": { "list": "Oem", - "description": "Moving Assistant/Data Migration Assistant\nApp used to transfer data from one device to another.\nWARNING: Uninstalling it breaks the App info Settings page on Android 15, Blackview Tab 60 WiFi.", + "description": "Moving Assistant/Data Migration Assistant\nApp used to transfer data from one device to another.\nWARNING: Uninstalling it breaks the App info Settings page on Android 15, Blackview Tab 60 WiFi.\nTry disabling instead of uninstalling in case that happens.", "dependencies": [], "neededBy": [], "labels": [], @@ -43131,5 +43131,13 @@ "neededBy": [], "labels": [], "removal": "Expert" + }, + "com.blackview.systemmanager": { + "list": "Oem", + "description": "'System Manager'\nA pre-installed 'task killer', which ironically slows down the system. It also has internet and install apps permisions by default, making it a potential backdoor.\nAlthough 'systemmanager' might sound scary, disabling it will significantly reduce scroll stuttering. Try disabling instead of uninstalling in case that breaks the App info Settings page.", + "dependencies": [], + "neededBy": [], + "labels": [], + "removal": "Advanced" } } From 3bc538292db032c2c34380255ac8401254416aff Mon Sep 17 00:00:00 2001 From: Wakelock <263392226+Wakelock@users.noreply.github.com> Date: Thu, 16 Jul 2026 07:47:04 +0000 Subject: [PATCH 04/16] Update uad_lists.json --- resources/assets/uad_lists.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/assets/uad_lists.json b/resources/assets/uad_lists.json index d0b66aa..4368a13 100644 --- a/resources/assets/uad_lists.json +++ b/resources/assets/uad_lists.json @@ -43134,7 +43134,7 @@ }, "com.blackview.systemmanager": { "list": "Oem", - "description": "'System Manager'\nA pre-installed 'task killer', which ironically slows down the system. It also has internet and install apps permisions by default, making it a potential backdoor.\nAlthough 'systemmanager' might sound scary, disabling it will significantly reduce scroll stuttering. Try disabling instead of uninstalling in case that breaks the App info Settings page.", + "description": "'System Manager'\nA pre-installed 'task killer', which ironically slows down the system. It also has internet and install apps permisions by default, making it a potential backdoor.\nAlthough 'systemmanager' might sound important, disabling it will significantly reduce scroll stuttering. Try disabling instead of uninstalling in case that breaks the App info Settings page.", "dependencies": [], "neededBy": [], "labels": [], From 85c0d67776493a8ab9999a72f39ed1bb9e8466d8 Mon Sep 17 00:00:00 2001 From: Wakelock <263392226+Wakelock@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:04:11 +0000 Subject: [PATCH 05/16] Update uad_lists.json --- resources/assets/uad_lists.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/resources/assets/uad_lists.json b/resources/assets/uad_lists.json index 4368a13..8320cc0 100644 --- a/resources/assets/uad_lists.json +++ b/resources/assets/uad_lists.json @@ -43139,5 +43139,13 @@ "neededBy": [], "labels": [], "removal": "Advanced" + }, + "com.blackview.apkupgrade": { + "list": "Oem", + "description": "System Message\nSends unwanted promotional notifications.\nhttps://www.reddit.com/r/blackview/comments/1mo441x/get_rid_of_system_notification_that_contains_ads/", + "dependencies": [], + "neededBy": [], + "labels": [], + "removal": "Recommended" } } From 4ec90fa807c950222d49948b69164079550e4837 Mon Sep 17 00:00:00 2001 From: Wakelock <263392226+Wakelock@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:07:28 +0000 Subject: [PATCH 06/16] Update uad_lists.json --- resources/assets/uad_lists.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/assets/uad_lists.json b/resources/assets/uad_lists.json index 8320cc0..1ca7540 100644 --- a/resources/assets/uad_lists.json +++ b/resources/assets/uad_lists.json @@ -43142,7 +43142,7 @@ }, "com.blackview.apkupgrade": { "list": "Oem", - "description": "System Message\nSends unwanted promotional notifications.\nhttps://www.reddit.com/r/blackview/comments/1mo441x/get_rid_of_system_notification_that_contains_ads/", + "description": "System Message\nAdware, sends unwanted promotional notifications.\nhttps://www.reddit.com/r/blackview/comments/1mo441x/get_rid_of_system_notification_that_contains_ads/", "dependencies": [], "neededBy": [], "labels": [], From 43d323332f4c7a074fffd3cdb31a7ce20f464fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Klein?= <44199273+burninc0de@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:35:15 +0300 Subject: [PATCH 07/16] 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. --- crates/uad-core/src/adb.rs | 12 ++++++++---- crates/uad-core/src/sync.rs | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/uad-core/src/adb.rs b/crates/uad-core/src/adb.rs index e7d223c..f2335e0 100644 --- a/crates/uad-core/src/adb.rs +++ b/crates/uad-core/src/adb.rs @@ -44,7 +44,7 @@ use std::rc::Rc; use std::os::windows::process::CommandExt; use crate::utils::is_all_w_c; -use log::{error, info}; +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. @@ -366,11 +366,15 @@ impl PmCommand { self.0.0.run().map(|pack_ls| { pack_ls .lines() - .map(|p_ln| { + .filter_map(|p_ln| { debug_assert!(p_ln.starts_with(PACK_PREFIX)); let p = &p_ln[PACK_PREFIX.len()..]; - debug_assert!(PackageId::new(p).is_some()); - String::from(p) + if PackageId::new(p).is_some() { + Some(String::from(p)) + } else { + warn!("skipping nonstandard package name: {p:?}"); + None + } }) .collect() }) diff --git a/crates/uad-core/src/sync.rs b/crates/uad-core/src/sync.rs index 9bdd7cc..b4b912a 100644 --- a/crates/uad-core/src/sync.rs +++ b/crates/uad-core/src/sync.rs @@ -213,7 +213,7 @@ pub fn request_builder(commands: &[&str], package: &str, user: Option) -> // 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_some() { + if PackageId::new(package).is_none() { error!("request_builder: refusing invalid package name: {package:?}"); return Vec::new(); } From 067f2251e99bd0a023522803c8d13fa2316f8ef1 Mon Sep 17 00:00:00 2001 From: Wakelock <263392226+Wakelock@users.noreply.github.com> Date: Sat, 18 Jul 2026 09:43:54 +0000 Subject: [PATCH 08/16] Update uad_lists.json --- resources/assets/uad_lists.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/resources/assets/uad_lists.json b/resources/assets/uad_lists.json index 1ca7540..db8943f 100644 --- a/resources/assets/uad_lists.json +++ b/resources/assets/uad_lists.json @@ -43147,5 +43147,13 @@ "neededBy": [], "labels": [], "removal": "Recommended" + }, + "com.huawei.localBackup": { + "list": "Oem", + "description": "Backup\nA hidden app, which has storage, location, contacts, sms, call logs and calendar permissions. Can be uninstalled via Settings.", + "dependencies": [], + "neededBy": [], + "labels": [], + "removal": "Recommended" } } From 31234bed3ccc44e356cdb07b86e76013de6f284c Mon Sep 17 00:00:00 2001 From: Wakelock <263392226+Wakelock@users.noreply.github.com> Date: Sat, 18 Jul 2026 09:50:33 +0000 Subject: [PATCH 09/16] Update uad_lists.json --- resources/assets/uad_lists.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/resources/assets/uad_lists.json b/resources/assets/uad_lists.json index db8943f..9d1c5c6 100644 --- a/resources/assets/uad_lists.json +++ b/resources/assets/uad_lists.json @@ -43150,7 +43150,15 @@ }, "com.huawei.localBackup": { "list": "Oem", - "description": "Backup\nA hidden app, which has storage, location, contacts, sms, call logs and calendar permissions. Can be uninstalled via Settings.", + "description": "Backup\nA hidden app, which has storage, location, contacts, SMS, call logs and calendar permissions. Can be uninstalled via Settings.", + "dependencies": [], + "neededBy": [], + "labels": [], + "removal": "Recommended" + }, + "com.huawei.aml": { + "list": "Oem", + "description": "Huawei AML\nA hidden app, which has phone, location and SMS permissions.", "dependencies": [], "neededBy": [], "labels": [], From c38c1cd2d04d661b178fcf19112095560f71d697 Mon Sep 17 00:00:00 2001 From: Wakelock <263392226+Wakelock@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:05:39 +0000 Subject: [PATCH 10/16] Update uad_lists.json --- resources/assets/uad_lists.json | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/resources/assets/uad_lists.json b/resources/assets/uad_lists.json index 9d1c5c6..7ef314e 100644 --- a/resources/assets/uad_lists.json +++ b/resources/assets/uad_lists.json @@ -43142,11 +43142,11 @@ }, "com.blackview.apkupgrade": { "list": "Oem", - "description": "System Message\nAdware, sends unwanted promotional notifications.\nhttps://www.reddit.com/r/blackview/comments/1mo441x/get_rid_of_system_notification_that_contains_ads/", + "description": "System Message\nAdware, sends unwanted promotional notifications.\nTry disabling instead of uninstalling in case that breaks the App info Settings page.\nhttps://www.reddit.com/r/blackview/comments/1mo441x/get_rid_of_system_notification_that_contains_ads/", "dependencies": [], "neededBy": [], "labels": [], - "removal": "Recommended" + "removal": "Advanced" }, "com.huawei.localBackup": { "list": "Oem", @@ -43163,5 +43163,13 @@ "neededBy": [], "labels": [], "removal": "Recommended" + }, + "com.blackview.helper": { + "list": "Oem", + "description": "DKHelper\nTry disabling instead of uninstalling in case that breaks the App info Settings page.", + "dependencies": [], + "neededBy": [], + "labels": [], + "removal": "Advanced" } } From 3f55e6cdba096a5d089c6956552dd8416c6f5ae9 Mon Sep 17 00:00:00 2001 From: Wakelock <263392226+Wakelock@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:25:28 +0000 Subject: [PATCH 11/16] Update uad_lists.json --- resources/assets/uad_lists.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/resources/assets/uad_lists.json b/resources/assets/uad_lists.json index 7ef314e..8caff7f 100644 --- a/resources/assets/uad_lists.json +++ b/resources/assets/uad_lists.json @@ -43171,5 +43171,13 @@ "neededBy": [], "labels": [], "removal": "Advanced" + }, + "com.softwinner.dragonatt": { + "list": "Oem", + "description": "DragonAtt", + "dependencies": [], + "neededBy": [], + "labels": [], + "removal": "Recommended" } } From 856606c58b5872106516c465ddff6639e2f7258b Mon Sep 17 00:00:00 2001 From: TechyDodoDevMan <156003199+TechyDodoDevMan@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:05:16 -0400 Subject: [PATCH 12/16] Update uad_lists.json Added Fitbit Connected Fitness --- resources/assets/uad_lists.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/resources/assets/uad_lists.json b/resources/assets/uad_lists.json index 8caff7f..8ffc450 100644 --- a/resources/assets/uad_lists.json +++ b/resources/assets/uad_lists.json @@ -23,6 +23,14 @@ "labels": [], "removal": "Unsafe" }, + "com.google.android.wearable.pixel.aspen": { + "list": "Oem", + "description": "Fitbit's Connected Fitness feature for the Pixel Watch. Can be safely removed if you don't need/use it.", + "dependencies": [], + "neededBy": [], + "labels": [], + "removal": "Recommended" + }, "org.lineageos.recorder": { "list": "Oem", "description": "LineageOS Voice Recorder App.\nSafe to remove if you don't need it or have replaced it with another app.", From 266f1b9597f99e266ad8660bdd5886c49f7911c4 Mon Sep 17 00:00:00 2001 From: insanebecauseimbestwheniminlove Date: Sat, 25 Jul 2026 03:53:55 +0200 Subject: [PATCH 13/16] Update uad_lists.json --- resources/assets/uad_lists.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/resources/assets/uad_lists.json b/resources/assets/uad_lists.json index 8caff7f..d232403 100644 --- a/resources/assets/uad_lists.json +++ b/resources/assets/uad_lists.json @@ -367,6 +367,14 @@ "labels": [], "removal": "Advanced" }, + "com.xiaomi.aiservice": { + "list": "Oem", + "description": "Xiaomi on-device AI inference engine (HyperOS/MIUI). Runs many ML models (OCR, translation, etc.) in isolated per-model processes. \nEmbeds Xiaomi's OneTrack SDK, which collects OAID, android_id, device fingerprint, region, network type, and per-event usage data, then uploads it encrypted to tracking.*.miui.com. \nAlso embeds XCrash for crash reporting. Downloads AI model updates silently via Android DownloadManager (probably has to do with AI Core). \nRemoving probably breaks HyperOS AI features (I haven't tested) but does not affect core phone, SMS, or data functions.", + "dependencies": [], + "neededBy": [], + "labels": [], + "removal": "Advanced" + }, "com.xiaomi.phone.overlay": { "list": "Oem", "description": "An overlay needed for com.xiaomi.phone. Safe to remove if com.xiaomi.phone has been removed.", From 3c4011807ba29ca2ec1c38987eef0084ec10429e Mon Sep 17 00:00:00 2001 From: TechyDodoDevMan <156003199+TechyDodoDevMan@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:12:33 -0400 Subject: [PATCH 14/16] Change removal recommendation from 'Recommended' to 'Advanced' Resolved suggestion --- resources/assets/uad_lists.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/assets/uad_lists.json b/resources/assets/uad_lists.json index 75635c4..6b8ae92 100644 --- a/resources/assets/uad_lists.json +++ b/resources/assets/uad_lists.json @@ -29,7 +29,7 @@ "dependencies": [], "neededBy": [], "labels": [], - "removal": "Recommended" + "removal": "Advanced" }, "org.lineageos.recorder": { "list": "Oem", From f3df24beb987d9977ae5422d1cfd09b9fa60f4a1 Mon Sep 17 00:00:00 2001 From: Jarek <42780718+jarekt@users.noreply.github.com> Date: Wed, 5 Aug 2026 14:57:22 +0200 Subject: [PATCH 15/16] change com.android.networkstack.tethering.inprocess to unsafe --- resources/assets/uad_lists.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/assets/uad_lists.json b/resources/assets/uad_lists.json index 6b8ae92..ae97e4e 100644 --- a/resources/assets/uad_lists.json +++ b/resources/assets/uad_lists.json @@ -42228,11 +42228,11 @@ }, "com.android.networkstack.tethering.inprocess": { "list": "Oem", - "description": "Completely empty package. Useless.", + "description": "Causes a lockscreen loop on some Xiaomi devices. Provides TetherableWifiRegexs, the lack of which crashes systemui and miui.home.", "dependencies": [], "neededBy": [], "labels": [], - "removal": "Recommended" + "removal": "Unsafe" }, "com.samsung.android.a20.d01.wallpapermulti": { "list": "Oem", From 67f880b5078b40e38c17370d153077335aff4611 Mon Sep 17 00:00:00 2001 From: Rudxain <76864299+Rudxain@users.noreply.github.com> Date: Fri, 7 Aug 2026 18:06:51 -0400 Subject: [PATCH 16/16] 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" --- crates/uad-core/src/config.rs | 2 +- crates/uad-core/src/sync.rs | 27 ++++++++++++++++----------- crates/uad-gui/src/views/settings.rs | 13 +++++++------ 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/crates/uad-core/src/config.rs b/crates/uad-core/src/config.rs index 5522b16..5c41d78 100644 --- a/crates/uad-core/src/config.rs +++ b/crates/uad-core/src/config.rs @@ -37,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, diff --git a/crates/uad-core/src/sync.rs b/crates/uad-core/src/sync.rs index b4b912a..f90b6ee 100644 --- a/crates/uad-core/src/sync.rs +++ b/crates/uad-core/src/sync.rs @@ -180,16 +180,19 @@ pub fn apply_pkg_state_commands( _ => vec![], }, PackageState::Disabled => match package.state { - PackageState::Uninstalled | PackageState::Enabled => match phone.android_sdk { - sdk if sdk >= 23 => vec!["pm disable-user", "am force-stop", PM_CLEAR_PACK], - _ => vec![], - }, + PackageState::Uninstalled | PackageState::Enabled => { + if supports_disabling(phone) { + vec!["pm disable-user", "am force-stop", PM_CLEAR_PACK] + } else { + vec![] + } + } _ => vec![], }, PackageState::Uninstalled => match package.state { PackageState::Enabled | PackageState::Disabled => match phone.android_sdk { - sdk if sdk >= 23 => vec!["pm uninstall"], // > Android Marshmallow (6.0) - 21 | 22 => vec!["pm hide", PM_CLEAR_PACK], // Android Lollipop (5.x) + sdk if sdk >= 23 => vec!["pm uninstall"], // > Marshmallow (6.0) + 21 | 22 => vec!["pm hide", PM_CLEAR_PACK], // Lollipop (5.x) _ => vec!["pm block", PM_CLEAR_PACK], // Disable mode is unavailable on older devices because the specific ADB commands need root }, _ => vec![], @@ -401,10 +404,11 @@ pub fn detect_cross_user_behavior( } } -/// Minimum inclusive Android SDK version -/// that supports multi-user mode. -/// Lollipop 5.0 -pub const MULTI_USER_SDK: u8 = 21; +#[must_use] +pub const fn supports_disabling(dev: &Phone) -> bool { + // >= Marshmallow (6.0) + dev.android_sdk >= 23 +} /// Check if it might support multi-user mode, /// by simply comparing SDK version. @@ -415,7 +419,8 @@ pub const MULTI_USER_SDK: u8 = 21; /// - #[must_use] pub const fn supports_multi_user(dev: &Phone) -> bool { - dev.android_sdk >= MULTI_USER_SDK + // >= Lollipop (5.0) + dev.android_sdk >= 21 } /// Check if a `user_id` is protected on a device by trying diff --git a/crates/uad-gui/src/views/settings.rs b/crates/uad-gui/src/views/settings.rs index c19b999..3a09b80 100644 --- a/crates/uad-gui/src/views/settings.rs +++ b/crates/uad-gui/src/views/settings.rs @@ -19,7 +19,7 @@ use uad_core::{ save::{backup_phone, list_available_backup_user, list_available_backups, restore_backup}, sync::{ AdbError, CorePackage, Phone, User, get_android_sdk, run_adb_shell_action, - supports_multi_user, + supports_disabling, supports_multi_user, }, utils::{DisplayablePath, Error, NAME, export_packages, generate_backup_name, open_url}, }; @@ -113,7 +113,7 @@ impl Settings { } fn handle_disable_mode(&mut self, phone: &Phone, toggled: bool) -> Task { - if phone.android_sdk >= 23 { + if supports_disabling(phone) { self.device.disable_mode = toggled; debug!("Config change: {self:?}"); let mut config = Config::load_configuration_file(); @@ -170,8 +170,9 @@ impl Settings { None => { self.device = DeviceSettings { device_id: phone.adb_id.clone(), + // see FAQ, and GH-issue #1426 + disable_mode: supports_disabling(phone), multi_user_mode: supports_multi_user(phone), - disable_mode: false, backup, }; } @@ -498,14 +499,14 @@ impl Settings { .size(20) .style(style::CheckBox::SettingsEnabled); - let disable_checkbox_style = if phone.android_sdk >= 23 { + let disable_checkbox_style = if supports_disabling(phone) { style::CheckBox::SettingsEnabled } else { style::CheckBox::SettingsDisabled }; let disable_mode_descr = - text("In some cases, it can be better to disable a package instead of uninstalling it") + text("In many cases, it's better to disable a package instead of uninstalling it") .style(style::Text::Commentary); let unavailable_btn = button(text("Unavailable").size(14)) @@ -522,7 +523,7 @@ impl Settings { .size(20) .style(disable_checkbox_style); - let disable_setting_row = if phone.android_sdk >= 23 { + let disable_setting_row = if supports_disabling(phone) { row![ disable_mode_checkbox, Space::new().width(Length::Fill).height(Length::Shrink),