Compare commits

..

2 commits

Author SHA1 Message Date
Angel J
528ef586da
feat(adb): gate built-in backend behind default feature 2026-07-14 19:57:09 -07:00
iamanaws
6c851d1112
feat(adb): add built-in Rust ADB backend 2026-07-14 19:57:05 -07:00
19 changed files with 1239 additions and 462 deletions

538
Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -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<dyn std::error::Error>> {
println_or_exit!("Scanning for connected devices...");
let devices = get_devices_list();
pub fn list_devices(backend: AdbBackend) -> Result<(), Box<dyn std::error::Error>> {
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<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
pub fn list_packages(
backend: AdbBackend,
device: Option<String>,
state_filter: Option<StateFilter>,
removal_filter: Option<RemovalFilter>,
@ -113,7 +122,7 @@ pub fn list_packages(
search: Option<String>,
user_id: Option<u16>,
) -> Result<(), Box<dyn std::error::Error>> {
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<u16>,
context: &PackageListContext,
backend: AdbBackend,
) -> Result<usize, Box<dyn std::error::Error>> {
let config = context.display_config();
let mut displayed_count = 0;
let enabled_packages: HashSet<String> = ACommand::new()
let enabled_packages: HashSet<String> = 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<String> = ACommand::new()
let disabled_packages: HashSet<String> = 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<String>,
user_id: Option<u16>,
@ -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<String, Package>,
) -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
// Capture the before-state of packages on other users for cross-user detection
let before_cross_user_states =
uad_core::sync::capture_cross_user_states(package, &device.adb_id, user.id, device);
// Execute commands
for cmd in commands {
match run_adb_shell_action(&device.adb_id, cmd.as_str()) {
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<String>,
user_id: Option<u16>,
@ -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<dyn std::error::Error>> {
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())
}
}
}

View file

@ -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<String>) -> Result<Phone, Box<dyn std::error::Error>> {
let devices = get_devices_list();
pub fn get_target_device(
backend: AdbBackend,
device: Option<String>,
) -> Result<Phone, Box<dyn std::error::Error>> {
let devices = get_devices_list(backend);
if devices.is_empty() {
return Err(NO_DEVICES_FOUND.into());

View file

@ -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,42 @@ 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)
#[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)]
#[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 if enabled
#[arg(
short = 'B',
long = "backend",
value_enum,
global = true,
default_value = "system"
)]
backend: AdbBackendArg,
#[command(subcommand)]
command: Commands,
}
@ -136,6 +168,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 +204,11 @@ async fn main() -> ExitCode {
fn run() -> Result<(), Box<dyn std::error::Error>> {
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 +218,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
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 +227,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
dry_run,
} => {
commands::change_package_state(
backend,
&packages,
device,
user,
@ -206,6 +243,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
dry_run,
} => {
commands::change_package_state(
backend,
&packages,
device,
user,
@ -221,6 +259,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
dry_run,
} => {
commands::change_package_state(
backend,
&packages,
device,
user,
@ -234,16 +273,19 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
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)?;
}
}

View file

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

View file

@ -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<String>,
user_id: Option<u16>,
) -> Result<(), Box<dyn std::error::Error>> {
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<u16>,
uad_lists: &HashMap<String, Package>,
backend: AdbBackend,
) -> Result<ReplAction, Box<dyn std::error::Error>> {
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<u16>,
uad_lists: &HashMap<String, Package>,
backend: AdbBackend,
) -> Result<(), Box<dyn std::error::Error>> {
let parsed = ReplListArgs::parse(args)?;
let pm_flag = parsed.state_filter.and_then(StateFilter::to_pm_flag);
let system_packages = ACommand::new()
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<String, Package>,
backend: AdbBackend,
) -> Result<(), Box<dyn std::error::Error>> {
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<String, Package>,
backend: AdbBackend,
) -> Result<(), Box<dyn std::error::Error>> {
let current_state = get_package_state(&device.adb_id, pkg_name, Some(user.id))
.ok_or("Package not found on device")?;
@ -346,6 +372,7 @@ fn process_package_change(
user,
device,
&commands,
backend,
" ",
)
}

View file

@ -11,10 +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, optional = true }
serde.workspace = true
serde_json.workspace = true
log.workspace = true

View file

@ -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 the optional `adb_client` backend or the system ADB CLI,
//! which implies:
//! - no "magic"
//! - no custom commands
@ -33,18 +32,33 @@
//! 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/)
#[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;
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
use crate::utils::is_all_w_c;
use log::{error, info, warn};
use log::{error, info};
/// Convert ADB output bytes to a trimmed UTF-8 string.
/// Uses lossy conversion to prevent panics on non-UTF8 output from certain OEMs.
@ -53,25 +67,90 @@ 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.
#[cfg(feature = "builtin-adb")]
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
#[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)"),
}
}
}
#[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<String>,
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 +158,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 +183,8 @@ impl ACommand {
pub fn shell<S: AsRef<str>>(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 +196,195 @@ impl ACommand {
/// Status can be (but not limited to):
/// - "unauthorized"
/// - "device"
pub fn devices(mut self) -> Result<Vec<(String, String)>, 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<Vec<(String, String)>, String> {
match self.0.backend {
#[cfg(feature = "builtin-adb")]
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 <num>.<num>.<num>
/// Version <num>.<num>.<num>-<no spaces>
/// Installed as <ANDROID_SDK_HOME>/platform-tools/adb[.exe]
/// Running on <OS/kernel version> (<CPU arch>)
/// ```
#[expect(clippy::panic_in_result_fn, reason = "Assertions are fine")]
pub fn version(mut self) -> Result<String, String> {
self.0.arg("version");
let out = self.run()?;
pub fn version(self) -> Result<String, String> {
match self.0.backend {
#[cfg(feature = "builtin-adb")]
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`
#[cfg(feature = "builtin-adb")]
fn version_builtin() -> Result<String, String> {
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`
#[cfg(feature = "builtin-adb")]
fn devices_builtin() -> Result<Vec<(String, String)>, 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)
#[cfg(feature = "builtin-adb")]
fn run_shell_command_builtin(&self, shell_command: &str) -> Result<String, String> {
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::<Vec<_>>()
.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<String, String> {
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<String, String> {
let mut cmd = self.0;
/// List devices using the system `adb` binary
fn devices_system() -> Result<Vec<(String, String)>, 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<String, String> {
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<String, String> {
#[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::<Vec<_>>()
.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<String, String> {
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),
}
}
}
@ -227,12 +401,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 +416,26 @@ impl ShellCommand {
/// - etc...
///
/// So to avoid lossy conversions, we return strs
pub fn getprop(mut self, key: &str) -> Result<String, String> {
self.0.0.args(["getprop", key]);
self.0.run()
pub fn getprop(self, key: &str) -> Result<String, String> {
self.0.run_shell_command(&format!("getprop {key}"))
}
/// Reboots device
pub fn reboot(mut self) -> Result<String, String> {
self.0.0.arg("reboot");
self.0.run()
pub fn reboot(self) -> Result<String, String> {
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<String, String> {
self.0.0.arg(action);
self.0.run()
pub fn raw(self, action: &str) -> Result<String, String> {
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 +479,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 +487,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,33 +512,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<PmListPacksFlag>,
self,
flag: Option<PmListPacksFlag>,
user_id: Option<u16>,
) -> Result<Vec<String>, 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()
.filter_map(|p_ln| {
debug_assert!(p_ln.starts_with(PACK_PREFIX));
let p = &p_ln[PACK_PREFIX.len()..];
if PackageId::new(p).is_some() {
Some(String::from(p))
} else {
warn!("skipping nonstandard package name: {p:?}");
None
}
.filter_map(|line| {
let pkg = line.strip_prefix(PACK_PREFIX)?;
debug_assert!(PackageId::new(pkg).is_some());
Some(pkg.to_string())
})
.collect()
})
@ -384,56 +540,22 @@ impl PmCommand {
///
/// - <https://source.android.com/docs/devices/admin/multi-user-testing>
/// - <https://stackoverflow.com/questions/37495126/android-get-list-of-users-and-profile-name>
pub fn list_users(mut self) -> Result<Box<[UserInfo]>, String> {
self.0.0.0.args(["list", "users"]);
pub fn list_users(self) -> Result<Box<[UserInfo]>, String> {
// Expected shape: "UserInfo{<id>:<name>:<flags>}[ 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{<id>:<name>:<flags>}[ running]"
let mut comps = ln.split(':');
let id = comps
.next()
.expect("There must be at least 1 ':'-separated component")
.parse()
.expect("string assumed to be UID numeral");
//let name = comps
// .next()
// .expect("There must be at least 2 ':'-separated components. 2nd is user-name");
//let flags = u32::from_str_radix(
// comps.next().expect(
// "There must be at least 3 ':'-separated components. 3rd is user bit-flags",
// ),
// 16,
//)
//.expect("string assumed to be hexadecimal bit-flags");
UserInfo {
id,
//name: name.into(),
//flags,
//running: run,
}
.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())
}
@ -500,4 +622,16 @@ mod tests {
assert_ne!(PackageId::new(p_id), None);
}
}
#[test]
fn backend_display() {
#[cfg(feature = "builtin-adb")]
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);
}
}

View file

@ -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)]
@ -37,7 +41,7 @@ pub struct BackupSettings {
pub struct DeviceSettings {
/// Unique serial identifier
pub device_id: String,
pub disable_mode: bool, // should be `enum RemovalMode`
pub disable_mode: bool,
pub multi_user_mode: bool,
#[serde(skip)]
pub backup: BackupSettings,
@ -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(),
}
}
}

View file

@ -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<S: AsRef<str>>(
device_serial: S,
action: &str,
) -> Result<String, AdbError> {
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<S: AsRef<str>>(
device_serial: S,
action: &str,
backend: AdbBackend,
) -> Result<String, AdbError> {
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);
@ -180,19 +192,16 @@ pub fn apply_pkg_state_commands(
_ => vec![],
},
PackageState::Disabled => match package.state {
PackageState::Uninstalled | PackageState::Enabled => {
if supports_disabling(phone) {
vec!["pm disable-user", "am force-stop", PM_CLEAR_PACK]
} else {
vec![]
}
}
PackageState::Uninstalled | PackageState::Enabled => match phone.android_sdk {
sdk if sdk >= 23 => vec!["pm disable-user", "am force-stop", PM_CLEAR_PACK],
_ => vec![],
},
_ => vec![],
},
PackageState::Uninstalled => match package.state {
PackageState::Enabled | PackageState::Disabled => match phone.android_sdk {
sdk if sdk >= 23 => vec!["pm uninstall"], // > Marshmallow (6.0)
21 | 22 => vec!["pm hide", PM_CLEAR_PACK], // Lollipop (5.x)
sdk if sdk >= 23 => vec!["pm uninstall"], // > Android Marshmallow (6.0)
21 | 22 => vec!["pm hide", PM_CLEAR_PACK], // Android Lollipop (5.x)
_ => vec!["pm block", PM_CLEAR_PACK], // Disable mode is unavailable on older devices because the specific ADB commands need root
},
_ => vec![],
@ -227,23 +236,48 @@ pub fn request_builder(commands: &[&str], package: &str, user: Option<User>) ->
.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.
///
/// 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.
@ -254,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()
}
@ -269,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
}
})
}
@ -404,11 +444,10 @@ pub fn detect_cross_user_behavior(
}
}
#[must_use]
pub const fn supports_disabling(dev: &Phone) -> bool {
// >= Marshmallow (6.0)
dev.android_sdk >= 23
}
/// Minimum inclusive Android SDK version
/// that supports multi-user mode.
/// Lollipop 5.0
pub const MULTI_USER_SDK: u8 = 21;
/// Check if it might support multi-user mode,
/// by simply comparing SDK version.
@ -419,8 +458,7 @@ pub const fn supports_disabling(dev: &Phone) -> bool {
/// - <https://developer.android.com/reference/android/os/UserManager#supportsMultipleUsers()>
#[must_use]
pub const fn supports_multi_user(dev: &Phone) -> bool {
// >= Lollipop (5.0)
dev.android_sdk >= 21
dev.android_sdk >= MULTI_USER_SDK
}
/// Check if a `user_id` is protected on a device by trying
@ -457,31 +495,39 @@ pub fn list_users_idx_prot(device_serial: &str) -> Vec<User> {
.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<Phone> {
pub fn get_devices_list(backend: AdbBackend) -> Vec<Phone> {
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<Phone> = 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<Phone> = vec![];
OperationResult::Retry(test)
}
@ -491,8 +537,8 @@ pub fn get_devices_list() -> Vec<Phone> {
}
#[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,
}

View file

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

View file

@ -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<Message>) {
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,
)
}

View file

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

View file

@ -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<Removal> for SummaryEntry {
}
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(
&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<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 {
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<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.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<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() {
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<Message> {
self.input_value = letter;
Self::filter_package_lists(self);
Task::none()
self.refilter()
}
fn on_list_selected(&mut self, list: UadList) -> Task<Message> {
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<Message> {
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<Message> {
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<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")]
{
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(

View file

@ -15,11 +15,12 @@ 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::{
AdbError, CorePackage, Phone, User, get_android_sdk, run_adb_shell_action,
supports_disabling, supports_multi_user,
supports_multi_user,
},
utils::{DisplayablePath, Error, NAME, export_packages, generate_backup_name, open_url},
};
@ -35,15 +36,23 @@ pub struct Settings {
pub device: DeviceSettings,
is_loading: bool,
modal: Option<PopUpModal>,
/// 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<String, String>),
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),
@ -113,7 +126,7 @@ impl Settings {
}
fn handle_disable_mode(&mut self, phone: &Phone, toggled: bool) -> Task<Message> {
if supports_disabling(phone) {
if phone.android_sdk >= 23 {
self.device.disable_mode = toggled;
debug!("Config change: {self:?}");
let mut config = Config::load_configuration_file();
@ -138,6 +151,28 @@ impl Settings {
Task::none()
}
fn handle_apply_adb_backend(&mut self, backend: AdbBackend) -> Task<Message> {
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<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> {
open_url(url);
Task::none()
@ -170,9 +205,8 @@ 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,
};
}
@ -361,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),
@ -381,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),
@ -460,6 +498,50 @@ 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);
#[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),
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![
@ -499,14 +581,14 @@ impl Settings {
.size(20)
.style(style::CheckBox::SettingsEnabled);
let disable_checkbox_style = if supports_disabling(phone) {
let disable_checkbox_style = if phone.android_sdk >= 23 {
style::CheckBox::SettingsEnabled
} else {
style::CheckBox::SettingsDisabled
};
let disable_mode_descr =
text("In many cases, it's better to disable a package instead of uninstalling it")
text("In some cases, it can be better to disable a package instead of uninstalling it")
.style(style::Text::Commentary);
let unavailable_btn = button(text("Unavailable").size(14))
@ -523,7 +605,7 @@ impl Settings {
.size(20)
.style(disable_checkbox_style);
let disable_setting_row = if supports_disabling(phone) {
let disable_setting_row = if phone.android_sdk >= 23 {
row![
disable_mode_checkbox,
Space::new().width(Length::Fill).height(Length::Shrink),

6
flake.lock generated
View file

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

View file

@ -23,14 +23,6 @@
"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": "Advanced"
},
"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.",
@ -375,14 +367,6 @@
"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.",
@ -41236,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.\nTry disabling instead of uninstalling in case that happens.",
"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.",
"dependencies": [],
"neededBy": [],
"labels": [],
@ -42228,11 +42212,11 @@
},
"com.android.networkstack.tethering.inprocess": {
"list": "Oem",
"description": "Causes a lockscreen loop on some Xiaomi devices. Provides TetherableWifiRegexs, the lack of which crashes systemui and miui.home.",
"description": "Completely empty package. Useless.",
"dependencies": [],
"neededBy": [],
"labels": [],
"removal": "Unsafe"
"removal": "Recommended"
},
"com.samsung.android.a20.d01.wallpapermulti": {
"list": "Oem",
@ -43147,53 +43131,5 @@
"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 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": [],
"removal": "Advanced"
},
"com.blackview.apkupgrade": {
"list": "Oem",
"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": "Advanced"
},
"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"
},
"com.huawei.aml": {
"list": "Oem",
"description": "Huawei AML\nA hidden app, which has phone, location and SMS permissions.",
"dependencies": [],
"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"
},
"com.softwinner.dragonatt": {
"list": "Oem",
"description": "DragonAtt",
"dependencies": [],
"neededBy": [],
"labels": [],
"removal": "Recommended"
}
}