Compare commits

...

3 commits

Author SHA1 Message Date
Angel J
16a0217b7f
feat(adb): use direct USB for built-in backend
Replace the server-based backend with embedded USB communication and share
backend selection across the CLI and GUI.

Serialize USB operations, cache connections, handle device replugging, and
report authorization, busy, and permission errors.

Create keys atomically and validate existing keys. Convert PKCS#1 keys to
temporary PKCS#8 files without modifying the originals. Propagate remote
shell failures and add coverage for discovery, locking, and recovery.
2026-08-13 07:52:32 -07:00
Angel J
08578514db
feat(adb): gate built-in backend behind default feature 2026-08-12 21:43:21 -07:00
iamanaws
34df8d2296
feat(adb): add built-in Rust ADB backend 2026-08-12 21:43:06 -07:00
20 changed files with 2603 additions and 359 deletions

View file

@ -42,7 +42,7 @@ jobs:
restore-keys: ${{ runner.OS }}-release- restore-keys: ${{ runner.OS }}-release-
if: matrix.os == 'ubuntu-22.04' if: matrix.os == 'ubuntu-22.04'
- name: Building - name: Building
run: cargo build --release -p uad-gui --no-default-features --features wgpu,${{ matrix.update_feature }},img run: cargo build --release -p uad-gui --no-default-features --features wgpu,${{ matrix.update_feature }},img,builtin-adb
- name: Renaming binaries [Windows] - name: Renaming binaries [Windows]
if: matrix.os == 'windows-2022' if: matrix.os == 'windows-2022'
run: mv target/release/uad-ng.exe uad-ng-${{ matrix.build_target }}.exe run: mv target/release/uad-ng.exe uad-ng-${{ matrix.build_target }}.exe

569
Cargo.lock generated
View file

@ -18,6 +18,31 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
[[package]]
name = "adb_client"
version = "3.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773b25a440e72829bbb1b96435ee0e65ef0f14a3da545590930ca881a68e59d7"
dependencies = [
"base64",
"byteorder",
"chrono",
"log",
"num-bigint-dig",
"num-traits",
"num_enum",
"quick-protobuf",
"rand 0.10.1",
"rcgen",
"regex",
"rsa",
"rusb",
"rustls",
"rustls-pki-types",
"sha1",
"thiserror 2.0.18",
]
[[package]] [[package]]
name = "adler2" name = "adler2"
version = "2.0.1" version = "2.0.1"
@ -37,6 +62,15 @@ dependencies = [
"zerocopy", "zerocopy",
] ]
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "aligned" name = "aligned"
version = "0.4.3" version = "0.4.3"
@ -227,6 +261,45 @@ dependencies = [
"zbus", "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]] [[package]]
name = "async-broadcast" name = "async-broadcast"
version = "0.7.2" version = "0.7.2"
@ -473,7 +546,7 @@ dependencies = [
"anyhow", "anyhow",
"arrayvec", "arrayvec",
"log", "log",
"nom", "nom 8.0.0",
"num-rational", "num-rational",
"v_frame", "v_frame",
] ]
@ -493,13 +566,19 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.8.0" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [ dependencies = [
"bit-vec", "bit-vec 0.8.0",
] ]
[[package]] [[package]]
@ -508,6 +587,15 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" 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]] [[package]]
name = "bit_field" name = "bit_field"
version = "0.10.3" version = "0.10.3"
@ -541,6 +629,15 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" 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]] [[package]]
name = "block2" name = "block2"
version = "0.5.1" version = "0.5.1"
@ -604,6 +701,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "byteorder-lite" name = "byteorder-lite"
version = "0.1.0" version = "0.1.0"
@ -704,10 +807,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "chrono" name = "chacha20"
version = "0.4.43" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"iana-time-zone", "iana-time-zone",
"num-traits", "num-traits",
@ -854,6 +968,12 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]] [[package]]
name = "cookie" name = "cookie"
version = "0.18.1" version = "0.18.1"
@ -986,6 +1106,24 @@ dependencies = [
"unicode-segmentation", "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]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.0" version = "1.5.0"
@ -1039,6 +1177,16 @@ dependencies = [
"wgpu", "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]] [[package]]
name = "csv" name = "csv"
version = "1.4.0" version = "1.4.0"
@ -1086,6 +1234,37 @@ dependencies = [
"winreg 0.52.0", "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]] [[package]]
name = "deranged" name = "deranged"
version = "0.5.6" version = "0.5.6"
@ -1095,6 +1274,17 @@ dependencies = [
"powerfmt", "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]] [[package]]
name = "dirs" name = "dirs"
version = "6.0.0" version = "6.0.0"
@ -1597,6 +1787,16 @@ dependencies = [
"slab", "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]] [[package]]
name = "gethostname" name = "gethostname"
version = "1.1.0" version = "1.1.0"
@ -1639,6 +1839,7 @@ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"r-efi", "r-efi",
"rand_core 0.10.1",
"wasip2", "wasip2",
"wasip3", "wasip3",
] ]
@ -2177,9 +2378,9 @@ dependencies = [
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.9" version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"byteorder-lite", "byteorder-lite",
@ -2195,8 +2396,8 @@ dependencies = [
"rayon", "rayon",
"rgb", "rgb",
"tiff", "tiff",
"zune-core 0.5.1", "zune-core",
"zune-jpeg 0.5.12", "zune-jpeg",
] ]
[[package]] [[package]]
@ -2351,6 +2552,9 @@ name = "lazy_static"
version = "1.5.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin",
]
[[package]] [[package]]
name = "leb128fmt" name = "leb128fmt"
@ -2407,6 +2611,18 @@ dependencies = [
"redox_syscall 0.7.1", "redox_syscall 0.7.1",
] ]
[[package]]
name = "libusb1-sys"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da050ade7ac4ff1ba5379af847a10a10a8e284181e060105bf8d86960ce9ce0f"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "lilt" name = "lilt"
version = "0.8.1" version = "0.8.1"
@ -2457,9 +2673,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.29" version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
dependencies = [ dependencies = [
"value-bag", "value-bag",
] ]
@ -2537,6 +2753,12 @@ dependencies = [
"paste", "paste",
] ]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.9" version = "0.8.9"
@ -2560,9 +2782,9 @@ dependencies = [
[[package]] [[package]]
name = "moxcms" name = "moxcms"
version = "0.7.11" version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [ dependencies = [
"num-traits", "num-traits",
"pxfm", "pxfm",
@ -2682,6 +2904,16 @@ dependencies = [
"libc", "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]] [[package]]
name = "nom" name = "nom"
version = "8.0.0" version = "8.0.0"
@ -2707,6 +2939,23 @@ dependencies = [
"num-traits", "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]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.2.0" version = "0.2.0"
@ -2733,6 +2982,17 @@ dependencies = [
"num-traits", "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]] [[package]]
name = "num-rational" name = "num-rational"
version = "0.4.2" version = "0.4.2"
@ -2766,9 +3026,9 @@ dependencies = [
[[package]] [[package]]
name = "num_enum" name = "num_enum"
version = "0.7.5" version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
dependencies = [ dependencies = [
"num_enum_derive", "num_enum_derive",
"rustversion", "rustversion",
@ -2776,9 +3036,9 @@ dependencies = [
[[package]] [[package]]
name = "num_enum_derive" name = "num_enum_derive"
version = "0.7.5" version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
dependencies = [ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
@ -3145,6 +3405,15 @@ dependencies = [
"objc2-foundation 0.2.2", "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]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.3"
@ -3242,6 +3511,25 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" 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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@ -3291,6 +3579,27 @@ dependencies = [
"futures-io", "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]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.32" version = "0.3.32"
@ -3446,6 +3755,15 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" 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]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.38.4" version = "0.38.4"
@ -3501,6 +3819,17 @@ dependencies = [
"rand_core 0.9.5", "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]] [[package]]
name = "rand_chacha" name = "rand_chacha"
version = "0.3.1" version = "0.3.1"
@ -3539,6 +3868,12 @@ dependencies = [
"getrandom 0.3.4", "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]] [[package]]
name = "range-alloc" name = "range-alloc"
version = "0.1.4" version = "0.1.4"
@ -3588,9 +3923,9 @@ dependencies = [
[[package]] [[package]]
name = "ravif" name = "ravif"
version = "0.12.0" version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
dependencies = [ dependencies = [
"avif-serialize", "avif-serialize",
"imgref", "imgref",
@ -3627,6 +3962,20 @@ dependencies = [
"crossbeam-utils", "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]] [[package]]
name = "read-fonts" name = "read-fonts"
version = "0.35.0" version = "0.35.0"
@ -3676,6 +4025,35 @@ dependencies = [
"thiserror 2.0.18", "thiserror 2.0.18",
] ]
[[package]]
name = "regex"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]] [[package]]
name = "renderdoc-sys" name = "renderdoc-sys"
version = "1.1.0" version = "1.1.0"
@ -3744,6 +4122,36 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" 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 = "rusb"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab9f9ff05b63a786553a4c02943b74b34a988448671001e9a27e2f0565cc05a4"
dependencies = [
"libc",
"libusb1-sys",
]
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "1.1.0" version = "1.1.0"
@ -3765,6 +4173,15 @@ dependencies = [
"semver", "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]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.44" version = "0.38.44"
@ -3793,9 +4210,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.36" version = "0.23.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
dependencies = [ dependencies = [
"log", "log",
"once_cell", "once_cell",
@ -3808,9 +4225,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.14.0" version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
dependencies = [ dependencies = [
"zeroize", "zeroize",
] ]
@ -3969,6 +4386,17 @@ dependencies = [
"serde_core", "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]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "1.3.0"
@ -3985,6 +4413,16 @@ dependencies = [
"libc", "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]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.8" version = "0.3.8"
@ -4144,6 +4582,12 @@ dependencies = [
"x11rb", "x11rb",
] ]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
[[package]] [[package]]
name = "spirv" name = "spirv"
version = "0.3.0+sdk-1.3.268.0" version = "0.3.0+sdk-1.3.268.0"
@ -4153,6 +4597,16 @@ dependencies = [
"bitflags 2.10.0", "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]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.1" version = "1.2.1"
@ -4306,16 +4760,16 @@ dependencies = [
[[package]] [[package]]
name = "tiff" name = "tiff"
version = "0.10.3" version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
dependencies = [ dependencies = [
"fax", "fax",
"flate2", "flate2",
"half", "half",
"quick-error", "quick-error",
"weezl", "weezl",
"zune-jpeg 0.4.21", "zune-jpeg",
] ]
[[package]] [[package]]
@ -4532,6 +4986,12 @@ dependencies = [
"core_maths", "core_maths",
] ]
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]] [[package]]
name = "uad-cli" name = "uad-cli"
version = "1.2.0" version = "1.2.0"
@ -4556,15 +5016,19 @@ dependencies = [
name = "uad-core" name = "uad-core"
version = "1.2.0" version = "1.2.0"
dependencies = [ dependencies = [
"adb_client",
"chrono", "chrono",
"csv", "csv",
"dirs", "dirs",
"flate2", "flate2",
"log", "log",
"retry", "retry",
"rsa",
"rusb",
"serde", "serde",
"serde_json", "serde_json",
"tar", "tar",
"tempfile",
"toml", "toml",
"ureq", "ureq",
"win32console", "win32console",
@ -4745,6 +5209,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"
@ -5119,7 +5589,7 @@ checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"bit-set", "bit-set",
"bit-vec", "bit-vec 0.8.0",
"bitflags 2.10.0", "bitflags 2.10.0",
"bytemuck", "bytemuck",
"cfg_aliases 0.2.1", "cfg_aliases 0.2.1",
@ -5891,6 +6361,24 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" 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]] [[package]]
name = "xattr" name = "xattr"
version = "1.6.1" version = "1.6.1"
@ -5938,6 +6426,16 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" 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]] [[package]]
name = "yazi" name = "yazi"
version = "0.2.1" version = "0.2.1"
@ -6120,12 +6618,6 @@ version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zune-core"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]] [[package]]
name = "zune-core" name = "zune-core"
version = "0.5.1" version = "0.5.1"
@ -6141,22 +6633,13 @@ dependencies = [
"simd-adler32", "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]] [[package]]
name = "zune-jpeg" name = "zune-jpeg"
version = "0.5.12" version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe"
dependencies = [ dependencies = [
"zune-core 0.5.1", "zune-core",
] ]
[[package]] [[package]]

View file

@ -19,6 +19,10 @@ edition = "2024"
[workspace.dependencies] [workspace.dependencies]
# Core dependencies # Core dependencies
adb_client = { version = "3.2.3", default-features = false }
rusb = { version = "0.9.4", features = ["vendored"] }
rsa = { version = "0.9.10", features = ["pem"] }
tempfile = "3.25.0"
serde = { version = "^1.0", features = ["derive"] } serde = { version = "^1.0", features = ["derive"] }
serde_json = "^1.0" serde_json = "^1.0"
log = "^0.4" log = "^0.4"

View file

@ -14,8 +14,12 @@ edition.workspace = true
name = "uad" name = "uad"
path = "src/main.rs" path = "src/main.rs"
[features]
default = ["builtin-adb"]
builtin-adb = ["uad-core/builtin-adb"]
[dependencies] [dependencies]
uad-core = { path = "../uad-core" } uad-core = { path = "../uad-core", default-features = false }
clap.workspace = true clap.workspace = true
clap_complete = "4.4" clap_complete = "4.4"
tokio.workspace = true tokio.workspace = true

View file

@ -4,7 +4,7 @@ use std::collections::{HashMap, HashSet};
use std::io::Write; use std::io::Write;
use uad_core::adb::{ACommand, PmListPacksFlag}; use uad_core::adb::{ACommand, PmListPacksFlag};
use uad_core::sync::{ use uad_core::sync::{
CorePackage, Phone, User, apply_pkg_state_commands, get_devices_list, get_package_state, CorePackage, Phone, User, apply_pkg_state_commands, discover_devices, get_package_state,
run_adb_shell_action, run_adb_shell_action,
}; };
use uad_core::uad_lists::{Package, PackageState, Removal, UadList, load_debloat_lists}; use uad_core::uad_lists::{Package, PackageState, Removal, UadList, load_debloat_lists};
@ -17,10 +17,14 @@ use crate::{Cli, print_or_exit, println_or_exit};
/// List all connected Android devices /// List all connected Android devices
pub fn list_devices() -> Result<(), Box<dyn std::error::Error>> { pub fn list_devices() -> Result<(), Box<dyn std::error::Error>> {
println_or_exit!("Scanning for connected devices..."); println_or_exit!("Scanning for connected devices...");
let devices = get_devices_list(); let discovery = discover_devices();
let devices = discovery.devices;
if devices.is_empty() { if devices.is_empty() {
return Err(NO_DEVICES_FOUND.into()); return Err(discovery
.issue
.map_or_else(|| NO_DEVICES_FOUND.to_string(), |issue| issue.to_string())
.into());
} }
println_or_exit!("\nFound {} device(s):\n", devices.len()); println_or_exit!("\nFound {} device(s):\n", devices.len());
@ -104,6 +108,15 @@ pub struct DisplayConfig {
pub show_removal: bool, pub show_removal: bool,
} }
pub(crate) fn resolve_pm_flag(state_filter: Option<StateFilter>) -> Option<PmListPacksFlag> {
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 /// List packages on a device with filtering
pub fn list_packages( pub fn list_packages(
device: Option<String>, device: Option<String>,
@ -129,7 +142,7 @@ pub fn list_packages(
search, search,
}; };
let pm_flag = state_filter.and_then(StateFilter::to_pm_flag); let pm_flag = resolve_pm_flag(state_filter);
let system_packages = ACommand::new() let system_packages = ACommand::new()
.shell(&target_device.adb_id) .shell(&target_device.adb_id)
.pm() .pm()
@ -468,3 +481,37 @@ pub fn generate_completions(shell: Shell) {
let name = cmd.get_name().to_string(); let name = cmd.get_name().to_string();
generate(shell, &mut cmd, name, &mut std::io::stdout()); generate(shell, &mut cmd, name, &mut std::io::stdout());
} }
/// Show ADB backend and version information
pub fn show_adb_info() -> Result<(), Box<dyn std::error::Error>> {
let backend = uad_core::adb::AdbBackend::current();
println!("ADB Backend: {}\n", backend);
match ACommand::new().version() {
Ok(version) => {
println!("{version}");
Ok(())
}
Err(e) => {
eprintln!("Failed to get ADB version: {e}");
Err(e.into())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_and_uninstalled_lists_include_removed_packages() {
assert_eq!(
resolve_pm_flag(None),
Some(PmListPacksFlag::IncludeUninstalled)
);
assert_eq!(
resolve_pm_flag(Some(StateFilter::Uninstalled)),
Some(PmListPacksFlag::IncludeUninstalled)
);
}
}

View file

@ -1,14 +1,18 @@
use uad_core::sync::{Phone, User, get_devices_list}; use uad_core::sync::{Phone, User, discover_devices};
pub const NO_DEVICES_FOUND: &str = pub const NO_DEVICES_FOUND: &str =
"No devices found. Make sure ADB is installed and devices are connected."; "No devices found. Make sure ADB is installed and devices are connected.";
/// Get target device, either by serial or first available /// Get target device, either by serial or first available
pub fn get_target_device(device: Option<String>) -> Result<Phone, Box<dyn std::error::Error>> { pub fn get_target_device(device: Option<String>) -> Result<Phone, Box<dyn std::error::Error>> {
let devices = get_devices_list(); let discovery = discover_devices();
let devices = discovery.devices;
if devices.is_empty() { if devices.is_empty() {
return Err(NO_DEVICES_FOUND.into()); return Err(discovery
.issue
.map_or_else(|| NO_DEVICES_FOUND.to_string(), |issue| issue.to_string())
.into());
} }
let target_device = if let Some(device_id) = device { let target_device = if let Some(device_id) = device {

View file

@ -4,12 +4,15 @@
clippy::uninlined_format_args, clippy::uninlined_format_args,
clippy::map_unwrap_or, clippy::map_unwrap_or,
clippy::unnecessary_wraps, clippy::unnecessary_wraps,
clippy::exit,
reason = "Suppress non-critical pedantic/style lints to keep build green" 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 clap_complete::Shell;
use std::process::ExitCode; use std::process::ExitCode;
use uad_core::adb::{ACommand, AdbBackend, AdbDeviceStatus};
use uad_core::sync::{DeviceDiscoveryIssue, classify_device_issue};
use uad_core::uad_lists::PackageState; use uad_core::uad_lists::PackageState;
mod commands; mod commands;
@ -20,12 +23,46 @@ mod repl;
use filters::{ListFilter, RemovalFilter, StateFilter}; use filters::{ListFilter, RemovalFilter, StateFilter};
/// CLI-compatible ADB backend selection
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum AdbBackendArg {
/// Embedded ADB over USB
#[cfg(feature = "builtin-adb")]
Builtin,
/// Use system-installed adb binary
System,
}
impl From<AdbBackendArg> for AdbBackend {
fn from(arg: AdbBackendArg) -> Self {
match arg {
#[cfg(feature = "builtin-adb")]
AdbBackendArg::Builtin => AdbBackend::Builtin,
AdbBackendArg::System => AdbBackend::System,
}
}
}
#[derive(Parser)] #[derive(Parser)]
#[command(name = "uad")] #[command(name = "uad")]
#[command(about = "Universal Android Debloater - Command Line Interface", long_about = None)] #[command(about = "Universal Android Debloater - Command Line Interface", long_about = None)]
#[command(version)] #[command(version)]
#[command(propagate_version = true)] #[command(propagate_version = true)]
pub struct Cli { pub struct Cli {
/// ADB backend to use
#[arg(
short = 'B',
long = "backend",
value_enum,
global = true,
default_value = "system"
)]
backend: AdbBackendArg,
/// Stop the system ADB server before using the Builtin backend
#[arg(long, global = true)]
kill_adb_server: bool,
#[command(subcommand)] #[command(subcommand)]
command: Commands, command: Commands,
} }
@ -136,6 +173,9 @@ enum Commands {
/// Update UAD package lists from remote repository /// Update UAD package lists from remote repository
Update, Update,
/// Show ADB backend and version information
Adb,
/// Generate shell completion script /// Generate shell completion script
Completions { Completions {
/// Shell to generate completions for /// Shell to generate completions for
@ -169,6 +209,7 @@ async fn main() -> ExitCode {
fn run() -> Result<(), Box<dyn std::error::Error>> { fn run() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse(); let cli = Cli::parse();
configure_adb(&cli)?;
match cli.command { match cli.command {
Commands::Devices => { Commands::Devices => {
@ -239,6 +280,9 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
Commands::Update => { Commands::Update => {
commands::update_lists()?; commands::update_lists()?;
} }
Commands::Adb => {
commands::show_adb_info()?;
}
Commands::Completions { shell } => { Commands::Completions { shell } => {
commands::generate_completions(shell); commands::generate_completions(shell);
} }
@ -249,3 +293,123 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
Ok(()) Ok(())
} }
fn configure_adb(cli: &Cli) -> Result<(), Box<dyn std::error::Error>> {
let backend: AdbBackend = cli.backend.into();
#[cfg(feature = "builtin-adb")]
let using_builtin = backend == AdbBackend::Builtin;
#[cfg(not(feature = "builtin-adb"))]
let using_builtin = false;
if cli.kill_adb_server && !using_builtin {
return Err("--kill-adb-server requires --backend builtin".into());
}
if cli.kill_adb_server {
ACommand::kill_system_server()
.map_err(|err| format!("Failed to stop the system ADB server: {err}"))?;
}
backend.set_current();
if using_builtin && !cli.kill_adb_server && command_uses_device(&cli.command) {
let devices = ACommand::new().devices()?;
match builtin_preflight_issue(&devices) {
Some(DeviceDiscoveryIssue::Busy) => {
return Err(
"The system ADB server is using the USB device. Stop it with `adb kill-server` \
or rerun with `--kill-adb-server`."
.into(),
);
}
Some(issue @ DeviceDiscoveryIssue::NoPermissions) => {
return Err(issue.to_string().into());
}
_ => {}
}
}
Ok(())
}
fn builtin_preflight_issue(devices: &[(String, AdbDeviceStatus)]) -> Option<DeviceDiscoveryIssue> {
if devices
.iter()
.any(|(_, status)| status == &AdbDeviceStatus::Device)
{
return None;
}
classify_device_issue(devices).filter(|issue| {
matches!(
issue,
DeviceDiscoveryIssue::Busy | DeviceDiscoveryIssue::NoPermissions
)
})
}
fn command_uses_device(command: &Commands) -> bool {
match command {
Commands::Devices
| Commands::List { .. }
| Commands::Uninstall { .. }
| Commands::Enable { .. }
| Commands::Disable { .. }
| Commands::Repl { .. } => true,
Commands::Info { device, .. } => device.is_some(),
Commands::Update | Commands::Adb | Commands::Completions { .. } => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "builtin-adb")]
#[test]
fn builtin_server_shutdown_is_explicitly_opted_in() {
let cli = Cli::try_parse_from([
"uad",
"--backend",
"builtin",
"--kill-adb-server",
"devices",
])
.expect("valid Builtin CLI arguments");
assert_eq!(cli.backend, AdbBackendArg::Builtin);
assert!(cli.kill_adb_server);
assert!(command_uses_device(&cli.command));
}
#[test]
fn non_device_commands_skip_usb_preflight() {
assert!(!command_uses_device(&Commands::Update));
assert!(!command_uses_device(&Commands::Adb));
}
#[test]
fn builtin_preflight_allows_any_ready_device() {
let devices = vec![
("ready".to_string(), AdbDeviceStatus::Device),
("busy".to_string(), AdbDeviceStatus::Busy),
];
assert_eq!(builtin_preflight_issue(&devices), None);
}
#[test]
fn builtin_preflight_reports_blocking_issues_without_a_ready_device() {
assert_eq!(
builtin_preflight_issue(&[("busy".to_string(), AdbDeviceStatus::Busy)]),
Some(DeviceDiscoveryIssue::Busy)
);
assert_eq!(
builtin_preflight_issue(&[("denied".to_string(), AdbDeviceStatus::NoPermissions,)]),
Some(DeviceDiscoveryIssue::NoPermissions)
);
assert_eq!(
builtin_preflight_issue(&[("pending".to_string(), AdbDeviceStatus::Unauthorized,)]),
None
);
}
}

View file

@ -3,16 +3,22 @@
#[macro_export] #[macro_export]
macro_rules! println_or_exit { macro_rules! println_or_exit {
() => { () => {
let _ = writeln!(std::io::stdout()); if writeln!(std::io::stdout()).is_err() {
std::process::exit(0);
}
}; };
($($arg:tt)*) => { ($($arg:tt)*) => {
let _ = writeln!(std::io::stdout(), $($arg)*); if writeln!(std::io::stdout(), $($arg)*).is_err() {
std::process::exit(0);
}
}; };
} }
#[macro_export] #[macro_export]
macro_rules! print_or_exit { macro_rules! print_or_exit {
($($arg:tt)*) => { ($($arg:tt)*) => {
let _ = write!(std::io::stdout(), $($arg)*); if write!(std::io::stdout(), $($arg)*).is_err() {
std::process::exit(0);
}
}; };
} }

View file

@ -2,11 +2,13 @@ use rustyline::DefaultEditor;
use rustyline::error::ReadlineError; use rustyline::error::ReadlineError;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::Write; 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::sync::{CorePackage, Phone, User, apply_pkg_state_commands, get_package_state};
use uad_core::uad_lists::{Package, PackageState, Removal, UadList, load_debloat_lists}; use uad_core::uad_lists::{Package, PackageState, Removal, UadList, load_debloat_lists};
use crate::commands::{PackageListContext, display_package_list, execute_with_fallback}; use crate::commands::{
PackageListContext, display_package_list, execute_with_fallback, resolve_pm_flag,
};
use crate::device::{get_target_device, get_user}; use crate::device::{get_target_device, get_user};
use crate::filters::StateFilter; use crate::filters::StateFilter;
use crate::println_or_exit; use crate::println_or_exit;
@ -21,7 +23,9 @@ pub fn repl_mode(
device: Option<String>, device: Option<String>,
user_id: Option<u16>, user_id: Option<u16>,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let backend = AdbBackend::current();
println!("Universal Android Debloater - Interactive Mode"); println!("Universal Android Debloater - Interactive Mode");
println!("Using {} ADB backend", backend);
println!("Type 'help' for available commands, 'exit' or 'quit' to leave\n"); 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(device)?;
@ -145,8 +149,12 @@ fn handle_repl_line(
} }
"device" => { "device" => {
println!( println!(
"Device: {} ({}), Android SDK: {}, User: {}", "Device: {} ({}), Android SDK: {}, User: {}, Backend: {}",
device.model, device.adb_id, device.android_sdk, user.id device.model,
device.adb_id,
device.android_sdk,
user.id,
AdbBackend::current()
); );
} }
"clear" => { "clear" => {
@ -219,7 +227,7 @@ fn handle_list_command(
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let parsed = ReplListArgs::parse(args)?; let parsed = ReplListArgs::parse(args)?;
let pm_flag = parsed.state_filter.and_then(StateFilter::to_pm_flag); let pm_flag = resolve_pm_flag(parsed.state_filter);
let system_packages = ACommand::new() let system_packages = ACommand::new()
.shell(&device.adb_id) .shell(&device.adb_id)
.pm() .pm()

View file

@ -11,10 +11,21 @@ categories = ["command-line-utilities"]
edition.workspace = true edition.workspace = true
[features] [features]
default = [] default = ["builtin-adb"]
builtin-adb = [
"dep:adb_client",
"adb_client/usb",
"dep:rusb",
"dep:rsa",
"dep:tempfile",
]
self-update = ["dep:flate2", "dep:tar"] self-update = ["dep:flate2", "dep:tar"]
[dependencies] [dependencies]
adb_client = { workspace = true, optional = true }
rusb = { workspace = true, optional = true }
rsa = { workspace = true, optional = true }
tempfile = { workspace = true, optional = true }
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
log.workspace = true log.workspace = true

File diff suppressed because it is too large Load diff

View file

@ -1,9 +1,10 @@
use crate::CACHE_DIR; use crate::CACHE_DIR;
use crate::CONFIG_DIR; use crate::CONFIG_DIR;
use crate::adb::AdbBackend;
use crate::sync::User; use crate::sync::User;
use crate::utils::DisplayablePath; use crate::utils::DisplayablePath;
use log::error; use log::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::LazyLock; use std::sync::LazyLock;
@ -22,6 +23,28 @@ pub struct GeneralSettings {
pub theme: String, pub theme: String,
pub expert_mode: bool, pub expert_mode: bool,
pub backup_folder: PathBuf, pub backup_folder: PathBuf,
/// ADB backend to use: Builtin (direct USB) or System (installed adb)
#[serde(default, deserialize_with = "deserialize_adb_backend")]
pub adb_backend: AdbBackend,
}
#[derive(Deserialize)]
enum PersistedAdbBackend {
Builtin,
System,
}
fn deserialize_adb_backend<'de, D>(deserializer: D) -> Result<AdbBackend, D::Error>
where
D: Deserializer<'de>,
{
match PersistedAdbBackend::deserialize(deserializer)? {
#[cfg(feature = "builtin-adb")]
PersistedAdbBackend::Builtin => Ok(AdbBackend::Builtin),
#[cfg(not(feature = "builtin-adb"))]
PersistedAdbBackend::Builtin => Ok(AdbBackend::System),
PersistedAdbBackend::System => Ok(AdbBackend::System),
}
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
@ -49,6 +72,7 @@ impl Default for GeneralSettings {
theme: DEFAULT_THEME.to_string(), theme: DEFAULT_THEME.to_string(),
expert_mode: false, expert_mode: false,
backup_folder: CACHE_DIR.join("backups"), backup_folder: CACHE_DIR.join("backups"),
adb_backend: AdbBackend::default(),
} }
} }
} }
@ -121,6 +145,27 @@ mod tests {
assert_eq!(config.general.backup_folder, CACHE_DIR.join("backups")); assert_eq!(config.general.backup_folder, CACHE_DIR.join("backups"));
} }
#[test]
fn builtin_backend_config_is_preserved_or_safely_downgraded() {
let config: Config = toml::from_str(
r#"
[general]
theme = "Custom"
expert_mode = true
backup_folder = "/tmp/uad-backups"
adb_backend = "Builtin"
"#,
)
.expect("Builtin backend config should remain readable");
assert_eq!(config.general.theme, "Custom");
assert!(config.general.expert_mode);
#[cfg(feature = "builtin-adb")]
assert_eq!(config.general.adb_backend, AdbBackend::Builtin);
#[cfg(not(feature = "builtin-adb"))]
assert_eq!(config.general.adb_backend, AdbBackend::System);
}
// non-deterministic // non-deterministic
/* /*
#[test] #[test]

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
adb::{ACommand as AdbCommand, PM_CLEAR_PACK, PackageId}, adb::{ACommand as AdbCommand, AdbBackend, AdbDeviceStatus, PM_CLEAR_PACK, PackageId},
uad_lists::PackageState, uad_lists::PackageState,
}; };
use log::{error, info}; use log::{error, info};
@ -37,6 +37,96 @@ impl std::fmt::Display for Phone {
} }
} }
/// Why device discovery found no ready devices.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeviceDiscoveryIssue {
Busy,
Unauthorized,
NoPermissions,
Offline,
Backend(String),
}
impl DeviceDiscoveryIssue {
#[must_use]
pub const fn is_terminal_for(&self, backend: AdbBackend) -> bool {
if matches!(self, Self::Busy | Self::NoPermissions) {
return true;
}
#[cfg(feature = "builtin-adb")]
return matches!((self, backend), (Self::Backend(_), AdbBackend::Builtin));
#[cfg(not(feature = "builtin-adb"))]
{
let _ = backend;
false
}
}
}
impl std::fmt::Display for DeviceDiscoveryIssue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Busy => write!(
f,
"The system ADB server is using the USB device. Stop it and retry."
),
Self::Unauthorized => write!(
f,
"USB debugging authorization is pending. Accept the prompt on the device."
),
Self::NoPermissions => write!(
f,
"USB access was denied. Check the host's USB permissions or udev rules."
),
Self::Offline => write!(f, "The device is offline. Reconnect it and retry."),
Self::Backend(err) => write!(f, "ADB device discovery failed: {err}"),
}
}
}
/// Ready devices plus the reason no device was ready.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DeviceDiscovery {
pub devices: Vec<Phone>,
pub issue: Option<DeviceDiscoveryIssue>,
}
/// Classify statuses returned by `adb devices`.
#[must_use]
pub fn classify_device_issue(
devices: &[(String, AdbDeviceStatus)],
) -> Option<DeviceDiscoveryIssue> {
let mut statuses = devices.iter().map(|(_, status)| status);
if statuses
.clone()
.any(|status| matches!(status, AdbDeviceStatus::Busy))
{
Some(DeviceDiscoveryIssue::Busy)
} else if statuses
.clone()
.any(|status| matches!(status, AdbDeviceStatus::Unauthorized))
{
Some(DeviceDiscoveryIssue::Unauthorized)
} else if statuses
.clone()
.any(|status| matches!(status, AdbDeviceStatus::NoPermissions))
{
Some(DeviceDiscoveryIssue::NoPermissions)
} else if statuses
.clone()
.any(|status| matches!(status, AdbDeviceStatus::Offline))
{
Some(DeviceDiscoveryIssue::Offline)
} else {
statuses
.find_map(|status| match status {
AdbDeviceStatus::BackendError(error) => Some(error.clone()),
_ => None,
})
.map(DeviceDiscoveryIssue::Backend)
}
}
/// `UserInfo` but relevant to UAD /// `UserInfo` but relevant to UAD
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub struct User { pub struct User {
@ -61,6 +151,7 @@ pub enum AdbError {
/// This replaces the deprecated `adb_shell_command`. /// This replaces the deprecated `adb_shell_command`.
/// ///
/// If `serial` is empty, it lets ADB choose the default device. /// If `serial` is empty, it lets ADB choose the default device.
/// Uses the process-wide selected ADB backend.
pub fn run_adb_shell_action<S: AsRef<str>>( pub fn run_adb_shell_action<S: AsRef<str>>(
device_serial: S, device_serial: S,
action: &str, action: &str,
@ -227,23 +318,48 @@ pub fn request_builder(commands: &[&str], package: &str, user: Option<User>) ->
.collect() .collect()
} }
#[must_use]
fn sanitize_device_prop(raw: &str) -> String {
raw.chars()
.filter(|c| !c.is_control())
.collect::<String>()
.trim()
.to_string()
}
#[must_use]
fn parse_sdk_version(raw: &str) -> Option<u8> {
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. /// Get the model by querying the `ro.product.model` property.
/// ///
/// If `serial` is empty, it lets ADB choose the default device. /// If `serial` is empty, it lets ADB choose the default device.
#[must_use] #[must_use]
pub fn get_device_model(serial: &str) -> String { pub fn get_device_model(serial: &str) -> String {
AdbCommand::new() match AdbCommand::new().shell(serial).getprop("ro.product.model") {
.shell(serial) Ok(model) => sanitize_device_prop(&model),
.getprop("ro.product.model") Err(err) => {
.unwrap_or_else(|err| {
eprintln!("ERROR: {err}"); eprintln!("ERROR: {err}");
error!("{err}"); error!("{err}");
if err.contains("adb: no devices/emulators found") { if err.contains("adb: no devices/emulators found") {
"no devices/emulators found".to_string() "no devices/emulators found".to_string()
} else { } else {
err sanitize_device_prop(&err)
} }
}) }
}
} }
/// Get the brand by querying the `ro.product.brand` property. /// Get the brand by querying the `ro.product.brand` property.
@ -254,8 +370,7 @@ pub fn get_device_brand(serial: &str) -> String {
AdbCommand::new() AdbCommand::new()
.shell(serial) .shell(serial)
.getprop("ro.product.brand") .getprop("ro.product.brand")
// `trim` is just-in-case .map(|s| sanitize_device_prop(&s))
.map(|s| s.trim().to_string())
.unwrap_or_default() .unwrap_or_default()
} }
@ -269,7 +384,14 @@ pub fn get_android_sdk(device_serial: &str) -> u8 {
.shell(device_serial) .shell(device_serial)
.getprop("ro.build.version.sdk") .getprop("ro.build.version.sdk")
.map_or(0, |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
}
}) })
} }
@ -457,44 +579,81 @@ pub fn list_users_idx_prot(device_serial: &str) -> Vec<User> {
.unwrap_or_default() .unwrap_or_default()
} }
/// This matches serials (`getprop ro.serialno`) /// Discover connected devices using the selected ADB backend.
/// that are authorized by the user.
#[must_use] #[must_use]
pub fn get_devices_list() -> Vec<Phone> { pub fn discover_devices() -> DeviceDiscovery {
let backend = AdbBackend::current();
retry( retry(
Fixed::from_millis(500).take(if cfg!(debug_assertions) { 3 } else { 10 }), Fixed::from_millis(500).take(if cfg!(debug_assertions) { 3 } else { 10 }),
|| match AdbCommand::new().devices() { || match AdbCommand::new().devices() {
Ok(devices) => { Ok(devices) => {
let issue = classify_device_issue(&devices);
let mut device_list: Vec<Phone> = vec![]; let mut device_list: Vec<Phone> = vec![];
if devices.iter().all(|(_, stat)| stat != "device") { for (serial, status) in devices {
return OperationResult::Retry(vec![]); if status != AdbDeviceStatus::Device {
} continue;
for device in devices { }
let serial = &device.0;
device_list.push(Phone { device_list.push(Phone {
model: format!("{} {}", get_device_brand(serial), get_device_model(serial)), model: format!(
android_sdk: get_android_sdk(serial), "{} {}",
user_list: list_users_idx_prot(serial), get_device_brand(&serial),
adb_id: serial.clone(), get_device_model(&serial)
),
android_sdk: get_android_sdk(&serial),
user_list: list_users_idx_prot(&serial),
adb_id: serial,
}); });
} }
OperationResult::Ok(device_list)
if !device_list.is_empty() {
return OperationResult::Ok(DeviceDiscovery {
devices: device_list,
issue: None,
});
}
let discovery = DeviceDiscovery {
devices: vec![],
issue,
};
if discovery
.issue
.as_ref()
.is_some_and(|current_issue| current_issue.is_terminal_for(backend))
{
OperationResult::Ok(discovery)
} else {
OperationResult::Retry(discovery)
}
} }
Err(err) => { Err(err) => {
error!("get_devices_list() -> {err}"); error!("get_devices_list() -> {err}");
let test: Vec<Phone> = vec![]; OperationResult::Retry(DeviceDiscovery {
OperationResult::Retry(test) devices: vec![],
issue: Some(DeviceDiscoveryIssue::Backend(err)),
})
} }
}, },
) )
.unwrap_or_default() .unwrap_or_else(|last_attempt| last_attempt.error)
}
/// Get ready connected devices using the selected ADB backend.
#[must_use]
pub fn get_devices_list() -> Vec<Phone> {
discover_devices().devices
} }
#[must_use] #[must_use]
pub fn initial_load() -> bool { pub fn initial_load() -> bool {
match AdbCommand::new().devices() { match AdbCommand::new().devices() {
Ok(_devices) => true, // A successful query means the selected backend is available. Device
Err(_err) => false, // presence and authorization are handled by get_devices_list().
Ok(_) => true,
Err(err) => {
error!("initial_load() -> {err}");
false
}
} }
} }
@ -661,3 +820,59 @@ pub fn attempt_fallback(
)), )),
} }
} }
#[cfg(test)]
mod tests {
use super::*;
fn statuses(values: &[AdbDeviceStatus]) -> Vec<(String, AdbDeviceStatus)> {
values
.iter()
.enumerate()
.map(|(index, status)| (format!("device-{index}"), status.clone()))
.collect()
}
#[test]
fn discovery_issue_classification_is_specific_and_prioritized() {
assert_eq!(
classify_device_issue(&statuses(&[
AdbDeviceStatus::Offline,
AdbDeviceStatus::Busy,
AdbDeviceStatus::Unauthorized,
])),
Some(DeviceDiscoveryIssue::Busy)
);
assert_eq!(
classify_device_issue(&statuses(&[AdbDeviceStatus::NoPermissions])),
Some(DeviceDiscoveryIssue::NoPermissions)
);
assert_eq!(
classify_device_issue(&statuses(&[AdbDeviceStatus::BackendError(
"invalid key".to_string(),
)])),
Some(DeviceDiscoveryIssue::Backend("invalid key".to_string()))
);
assert_eq!(
classify_device_issue(&statuses(&[AdbDeviceStatus::Device])),
None
);
}
#[test]
fn terminal_discovery_issues_are_backend_aware() {
assert!(DeviceDiscoveryIssue::Busy.is_terminal_for(AdbBackend::System));
assert!(DeviceDiscoveryIssue::NoPermissions.is_terminal_for(AdbBackend::System));
assert!(!DeviceDiscoveryIssue::Unauthorized.is_terminal_for(AdbBackend::System));
assert!(!DeviceDiscoveryIssue::Offline.is_terminal_for(AdbBackend::System));
#[cfg(feature = "builtin-adb")]
{
assert!(!DeviceDiscoveryIssue::Unauthorized.is_terminal_for(AdbBackend::Builtin));
assert!(!DeviceDiscoveryIssue::Offline.is_terminal_for(AdbBackend::Builtin));
assert!(
DeviceDiscoveryIssue::Backend("invalid key".to_string())
.is_terminal_for(AdbBackend::Builtin)
);
}
}
}

View file

@ -15,14 +15,15 @@ name = "uad-ng"
path = "src/main.rs" path = "src/main.rs"
[features] [features]
default = ["wgpu", "self-update", "img"] default = ["wgpu", "self-update", "img", "builtin-adb"]
wgpu = [] # Iced/wgpu is default builtin-adb = ["uad-core/builtin-adb"]
self-update = ["flate2", "tar"] self-update = ["flate2", "tar", "uad-core/self-update"]
no-self-update = [] no-self-update = []
wgpu = [] # Iced/wgpu is default
img = ["image", "iced/image"] img = ["image", "iced/image"]
[dependencies] [dependencies]
uad-core = { path = "../uad-core", features = ["self-update"] } uad-core = { path = "../uad-core", default-features = false }
iced.workspace = true iced.workspace = true
image = { workspace = true, optional = true } image = { workspace = true, optional = true }
rfd.workspace = true rfd.workspace = true

View file

@ -16,11 +16,13 @@ use iced::{Alignment, Element, Length, Settings, Task, window::Settings as Windo
use iced::{Subscription, event, keyboard}; use iced::{Subscription, event, keyboard};
#[cfg(feature = "img")] #[cfg(feature = "img")]
use image::ImageFormat; use image::ImageFormat;
use log::{debug, error, info}; #[cfg(feature = "self-update")]
use log::debug;
use log::{error, info};
#[cfg(feature = "self-update")] #[cfg(feature = "self-update")]
use std::path::PathBuf; use std::path::PathBuf;
use uad_core::adb; use uad_core::adb;
use uad_core::sync::{Phone, get_devices_list, initial_load}; use uad_core::sync::{DeviceDiscovery, Phone, discover_devices, initial_load};
use uad_core::uad_lists::UadListState; use uad_core::uad_lists::UadListState;
use uad_core::update::{Release, SelfUpdateState, SelfUpdateStatus, get_latest_release}; use uad_core::update::{Release, SelfUpdateState, SelfUpdateStatus, get_latest_release};
use uad_core::utils::{FULL_NAME, NAME}; use uad_core::utils::{FULL_NAME, NAME};
@ -68,7 +70,7 @@ pub enum Message {
SettingsAction(SettingsMessage), SettingsAction(SettingsMessage),
RefreshButtonPressed, RefreshButtonPressed,
RebootButtonPressed, RebootButtonPressed,
LoadDevices(Vec<Phone>), LoadDevices(DeviceDiscovery),
#[cfg(feature = "self-update")] #[cfg(feature = "self-update")]
_NewReleaseDownloaded(Result<(PathBuf, PathBuf), ()>), _NewReleaseDownloaded(Result<(PathBuf, PathBuf), ()>),
GetLatestRelease(Result<Option<Release>, ()>), GetLatestRelease(Result<Option<Release>, ()>),
@ -83,21 +85,24 @@ impl UadGui {
reason = "required by iced's Application trait interface" reason = "required by iced's Application trait interface"
)] )]
fn title(&self) -> String { fn title(&self) -> String {
self.selected_device.as_ref().map_or_else( if self.selected_device.is_some() {
|| FULL_NAME.to_string(), format!("{FULL_NAME} - device connected")
|device| format!("{FULL_NAME} - {}", device.model), } else {
) FULL_NAME.to_string()
}
} }
fn new() -> (Self, Task<Message>) { fn new() -> (Self, Task<Message>) {
let app = Self::default();
let backend = app.settings_view.general.adb_backend;
backend.set_current();
( (
Self::default(), app,
Task::batch([ Task::batch([
// Used in crate::widgets::navigation_menu::ICONS. Name is `icomoon`. // Used in crate::widgets::navigation_menu::ICONS. Name is `icomoon`.
font::load(include_bytes!("../../../resources/assets/icons.ttf").as_slice()) font::load(include_bytes!("../../../resources/assets/icons.ttf").as_slice())
.map(Message::FontLoaded), .map(Message::FontLoaded),
Task::perform(async { initial_load() }, Message::ADBSatisfied), Task::perform(async move { initial_load() }, Message::ADBSatisfied),
Task::perform(async { get_devices_list() }, Message::LoadDevices),
Task::perform( Task::perform(
async move { get_latest_release() }, async move { get_latest_release() },
Message::GetLatestRelease, Message::GetLatestRelease,
@ -158,7 +163,12 @@ impl UadGui {
)] )]
fn update(&mut self, msg: Message) -> Task<Message> { fn update(&mut self, msg: Message) -> Task<Message> {
match msg { match msg {
Message::LoadDevices(devices_list) => { Message::LoadDevices(discovery) => {
let DeviceDiscovery {
devices: devices_list,
issue,
} = discovery;
self.apps_view.discovery_issue = issue;
self.selected_device = match &self.selected_device { self.selected_device = match &self.selected_device {
Some(s_device) => { Some(s_device) => {
// Try to reload last selected phone // Try to reload last selected phone
@ -171,6 +181,11 @@ impl UadGui {
}; };
self.devices_list = devices_list; self.devices_list = devices_list;
if self.devices_list.is_empty() {
self.apps_view.loading_state = ListLoadingState::FindingPhones;
return Task::none();
}
#[expect(unused_must_use, reason = "side-effect")] #[expect(unused_must_use, reason = "side-effect")]
{ {
self.update(Message::SettingsAction(SettingsMessage::LoadDeviceSettings)); self.update(Message::SettingsAction(SettingsMessage::LoadDeviceSettings));
@ -202,7 +217,7 @@ impl UadGui {
self.adb_satisfied, self.adb_satisfied,
))); )));
} }
Task::perform(async { get_devices_list() }, Message::LoadDevices) Task::perform(async move { discover_devices() }, Message::LoadDevices)
} }
Message::RebootButtonPressed => { Message::RebootButtonPressed => {
self.apps_view = AppsView::default(); self.apps_view = AppsView::default();
@ -213,10 +228,14 @@ impl UadGui {
self.selected_device = None; self.selected_device = None;
self.devices_list = vec![]; self.devices_list = vec![];
Task::perform( Task::perform(
async { adb::ACommand::new().shell(serial).reboot() }, async move { adb::ACommand::new().shell(serial).reboot() },
|_| Message::Nothing, |_| Message::Nothing,
) )
} }
Message::AppsAction(AppsMessage::OpenAdbSettings) => {
self.view = View::Settings;
Task::none()
}
Message::AppsAction(msg) => self Message::AppsAction(msg) => self
.apps_view .apps_view
.update( .update(
@ -261,6 +280,15 @@ impl UadGui {
} }
} }
} }
SettingsMessage::AdbBackendApplied(backend) => {
backend.set_current();
self.apps_view = AppsView::default();
self.selected_device = None;
self.devices_list.clear();
self.adb_satisfied = false;
return Task::perform(async move { initial_load() }, Message::ADBSatisfied);
}
_ => (), _ => (),
} }
self.settings_view self.settings_view
@ -387,9 +415,17 @@ impl UadGui {
} }
Message::ADBSatisfied(result) => { Message::ADBSatisfied(result) => {
self.adb_satisfied = result; self.adb_satisfied = result;
self.update(Message::AppsAction(AppsMessage::ADBSatisfied( let update_view = self.update(Message::AppsAction(AppsMessage::ADBSatisfied(
self.adb_satisfied, self.adb_satisfied,
))) )));
if result {
Task::batch([
update_view,
Task::perform(async move { discover_devices() }, Message::LoadDevices),
])
} else {
update_view
}
} }
Message::Nothing => Task::none(), Message::Nothing => Task::none(),
} }

View file

@ -3,10 +3,8 @@ use crate::theme::Theme;
use crate::{gui::UpdateState, style, widgets::text}; use crate::{gui::UpdateState, style, widgets::text};
use iced::widget::{Space, column, container, row}; use iced::widget::{Space, column, container, row};
use iced::{Alignment, Element, Length, Renderer}; use iced::{Alignment, Element, Length, Renderer};
use log::error;
use std::path::PathBuf; use std::path::PathBuf;
use uad_core::CACHE_DIR; use uad_core::CACHE_DIR;
use uad_core::adb;
use uad_core::uad_lists::LIST_FNAME; use uad_core::uad_lists::LIST_FNAME;
use uad_core::utils::{FULL_NAME, NAME, last_modified_date, open_url}; use uad_core::utils::{FULL_NAME, NAME, last_modified_date, open_url};
@ -95,38 +93,10 @@ impl About {
.spacing(10) .spacing(10)
.width(550); .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")] #[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"))] #[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); let update_column = update_column.align_x(Alignment::Center).spacing(10);

View file

@ -4,8 +4,11 @@ use crate::theme::Theme;
use crate::widgets::navigation_menu::ICONS; use crate::widgets::navigation_menu::ICONS;
use log::{error, info, warn}; use log::{error, info, warn};
use std::path::PathBuf; use std::path::PathBuf;
use uad_core::adb::AdbBackend;
use uad_core::config::DeviceSettings; use uad_core::config::DeviceSettings;
use uad_core::sync::{AdbError, Phone, User, apply_pkg_state_commands}; use uad_core::sync::{
AdbError, CorePackage, DeviceDiscoveryIssue, Phone, User, apply_pkg_state_commands,
};
use uad_core::uad_lists::{ use uad_core::uad_lists::{
Opposite, PackageHashMap, PackageState, Removal, UadList, UadListState, load_debloat_lists, Opposite, PackageHashMap, PackageState, Removal, UadList, UadListState, load_debloat_lists,
}; };
@ -68,6 +71,7 @@ pub struct List {
export_modal: bool, export_modal: bool,
current_package_index: usize, current_package_index: usize,
is_adb_satisfied: bool, is_adb_satisfied: bool,
pub discovery_issue: Option<DeviceDiscoveryIssue>,
copy_confirmation: bool, copy_confirmation: bool,
fallback_notifications: Vec<String>, fallback_notifications: Vec<String>,
} }
@ -102,6 +106,7 @@ pub enum Message {
DescriptionEdit(text_editor::Action), DescriptionEdit(text_editor::Action),
CopyError(String), CopyError(String),
HideCopyConfirmation, HideCopyConfirmation,
OpenAdbSettings,
} }
pub struct SummaryEntry { pub struct SummaryEntry {
@ -130,6 +135,33 @@ impl From<Removal> for SummaryEntry {
} }
impl List { impl List {
#[inline]
fn current_user_index(&self) -> usize {
self.selected_user.unwrap_or_default().index
}
#[inline]
fn refilter(&mut self) -> Task<Message> {
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( pub fn update(
&mut self, &mut self,
settings: &mut Settings, settings: &mut Settings,
@ -172,7 +204,7 @@ impl List {
Message::GoToUrl(url) => Self::on_go_to_url(url), Message::GoToUrl(url) => Self::on_go_to_url(url),
Message::ExportSelection => self.on_export_selection(), Message::ExportSelection => self.on_export_selection(),
Message::SelectionExported(res) => self.on_selection_exported(res), Message::SelectionExported(res) => self.on_selection_exported(res),
Message::Nothing => Task::none(), Message::Nothing | Message::OpenAdbSettings => Task::none(),
Message::DescriptionEdit(action) => self.on_description_edit(action), Message::DescriptionEdit(action) => self.on_description_edit(action),
Message::CopyError(err) => self.on_copy_error(err), Message::CopyError(err) => self.on_copy_error(err),
Message::HideCopyConfirmation => self.on_hide_copy_confirmation(), Message::HideCopyConfirmation => self.on_hide_copy_confirmation(),
@ -213,10 +245,36 @@ impl List {
), ),
LoadingState::FindingPhones => { LoadingState::FindingPhones => {
if self.is_adb_satisfied { if self.is_adb_satisfied {
waiting_view("Finding connected devices...", None, style::Text::Default) if let Some(issue) = &self.discovery_issue {
let action = if matches!(issue, DeviceDiscoveryIssue::Busy) {
Some(button("Open ADB settings").on_press(Message::OpenAdbSettings))
} else {
None
};
return waiting_view(&issue.to_string(), action, style::Text::Danger);
}
let waiting_message = match AdbBackend::current() {
#[cfg(feature = "builtin-adb")]
AdbBackend::Builtin => {
"Waiting for a USB device and USB debugging authorization..."
}
AdbBackend::System => "Finding connected devices...",
};
waiting_view(waiting_message, None, style::Text::Default)
} else { } else {
let connection_help = match AdbBackend::current() {
#[cfg(feature = "builtin-adb")]
AdbBackend::Builtin => {
"Builtin ADB could not initialize USB access. Check USB permissions \
and reconnect the device."
}
AdbBackend::System => {
"ADB is not installed or could not be started. Install Android \
platform-tools and relaunch the application."
}
};
waiting_view( waiting_view(
"ADB is not installed on your system, install ADB and relaunch application.", connection_help,
Some(button("Read on how to get started.") Some(button("Read on how to get started.")
.on_press(Message::GoToUrl(PathBuf::from( .on_press(Message::GoToUrl(PathBuf::from(
"https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Getting-started", "https://github.com/Universal-Debloater-Alliance/universal-android-debloater-next-generation/wiki/Getting-started",
@ -329,12 +387,13 @@ impl List {
settings: &Settings, settings: &Settings,
selected_device: &Phone, selected_device: &Phone,
) -> Element<'_, Message, Theme, Renderer> { ) -> Element<'_, Message, Theme, Renderer> {
let i_user = self.current_user_index();
let packages = self let packages = self
.filtered_packages .filtered_packages
.iter() .iter()
.fold(column![].spacing(6), |col, &i| { .fold(column![].spacing(6), |col, &i| {
col.push( col.push(
self.phone_packages[self.selected_user.unwrap_or_default().index][i] self.phone_packages[i_user][i]
.view(settings, selected_device) .view(settings, selected_device)
.map(move |msg| Message::List(i, msg)), .map(move |msg| Message::List(i, msg)),
) )
@ -748,8 +807,7 @@ impl List {
.selected_removal .selected_removal
.expect("removal recommendation must be selected"); .expect("removal recommendation must be selected");
self.filtered_packages = self.phone_packages self.filtered_packages = self.phone_packages[self.current_user_index()]
[self.selected_user.expect("User must be selected").index]
.iter() .iter()
// we must filter the indices associated with pack-rows, // we must filter the indices associated with pack-rows,
// that's why `enumerate` is before `filter`. // that's why `enumerate` is before `filter`.
@ -776,7 +834,7 @@ impl List {
vec![ vec![
fetch_packages(&uad_list, serial, None) fetch_packages(&uad_list, serial, None)
.into_iter() .into_iter()
.map(PackageRow::from) .map(|core| Self::map_core_to_row(&uad_list, &core))
.collect(), .collect(),
] ]
} else { } else {
@ -785,7 +843,7 @@ impl List {
.map(|user| { .map(|user| {
fetch_packages(&uad_list, serial, Some(user.id)) fetch_packages(&uad_list, serial, Some(user.id))
.into_iter() .into_iter()
.map(PackageRow::from) .map(|core| Self::map_core_to_row(&uad_list, &core))
.collect() .collect()
}) })
.collect() .collect()
@ -841,7 +899,7 @@ impl List {
} }
fn on_restoring_device(&mut self, output: Result<PackageInfo, AdbError>) -> Task<Message> { fn on_restoring_device(&mut self, output: Result<PackageInfo, AdbError>) -> Task<Message> {
let i_user = self.selected_user.unwrap_or_default().index; let i_user = self.current_user_index();
if let Ok(p) = output { if let Ok(p) = output {
self.loading_state = self.loading_state =
LoadingState::RestoringDevice(self.phone_packages[i_user][p.index].name.clone()); LoadingState::RestoringDevice(self.phone_packages[i_user][p.index].name.clone());
@ -886,7 +944,7 @@ impl List {
} }
fn on_apply_filters(&mut self, packages: Vec<Vec<PackageRow>>) -> Task<Message> { fn on_apply_filters(&mut self, packages: Vec<Vec<PackageRow>>) -> Task<Message> {
let i_user = self.selected_user.unwrap_or_default().index; let i_user = self.current_user_index();
self.phone_packages = packages; self.phone_packages = packages;
self.filtered_packages = (0..self.phone_packages[i_user].len()).collect(); self.filtered_packages = (0..self.phone_packages[i_user].len()).collect();
self.selected_removal = Some(Removal::Recommended); self.selected_removal = Some(Removal::Recommended);
@ -906,7 +964,7 @@ impl List {
selected_device: &mut Phone, selected_device: &mut Phone,
list_update_state: &mut UadListState, list_update_state: &mut UadListState,
) -> Task<Message> { ) -> Task<Message> {
let i_user = self.selected_user.unwrap_or_default().index; let i_user = self.current_user_index();
for i in self.filtered_packages.clone() { for i in self.filtered_packages.clone() {
if self.phone_packages[i_user][i].selected != selected { if self.phone_packages[i_user][i].selected != selected {
#[expect(unused_must_use, reason = "side-effect")] #[expect(unused_must_use, reason = "side-effect")]
@ -924,26 +982,22 @@ impl List {
fn on_search_input_changed(&mut self, letter: String) -> Task<Message> { fn on_search_input_changed(&mut self, letter: String) -> Task<Message> {
self.input_value = letter; self.input_value = letter;
Self::filter_package_lists(self); self.refilter()
Task::none()
} }
fn on_list_selected(&mut self, list: UadList) -> Task<Message> { fn on_list_selected(&mut self, list: UadList) -> Task<Message> {
self.selected_list = Some(list); self.selected_list = Some(list);
Self::filter_package_lists(self); self.refilter()
Task::none()
} }
fn on_package_state_selected(&mut self, package_state: PackageState) -> Task<Message> { fn on_package_state_selected(&mut self, package_state: PackageState) -> Task<Message> {
self.selected_package_state = Some(package_state); self.selected_package_state = Some(package_state);
Self::filter_package_lists(self); self.refilter()
Task::none()
} }
fn on_removal_selected(&mut self, removal: Removal) -> Task<Message> { fn on_removal_selected(&mut self, removal: Removal) -> Task<Message> {
self.selected_removal = Some(removal); self.selected_removal = Some(removal);
Self::filter_package_lists(self); self.refilter()
Task::none()
} }
fn on_list_row( fn on_list_row(
@ -953,7 +1007,7 @@ impl List {
settings: &Settings, settings: &Settings,
selected_device: &mut Phone, selected_device: &mut Phone,
) -> Task<Message> { ) -> Task<Message> {
let i_user = self.selected_user.unwrap_or_default().index; let i_user = self.current_user_index();
#[expect(unused_must_use, reason = "side-effect")] #[expect(unused_must_use, reason = "side-effect")]
{ {
self.phone_packages[i_user][i_package] self.phone_packages[i_user][i_package]
@ -1031,8 +1085,7 @@ impl List {
self.selected_user = Some(user); self.selected_user = Some(user);
self.fallback_notifications.clear(); self.fallback_notifications.clear();
self.filtered_packages = (0..self.phone_packages[user.index].len()).collect(); self.filtered_packages = (0..self.phone_packages[user.index].len()).collect();
Self::filter_package_lists(self); self.refilter()
Task::none()
} }
#[allow( #[allow(

View file

@ -15,6 +15,7 @@ use iced::{Alignment, Element, Length, Renderer, Task, alignment};
use log::{debug, error, info}; use log::{debug, error, info};
use std::path::PathBuf; use std::path::PathBuf;
use uad_core::{ use uad_core::{
adb::{ACommand, AdbBackend},
config::{BackupSettings, Config, DeviceSettings, GeneralSettings}, config::{BackupSettings, Config, DeviceSettings, GeneralSettings},
save::{backup_phone, list_available_backup_user, list_available_backups, restore_backup}, save::{backup_phone, list_available_backup_user, list_available_backups, restore_backup},
sync::{ sync::{
@ -27,6 +28,9 @@ use uad_core::{
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum PopUpModal { pub enum PopUpModal {
ExportUninstalled, ExportUninstalled,
ConfirmAdbServerStop,
StoppingAdbServer,
AdbBackendError,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -35,15 +39,28 @@ pub struct Settings {
pub device: DeviceSettings, pub device: DeviceSettings,
is_loading: bool, is_loading: bool,
modal: Option<PopUpModal>, modal: Option<PopUpModal>,
/// Cached ADB version string for display
adb_version: String,
pending_adb_backend: Option<AdbBackend>,
adb_backend_error: Option<String>,
} }
impl Default for Settings { impl Default for Settings {
fn default() -> Self { fn default() -> Self {
let general = Config::load_configuration_file().general;
general.adb_backend.set_current();
// Fetch initial ADB version
let adb_version = ACommand::new()
.version()
.unwrap_or_else(|e| format!("Error: {e}"));
Self { Self {
general: Config::load_configuration_file().general, general,
device: DeviceSettings::default(), device: DeviceSettings::default(),
is_loading: false, is_loading: false,
modal: None, modal: None,
adb_version,
pending_adb_backend: None,
adb_backend_error: None,
} }
} }
} }
@ -55,6 +72,13 @@ pub enum Message {
DisableMode(bool), DisableMode(bool),
MultiUserMode(bool), MultiUserMode(bool),
ApplyTheme(Theme), ApplyTheme(Theme),
ApplyAdbBackend(AdbBackend),
#[cfg(feature = "builtin-adb")]
RetryBuiltinAdb,
ConfirmAdbBackendSwitch,
AdbServerStopped(Result<String, String>),
AdbBackendApplied(AdbBackend),
AdbVersionFetched(Result<String, String>),
UrlPressed(PathBuf), UrlPressed(PathBuf),
BackupSelected(DisplayablePath), BackupSelected(DisplayablePath),
BackupDevice, BackupDevice,
@ -83,6 +107,12 @@ impl Settings {
Message::DisableMode(toggled) => self.handle_disable_mode(phone, toggled), Message::DisableMode(toggled) => self.handle_disable_mode(phone, toggled),
Message::MultiUserMode(toggled) => self.handle_multi_user_mode(phone, toggled), Message::MultiUserMode(toggled) => self.handle_multi_user_mode(phone, toggled),
Message::ApplyTheme(theme) => self.handle_apply_theme(phone, theme), Message::ApplyTheme(theme) => self.handle_apply_theme(phone, theme),
Message::ApplyAdbBackend(backend) => self.handle_apply_adb_backend(backend),
#[cfg(feature = "builtin-adb")]
Message::RetryBuiltinAdb => self.handle_retry_builtin_adb(),
Message::ConfirmAdbBackendSwitch => self.handle_confirm_adb_backend_switch(),
Message::AdbServerStopped(result) => self.handle_adb_server_stopped(result),
Message::AdbVersionFetched(result) => self.handle_adb_version_fetched(result),
Message::UrlPressed(url) => Self::handle_url_pressed(url), Message::UrlPressed(url) => Self::handle_url_pressed(url),
Message::LoadDeviceSettings => self.handle_load_device_settings(phone), Message::LoadDeviceSettings => self.handle_load_device_settings(phone),
Message::BackupSelected(d_path) => self.handle_backup_selected(d_path), Message::BackupSelected(d_path) => self.handle_backup_selected(d_path),
@ -91,7 +121,7 @@ impl Settings {
Message::RestoreDevice => { Message::RestoreDevice => {
self.handle_restore_device(phone, packages, nb_running_async_adb_commands) self.handle_restore_device(phone, packages, nb_running_async_adb_commands)
} }
Message::RestoringDevice(_) => Task::none(), Message::RestoringDevice(_) | Message::AdbBackendApplied(_) => Task::none(),
Message::FolderChosen(result) => self.handle_folder_chosen(phone, result), Message::FolderChosen(result) => self.handle_folder_chosen(phone, result),
Message::ChooseBackUpFolder => self.handle_choose_backup_folder(), Message::ChooseBackUpFolder => self.handle_choose_backup_folder(),
Message::ExportPackages => Self::handle_export_packages(selected_user, packages), Message::ExportPackages => Self::handle_export_packages(selected_user, packages),
@ -101,6 +131,8 @@ impl Settings {
fn handle_modal_hide(&mut self) -> Task<Message> { fn handle_modal_hide(&mut self) -> Task<Message> {
self.modal = None; self.modal = None;
self.pending_adb_backend = None;
self.adb_backend_error = None;
Task::none() Task::none()
} }
@ -138,6 +170,85 @@ impl Settings {
Task::none() Task::none()
} }
fn handle_apply_adb_backend(&mut self, backend: AdbBackend) -> Task<Message> {
if backend == self.general.adb_backend {
return Task::none();
}
#[cfg(feature = "builtin-adb")]
if backend == AdbBackend::Builtin {
self.pending_adb_backend = Some(backend);
self.adb_backend_error = None;
self.modal = Some(PopUpModal::ConfirmAdbServerStop);
return Task::none();
}
self.commit_adb_backend(backend)
}
#[cfg(feature = "builtin-adb")]
fn handle_retry_builtin_adb(&mut self) -> Task<Message> {
self.pending_adb_backend = Some(AdbBackend::Builtin);
self.adb_backend_error = None;
self.modal = Some(PopUpModal::ConfirmAdbServerStop);
Task::none()
}
fn handle_confirm_adb_backend_switch(&mut self) -> Task<Message> {
self.modal = Some(PopUpModal::StoppingAdbServer);
Task::perform(
async { ACommand::kill_system_server() },
Message::AdbServerStopped,
)
}
fn handle_adb_server_stopped(&mut self, result: Result<String, String>) -> Task<Message> {
match result {
Ok(_) => {
let Some(backend) = self.pending_adb_backend.take() else {
self.modal = None;
return Task::none();
};
self.modal = None;
self.commit_adb_backend(backend)
}
Err(err) => {
self.pending_adb_backend = None;
self.adb_backend_error = Some(format!(
"Could not stop the system ADB server: {err}\n\
Builtin ADB was not enabled."
));
self.modal = Some(PopUpModal::AdbBackendError);
Task::none()
}
}
}
fn commit_adb_backend(&mut self, backend: AdbBackend) -> Task<Message> {
self.general.adb_backend = backend;
backend.set_current();
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}");
Task::batch([
Task::perform(
async move { ACommand::new().version() },
Message::AdbVersionFetched,
),
Task::done(Message::AdbBackendApplied(backend)),
])
}
fn handle_adb_version_fetched(&mut self, result: Result<String, String>) -> Task<Message> {
self.adb_version = match result {
Ok(version) => version,
Err(e) => format!("Error: {e}"),
};
Task::none()
}
fn handle_url_pressed(url: PathBuf) -> Task<Message> { fn handle_url_pressed(url: PathBuf) -> Task<Message> {
open_url(url); open_url(url);
Task::none() Task::none()
@ -344,8 +455,18 @@ impl Settings {
self.build_device_content(phone, apps_view) self.build_device_content(phone, apps_view)
}; };
if let Some(PopUpModal::ExportUninstalled) = self.modal { match self.modal.as_ref() {
return Self::render_export_modal(content); Some(PopUpModal::ExportUninstalled) => return Self::render_export_modal(content),
Some(PopUpModal::ConfirmAdbServerStop) => {
return Self::render_adb_server_confirmation_modal(content);
}
Some(PopUpModal::StoppingAdbServer) => {
return Self::render_adb_server_stopping_modal(content);
}
Some(PopUpModal::AdbBackendError) => {
return self.render_adb_backend_error_modal(content);
}
None => {}
} }
container(scrollable(content)) container(scrollable(content))
@ -361,6 +482,8 @@ impl Settings {
self.theme_container(), self.theme_container(),
text("General").size(26), text("General").size(26),
self.general_container(), self.general_container(),
text("ADB").size(26),
self.adb_container(),
text("Current device").size(26), text("Current device").size(26),
Self::no_device_container(), Self::no_device_container(),
text("Backup / Restore").size(26), text("Backup / Restore").size(26),
@ -381,6 +504,8 @@ impl Settings {
self.theme_container(), self.theme_container(),
text("General").size(26), text("General").size(26),
self.general_container(), self.general_container(),
text("ADB").size(26),
self.adb_container(),
text("Current device").size(26), text("Current device").size(26),
Self::warning_container(phone), Self::warning_container(phone),
self.device_specific_container(phone), self.device_specific_container(phone),
@ -460,6 +585,62 @@ impl Settings {
.into() .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);
#[cfg(feature = "builtin-adb")]
let backend_description =
"System: Uses your installed adb binary. Builtin: Embedded ADB over USB.";
#[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),
text(&self.adb_version)
.size(14)
.style(style::Text::Commentary),
]
.spacing(5)
.align_y(Alignment::Center);
let content = column![backend_row, backend_descr, version_row].spacing(10);
#[cfg(feature = "builtin-adb")]
let content = if self.general.adb_backend == AdbBackend::Builtin {
content.push(
button_primary(text("Stop system ADB and retry"))
.on_press(Message::RetryBuiltinAdb),
)
} else {
content
};
container(content)
.padding(10)
.width(Length::Fill)
.height(Length::Shrink)
.style(style::Container::Frame)
.into()
}
fn warning_container(phone: &Phone) -> Element<'static, Message, Theme, Renderer> { fn warning_container(phone: &Phone) -> Element<'static, Message, Theme, Renderer> {
container( container(
row![ row![
@ -691,4 +872,86 @@ impl Settings {
.on_blur(Message::ModalHide) .on_blur(Message::ModalHide)
.into() .into()
} }
fn render_adb_server_confirmation_modal(
content: Element<'_, Message, Theme, Renderer>,
) -> Element<'_, Message, Theme, Renderer> {
let title = container(
row![text("Stop the system ADB server?").size(24)].align_y(Alignment::Center),
)
.width(Length::Fill)
.style(style::Container::Frame)
.padding([10, 0])
.center_x(Length::Shrink);
let explanation = text(
"Builtin ADB needs exclusive access to the USB device. The system ADB server may \
currently hold that access. Stop it and retry with Builtin ADB?",
)
.width(Length::Fill);
let actions = row![
button(text("Cancel")).on_press(Message::ModalHide),
Space::new().width(Length::Fill),
button_primary(text("Stop server and retry"))
.on_press(Message::ConfirmAdbBackendSwitch),
]
.spacing(10);
let modal = container(column![title, explanation, actions].spacing(20))
.width(520)
.padding(20)
.style(style::Container::Frame);
Modal::new(content, modal)
.on_blur(Message::ModalHide)
.into()
}
fn render_adb_server_stopping_modal(
content: Element<'_, Message, Theme, Renderer>,
) -> Element<'_, Message, Theme, Renderer> {
let modal = container(
column![
text("Stopping the system ADB server").size(24),
text("Please wait before reconnecting with Builtin ADB."),
]
.spacing(20),
)
.width(520)
.padding(20)
.style(style::Container::Frame);
Modal::new(content, modal).into()
}
fn render_adb_backend_error_modal<'a>(
&'a self,
content: Element<'a, Message, Theme, Renderer>,
) -> Element<'a, Message, Theme, Renderer> {
let message = self
.adb_backend_error
.as_deref()
.unwrap_or("Could not switch ADB backends.");
let actions = row![
Space::new().width(Length::Fill),
button(text("Close")).on_press(Message::ModalHide),
Space::new().width(Length::Fill),
];
let modal = container(
column![
text("ADB backend switch failed").size(24),
text(message),
actions,
]
.spacing(20),
)
.width(520)
.padding(20)
.style(style::Container::Frame);
Modal::new(content, modal)
.on_blur(Message::ModalHide)
.into()
}
} }

View file

@ -98,7 +98,10 @@ pub fn nav_menu<'a>(
.gap(4); .gap(4);
let device_list_text = match apps_view.loading_state { let device_list_text = match apps_view.loading_state {
ListLoadingState::FindingPhones => text("Finding connected devices..."), ListLoadingState::FindingPhones => apps_view.discovery_issue.as_ref().map_or_else(
|| text("Finding connected devices..."),
|issue| text(issue.to_string()),
),
_ => text("No devices/emulators found"), _ => text("No devices/emulators found"),
}; };

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1776949667, "lastModified": 1781607440,
"narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=", "narHash": "sha256-rxO+uc/KFbSJp+pgyXRuAX6QlG9hJdnt0BXpEQRXY+U=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30", "rev": "3e41b24abd260e8f71dbe2f5737d24122f972158",
"type": "github" "type": "github"
}, },
"original": { "original": {