diff --git a/src/core/config.rs b/src/core/config.rs index b23b765..c9f59de 100644 --- a/src/core/config.rs +++ b/src/core/config.rs @@ -1,8 +1,5 @@ use crate::core::utils::DisplayablePath; -use crate::core::{ - sync::{get_android_sdk, User}, - theme::Theme, -}; +use crate::core::{sync::User, theme::Theme}; use crate::gui::views::settings::Settings; use crate::CACHE_DIR; use crate::CONFIG_DIR; @@ -36,6 +33,7 @@ pub struct BackupSettings { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct DeviceSettings { + /// Unique serial identifier pub device_id: String, pub disable_mode: bool, pub multi_user_mode: bool, @@ -57,7 +55,7 @@ impl Default for DeviceSettings { fn default() -> Self { Self { device_id: String::default(), - multi_user_mode: get_android_sdk() > 21, + multi_user_mode: false, disable_mode: false, backup: BackupSettings::default(), } diff --git a/src/core/sync.rs b/src/core/sync.rs index 4fb2ff8..1dcd545 100644 --- a/src/core/sync.rs +++ b/src/core/sync.rs @@ -1,5 +1,4 @@ use crate::core::uad_lists::PackageState; -use crate::core::utils::set_adb_serial; use crate::gui::views::list::PackageInfo; use crate::gui::widgets::package_row::PackageRow; use regex::Regex; @@ -62,35 +61,37 @@ impl std::fmt::Display for User { } } -pub fn adb_shell_command(shell: bool, args: &str) -> Result { - let adb_command = if shell { - vec!["shell", args] - } else { - vec![args] +pub fn adb_shell_command(shell: bool, serial: Option<&str>, args: &str) -> Result { + // this could be a `tinyvec` or `arrayvec` + let mut adb_args = Vec::with_capacity(4); + if let Some(s) = serial { + adb_args.extend(["-s", s]); }; + if shell { + adb_args.push("shell"); + } + // the rest + adb_args.push(args); let mut command = Command::new("adb"); - command.args(adb_command); + command.args(adb_args); #[cfg(target_os = "windows")] - let command = command.creation_flags(0x08000000); // do not open a cmd window + let command = command.creation_flags(0x0800_0000); // do not open a cmd window match command.output() { Err(e) => { error!("ADB: {}", e); - Err("ADB was not found".to_string()) + Err("Cannot run ADB, likely not found".to_string()) } Ok(o) => { + let stdout = String::from_utf8(o.stdout) + .map_err(|e| e.to_string())? + .trim_end() + .to_string(); if o.status.success() { - Ok(String::from_utf8(o.stdout) - .map_err(|e| e.to_string())? - .trim_end() - .to_string()) + Ok(stdout) } else { - let stdout = String::from_utf8(o.stdout) - .map_err(|e| e.to_string())? - .trim_end() - .to_string(); let stderr = String::from_utf8(o.stderr) .map_err(|e| e.to_string())? .trim_end() @@ -116,7 +117,9 @@ pub enum AdbError { Generic(String), } -pub async fn perform_adb_commands( +/// Runs a shell command on the device. +pub async fn android_sh_cmd>( + device_serial: S, action: String, command_type: CommandType, ) -> Result { @@ -125,7 +128,7 @@ pub async fn perform_adb_commands( CommandType::Shell => "Shell", }; - match adb_shell_command(true, &action) { + match adb_shell_command(true, Some(device_serial.as_ref()), &action) { Ok(o) => { // On old devices, adb commands can return the `0` exit code even if there // is an error. On Android 4.4, ADB doesn't check if the package exists. @@ -155,15 +158,20 @@ pub fn user_flag(user_id: Option<&User>) -> String { .unwrap_or_default() } -pub fn list_all_system_packages(user_id: Option<&User>) -> String { +/// installed and uninstalled packages +pub fn list_all_system_packages(device_serial: &str, user_id: Option<&User>) -> String { let action = format!("{PM_LIST_PACKS} -s -u{}", user_flag(user_id)); - adb_shell_command(true, &action) + adb_shell_command(true, Some(device_serial), &action) .unwrap_or_default() .replace("package:", "") } -pub fn hashset_system_packages(state: PackageState, user_id: Option<&User>) -> HashSet { +pub fn hashset_system_packages( + state: PackageState, + device_serial: &str, + user_id: Option<&User>, +) -> HashSet { let user = user_flag(user_id); let action = match state { PackageState::Enabled => format!("{PM_LIST_PACKS} -s -e{user}"), @@ -171,7 +179,7 @@ pub fn hashset_system_packages(state: PackageState, user_id: Option<&User>) -> H _ => String::default(), // You probably don't need to use this function for anything else }; - adb_shell_command(true, &action) + adb_shell_command(true, Some(device_serial), &action) .unwrap_or_default() .replace("package:", "") .lines() @@ -216,45 +224,44 @@ pub fn apply_pkg_state_commands( package: &CorePackage, wanted_state: PackageState, selected_user: &User, - phone: &Device, + dev: &Device, ) -> Vec { // https://github.com/Universal-Debloater-Alliance/universal-android-debloater/wiki/ADB-reference // ALWAYS PUT THE COMMAND THAT CHANGES THE PACKAGE STATE FIRST! let commands = match wanted_state { - PackageState::Enabled => { - match package.state { - PackageState::Disabled => match phone.android_sdk { - i if i >= 23 => vec!["pm enable"], - _ => vec!["pm enable"], - }, - PackageState::Uninstalled => match phone.android_sdk { - i if i >= 23 => vec!["cmd package install-existing"], - 21 | 22 => vec!["pm unhide"], - 19 | 20 => vec!["pm unblock", PM_CLEAR_PACK], - _ => vec![], // Impossible action already prevented by the GUI - }, - _ => vec![], - } - } + PackageState::Enabled => match package.state { + PackageState::Disabled => match dev.android_sdk { + i if i >= 23 => vec!["pm enable"], + _ => vec!["pm enable"], + }, + PackageState::Uninstalled => match dev.android_sdk { + i if i >= 23 => vec!["cmd package install-existing"], + 21 | 22 => vec!["pm unhide"], + 19 | 20 => vec!["pm unblock", PM_CLEAR_PACK], + _ => unreachable!("already prevented by the GUI"), + }, + _ => vec![], + }, PackageState::Disabled => match package.state { - PackageState::Uninstalled | PackageState::Enabled => match phone.android_sdk { + PackageState::Uninstalled | PackageState::Enabled => match dev.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 { + PackageState::Enabled | PackageState::Disabled => match dev.android_sdk { sdk if sdk >= 23 => vec!["pm uninstall"], // > Android Marshmallow (6.0) 21 | 22 => vec!["pm hide", PM_CLEAR_PACK], // Android Lollipop (5.x) - 19 | 20 => vec!["pm block", PM_CLEAR_PACK], // Android KitKat (4.4/4.4W) + 19 | 20 => vec!["pm block", PM_CLEAR_PACK], // Android KitKat (4.4/4.4W) and older _ => vec!["pm block", PM_CLEAR_PACK], // Disable mode is unavailable on older devices because the specific ADB commands need root }, _ => vec![], }, PackageState::All => vec![], - }; - let user = (phone.android_sdk >= 21).then_some(selected_user); + }; // this should be a `tinyvec`, as `len <= 4` + + let user = supports_multi_user(dev).then_some(selected_user); request_builder(&commands, &package.name, user) } @@ -269,9 +276,9 @@ pub fn request_builder(commands: &[&str], package: &str, user: Option<&User>) -> .collect() } -/// Get the current device model by querying the `ro.product.model` property. -pub fn get_phone_model() -> String { - adb_shell_command(true, "getprop ro.product.model").unwrap_or_else(|err| { +/// Get the model by querying the `ro.product.model` property. +pub fn get_device_model(serial: &str) -> String { + adb_shell_command(true, Some(serial), "getprop ro.product.model").unwrap_or_else(|err| { println!("ERROR: {err}"); if err.contains("adb: no devices/emulators found") { "no devices/emulators found".to_string() @@ -281,40 +288,58 @@ pub fn get_phone_model() -> String { }) } -/// Get the current device Android SDK version by querying the +/// Get Android SDK version by querying the // `ro.build.version.sdk` property or defaulting to 0. -pub fn get_android_sdk() -> u8 { - adb_shell_command(true, "getprop ro.build.version.sdk").map_or(0, |sdk| sdk.parse().unwrap()) +pub fn get_android_sdk(device_serial: &str) -> u8 { + adb_shell_command(true, Some(device_serial), "getprop ro.build.version.sdk").map_or(0, |sdk| { + sdk.parse().expect("SDK version numeral must be valid") + }) } -/// Get the current device brand by querying the `ro.product.brand` property. -pub fn get_phone_brand() -> String { +/// Get the brand by querying the `ro.product.brand` property. +pub fn get_device_brand(serial: &str) -> String { format!( "{} {}", - adb_shell_command(true, "getprop ro.product.brand") + adb_shell_command(true, Some(serial), "getprop ro.product.brand") .map(|s| s.trim().to_string()) .unwrap_or_default(), - get_phone_model() + get_device_model(serial) ) } +/// Minimum inclusive Android SDK version +/// that supports multi-user mode. +/// Lollipop 5.0 +pub const MULTI_USER_SDK: u8 = 21; + +/// Check if it supports multi-user mode, by comparing SDK version. +#[must_use] +pub const fn supports_multi_user(dev: &Device) -> bool { + dev.android_sdk >= MULTI_USER_SDK +} + /// Check if a `user_id` is protected on a device by trying /// to list associated packages. -pub fn is_protected_user(user_id: &str) -> bool { - adb_shell_command(true, &format!("{PM_LIST_PACKS} -s --user {user_id}")).is_err() +pub fn is_protected_user(user_id: &str, device_serial: &str) -> bool { + adb_shell_command( + true, + Some(device_serial), + &format!("{PM_LIST_PACKS} -s --user {user_id}"), + ) + .is_err() } -pub fn get_user_list() -> Vec { +pub fn get_user_list(device_serial: &str) -> Vec { #[dynamic] static RE: Regex = Regex::new(r"\{([0-9]+)").unwrap_or_else(|_| unreachable!()); - adb_shell_command(true, "pm list users") + adb_shell_command(true, Some(device_serial), "pm list 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..]), + protected: is_protected_user(&u.as_str()[1..], device_serial), }) .collect() }) @@ -325,22 +350,19 @@ pub fn get_user_list() -> Vec { pub async fn get_devices_list() -> Vec { retry( Fixed::from_millis(500).take(120), - || match adb_shell_command(false, "devices") { + || match adb_shell_command(false, None, "devices") { Ok(devices) => { let mut device_list: Vec = vec![]; if !RE.is_match(&devices) { return OperationResult::Retry(vec![]); } for device in RE.captures_iter(&devices) { - #[allow(unsafe_code)] - unsafe { - set_adb_serial(&device[1]) - }; + let serial = &device[1]; device_list.push(Device { - model: get_phone_brand(), - android_sdk: get_android_sdk(), - user_list: get_user_list(), - adb_id: device[1].to_string(), + model: get_device_brand(serial), + android_sdk: get_android_sdk(serial), + user_list: get_user_list(serial), + adb_id: serial.to_string(), }); } OperationResult::Ok(device_list) @@ -356,7 +378,7 @@ pub async fn get_devices_list() -> Vec { } pub async fn initial_load() -> bool { - match adb_shell_command(false, "devices") { + match adb_shell_command(false, None, "devices") { Ok(_devices) => true, Err(_err) => false, } diff --git a/src/core/utils.rs b/src/core/utils.rs index 871798d..e1e5426 100644 --- a/src/core/utils.rs +++ b/src/core/utils.rs @@ -5,7 +5,6 @@ use crate::gui::widgets::package_row::PackageRow; use chrono::offset::Utc; use chrono::{DateTime, Local}; use csv::Writer; -use std::ffi::OsStr; use std::path::PathBuf; use std::process::Command; use std::{fmt, fs}; @@ -20,20 +19,16 @@ pub enum Error { DialogClosed, } -#[allow(unsafe_code)] -#[allow( - clippy::semicolon_if_nothing_returned, - reason = "fn must return whatever `set_var` returns" -)] -pub unsafe fn set_adb_serial>(device_serial: D) { - // https://developer.android.com/tools/variables#adb - std::env::set_var("ANDROID_SERIAL", device_serial) -} - -pub fn fetch_packages(uad_lists: &PackageHashMap, user_id: Option<&User>) -> Vec { - let all_system_packages = list_all_system_packages(user_id); // installed and uninstalled packages - let enabled_system_packages = hashset_system_packages(PackageState::Enabled, user_id); - let disabled_system_packages = hashset_system_packages(PackageState::Disabled, user_id); +pub fn fetch_packages( + uad_lists: &PackageHashMap, + device_serial: &str, + user_id: Option<&User>, +) -> Vec { + let all_system_packages = list_all_system_packages(device_serial, user_id); + let enabled_system_packages = + hashset_system_packages(PackageState::Enabled, device_serial, user_id); + let disabled_system_packages = + hashset_system_packages(PackageState::Disabled, device_serial, user_id); let mut description; let mut uad_list; let mut state; diff --git a/src/gui/mod.rs b/src/gui/mod.rs index a3e7f41..ff4b955 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -2,13 +2,11 @@ pub mod style; pub mod views; pub mod widgets; -use crate::core::sync::{ - get_devices_list, initial_load, perform_adb_commands, CommandType, Device, -}; +use crate::core::sync::{android_sh_cmd, get_devices_list, initial_load, CommandType, Device}; use crate::core::theme::Theme; use crate::core::uad_lists::UadListState; use crate::core::update::{get_latest_release, Release, SelfUpdateState, SelfUpdateStatus}; -use crate::core::utils::{set_adb_serial, string_to_theme, NAME}; +use crate::core::utils::{string_to_theme, NAME}; use iced::advanced::graphics::image::image_rs::ImageFormat; use iced::font; @@ -161,10 +159,15 @@ impl Application for UadGui { } Message::RebootButtonPressed => { self.apps_view = AppsView::default(); + let serial = match &self.selected_device { + Some(d) => d.adb_id.clone(), + _ => String::default(), + }; self.selected_device = None; self.devices_list = vec![]; Command::perform( - perform_adb_commands("reboot".to_string(), CommandType::Shell), + // https://android.stackexchange.com/questions/230256/adb-reboot-vs-adb-shell-reboot + android_sh_cmd(serial, "reboot".to_string(), CommandType::Shell), |_| Message::Nothing, ) } @@ -256,10 +259,6 @@ impl Application for UadGui { Message::DeviceSelected(s_device) => { self.selected_device = Some(s_device.clone()); self.view = View::List; - #[allow(unsafe_code)] - unsafe { - set_adb_serial(s_device.adb_id) - }; info!("{:-^65}", "-"); info!( "ANDROID_SDK: {} | DEVICE: {}", diff --git a/src/gui/views/list.rs b/src/gui/views/list.rs index 832487f..a8bca11 100644 --- a/src/gui/views/list.rs +++ b/src/gui/views/list.rs @@ -1,15 +1,13 @@ use crate::core::config::DeviceSettings; use crate::core::helpers::button_primary; use crate::core::sync::{ - apply_pkg_state_commands, perform_adb_commands, AdbError, CommandType, Device, User, + android_sh_cmd, apply_pkg_state_commands, AdbError, CommandType, Device, User, }; use crate::core::theme::Theme; use crate::core::uad_lists::{ load_debloat_lists, Opposite, PackageHashMap, PackageState, Removal, UadList, UadListState, }; -use crate::core::utils::{ - export_selection, fetch_packages, open_url, set_adb_serial, EXPORT_FILE_NAME, NAME, -}; +use crate::core::utils::{export_selection, fetch_packages, open_url, EXPORT_FILE_NAME, NAME}; use crate::gui::style; use crate::gui::widgets::navigation_menu::ICONS; use std::path::PathBuf; @@ -174,7 +172,11 @@ impl List { self.uad_lists.clone_from(&uad_list); *list_update_state = list_state; Command::perform( - Self::load_packages(uad_list, selected_device.user_list.clone()), + Self::load_packages( + uad_list, + selected_device.adb_id.clone(), + selected_device.user_list.clone(), + ), Message::ApplyFilters, ) } @@ -851,13 +853,18 @@ impl List { .collect(); } #[expect(clippy::unused_async, reason = "1 call-site")] - async fn load_packages(uad_list: PackageHashMap, user_list: Vec) -> Vec> { + async fn load_packages>( + uad_list: PackageHashMap, + device_serial: S, + user_list: Vec, + ) -> Vec> { + let serial = device_serial.as_ref(); if user_list.len() <= 1 { - vec![fetch_packages(&uad_list, None)] + vec![fetch_packages(&uad_list, serial, None)] } else { user_list .iter() - .map(|user| fetch_packages(&uad_list, Some(user))) + .map(|user| fetch_packages(&uad_list, serial, Some(user))) .collect() } } @@ -867,10 +874,6 @@ impl List { let uad_lists = load_debloat_lists(remote); match uad_lists { Ok(list) => { - #[allow(unsafe_code)] - unsafe { - set_adb_serial(device.adb_id.clone()) - }; if device.adb_id.is_empty() { error!("AppsView ready but no phone found"); } @@ -950,7 +953,9 @@ fn build_action_pkg_commands( let pkg = &packages[selection.0][selection.1]; let wanted_state = pkg.state.opposite(settings.disable_mode); - let mut commands = vec![]; + // assume 2 actions per user + let mut commands = Vec::with_capacity(device.user_list.len() * 2); + for u in device.user_list.iter().filter(|&&u| { !u.protected && (packages[u.index][selection.1].selected || settings.multi_user_mode) }) { @@ -971,7 +976,13 @@ fn build_action_pkg_commands( // In the end there is only one package state change // even if we run multiple adb commands commands.push(Command::perform( - perform_adb_commands(action, CommandType::PackageManager(p_info)), + android_sh_cmd( + // this is typically small, + // so it's fine. + device.adb_id.clone(), + action, + CommandType::PackageManager(p_info), + ), if j == 0 { Message::ChangePackageState } else { diff --git a/src/gui/views/settings.rs b/src/gui/views/settings.rs index f6382d5..933df48 100644 --- a/src/gui/views/settings.rs +++ b/src/gui/views/settings.rs @@ -1,11 +1,12 @@ use crate::core::helpers::button_primary; -use crate::core::sync::AdbError; use crate::core::config::{BackupSettings, Config, DeviceSettings, GeneralSettings}; use crate::core::save::{ backup_phone, list_available_backup_user, list_available_backups, restore_backup, }; -use crate::core::sync::{get_android_sdk, perform_adb_commands, CommandType, Device, User}; +use crate::core::sync::{ + get_android_sdk, android_sh_cmd, supports_multi_user, AdbError, CommandType, Device, User, +}; use crate::core::theme::Theme; use crate::core::utils::{ export_packages, open_folder, open_url, string_to_theme, DisplayablePath, @@ -137,7 +138,7 @@ impl Settings { None => { self.device = DeviceSettings { device_id: phone.adb_id.clone(), - multi_user_mode: phone.android_sdk > 21, + multi_user_mode: supports_multi_user(phone), disable_mode: false, backup, } @@ -186,7 +187,8 @@ impl Settings { for command in p.commands.clone() { *nb_running_async_adb_commands += 1; commands.push(Command::perform( - perform_adb_commands( + android_sh_cmd( + phone.adb_id.clone(), command, CommandType::PackageManager(p_info.clone()), ), @@ -195,7 +197,7 @@ impl Settings { } } if r_packages.is_empty() { - if get_android_sdk() == 0 { + if get_android_sdk(&phone.adb_id) == 0 { self.device.backup.backup_state = "Device is not connected".to_string(); } else { self.device.backup.backup_state =