diff --git a/src/core/adb.rs b/src/core/adb.rs index 59e13a3..1078dbf 100644 --- a/src/core/adb.rs +++ b/src/core/adb.rs @@ -19,8 +19,9 @@ //! //! Despite being "low-level", we can still "have cake and eat it too"; //! After all, what's the point of an abstraction if it doesn't come with goodies?: -//! We can take some freedoms, such as: -//! - pre-parsing or validanting output, to provide types with invariants. +//! We can reserve some artistic license, such as: +//! - shorter names, complemented by context +//! - pre-parsing or validanting output, to provide types with invariants //! - strongly-typed rather than "stringly-typed" APIs //! - nicer IDE support //! - compile-time prevention of malformed cmds @@ -77,14 +78,31 @@ impl Cmd { self.0.arg("shell"); ShCmd(self) } - /// List all detected devices: + /// List attached devices (as serials) and their status: /// - USB /// - TCP/IP: WIFI, Ethernet, etc... /// - Local emulators - /// Some may not be authorized by the user (yet) - pub fn devices(mut self) -> Result { + /// Status can be (but not limited to): + /// - "unauthorized" + /// - "device" + pub fn devices(mut self) -> Result, String> { self.0.arg("devices"); - self.run() + Ok(self + .run()? + .lines() + .skip(1) // header + .map(|dev_stat| { + let tab_idx = dev_stat + .find('\t') + .expect("There must be 1 tab after serial"); + ( + // serial + dev_stat[..tab_idx].to_string(), + // status + dev_stat[(tab_idx + 1)..].to_string(), + ) + }) + .collect()) } /// Reboots default device pub fn reboot(mut self) -> Result { @@ -182,9 +200,9 @@ impl ToString for PmLsPackFlag { } } -pub const PACK_URI_SCHEME: &str = "package:"; +pub const PACK_PREFIX: &str = "package:"; #[expect(clippy::cast_possible_truncation, reason = "")] -pub const PACK_URI_LEN: u8 = PACK_URI_SCHEME.len() as _; +pub const PACK_URI_LEN: u8 = PACK_PREFIX.len() as _; pub const PM_LIST_PACKS: &str = "pm list packages"; pub const PM_CLEAR_PACK: &str = "pm clear"; @@ -195,7 +213,8 @@ pub const PM_CLEAR_PACK: &str = "pm clear"; #[derive(Debug)] pub struct PmCmd(ShCmd); impl PmCmd { - /// `list packages` sub-command + /// `list packages` sub-command, + /// stripped of "package:" prefix pub fn ls_packs( mut self, f: Option, @@ -214,13 +233,14 @@ impl PmCmd { pack_ls .lines() .map(|p_ln| { - debug_assert!(p_ln.starts_with(PACK_URI_SCHEME)); + debug_assert!(p_ln.starts_with(PACK_PREFIX)); String::from(&p_ln[PACK_URI_LEN as usize..]) }) .collect() }) } - /// `list packages` sub-command, but pre-validated + /// `list packages` sub-command, pre-validated. + /// This is strongly-typed, at the cost of regex overhead. pub fn ls_packs_valid( self, f: Option, @@ -230,9 +250,12 @@ impl PmCmd { .into_iter() .map(|p| PackId::new(p).expect("One of these is wrong: `PackId` regex, ADB implementation. Or the spec now allows a wider char-set")).collect()) } - /// `list users` sub-command - pub fn ls_users(mut self) -> Result { + #[allow(clippy::doc_markdown, reason = "Multi URL")] + /// `list users` sub-command. + /// - 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 ls_users(mut self) -> Result, String> { self.0 .0 .0.args(["list", "users"]); - self.0 .0.run() + Ok(self.0 .0.run()?.lines().skip(1).map(String::from).collect()) } } diff --git a/src/core/sync.rs b/src/core/sync.rs index 0b1102e..3c133ce 100644 --- a/src/core/sync.rs +++ b/src/core/sync.rs @@ -7,8 +7,7 @@ use regex::Regex; use retry::{delay::Fixed, retry, OperationResult}; use serde::{Deserialize, Serialize}; use static_init::dynamic; -use std::collections::HashSet; -use std::process::Command; +use std::{collections::HashSet, process::Command}; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; @@ -327,45 +326,50 @@ pub fn is_protected_user(user_id: &str, device_serial: &str) -> bool { } /// `pm list users` parsed into a vec with extra info. -pub fn list_users_parsed(device_serial: &str) -> Vec { +pub fn ls_users_parsed(device_serial: &str) -> Vec { #[dynamic] static RE: Regex = Regex::new(r"\{([0-9]+)").unwrap_or_else(|_| unreachable!()); + AdbCmd::new() .sh(device_serial) .pm() .ls_users() - .map(|users| { - RE.find_iter(&users) - .enumerate() - .map(|(i, u)| User { - id: u.as_str()[1..].parse().unwrap(), - index: i, - protected: is_protected_user(&u.as_str()[1..], device_serial), - }) - .collect() - }) + // if default, then empty iter, which becomes empty vec (again) .unwrap_or_default() + .into_iter() + .enumerate() + .map(|(i, user)| { + // It seems each line is a user, + // optionally associated with a work-profile. + // This will ignore the work-profiles! + let u = &RE.captures(&user).expect("Each user should have an ID")[1]; + User { + id: u + .parse() + .unwrap_or_else(|_| unreachable!("User ID must be valid `u16`")), + index: i, + protected: is_protected_user(u, device_serial), + } + }) + .collect() } -// getprop ro.serialno +/// This matches serials (`getprop ro.serialno`) +/// that are authorized by the user. pub async fn get_devices_list() -> Vec { - /// This matches serials that are authorized by the user. - #[dynamic] - static RE: Regex = Regex::new(r"\n(\S+)\s+device").unwrap_or_else(|_| unreachable!()); - retry(Fixed::from_millis(500).take(120), || { match AdbCmd::new().devices() { Ok(devices) => { let mut device_list: Vec = vec![]; - if !RE.is_match(&devices) { + if devices.iter().all(|(_, stat)| stat != "device") { return OperationResult::Retry(vec![]); } - for device in RE.captures_iter(&devices) { - let serial = &device[1]; + for device in devices { + let serial = &device.0; device_list.push(Device { model: get_device_brand(serial), android_sdk: get_android_sdk(serial), - user_list: list_users_parsed(serial), + user_list: ls_users_parsed(serial), adb_id: serial.to_string(), }); }