Asynchronous + multi-threaded ADB commands (closes #65) 🚀

Smarter use of iced::Command to run as many tasks as possible concurrently/in parallel

- ADB commands no longer block the interaction with the UI.
- The UI now reacts in real-time to ADB actions (no longer waits for all actions to finish)
- Some refactoring along the way
This commit is contained in:
0x192 2021-12-20 13:59:19 +01:00
commit 608c05a408
No known key found for this signature in database
GPG key ID: 34D27465928A0A1D
4 changed files with 158 additions and 156 deletions

View file

@ -12,14 +12,18 @@ and `Removed`.
### Added
- [[#52](https://github.com/0x192/universal-android-debloater/issues/52)] `uk.co.ee.myee` to the debloat lists (thanks [@lawson58](https://github.com/lawson85)).
- [[#58](https://github.com/0x192/universal-android-debloater/issues/52)] `android` to the debloat lists with the tag `Unsafe`.
- [[#49](https://github.com/0x192/universal-android-debloater/issues/49)] Multi-device support: You are now able to select a device among the list of all ADB connected devices/emulators.
- [[#44](https://github.com/0x192/universal-android-debloater/issues/44)] Persistent settings: Settings (only `theme` for now) are saved to a config file. Its location follows [the standards of the different platforms](https://github.com/dirs-dev/dirs-rs#example).
### Changed
- Review of the package lists recommendations. The `Recommended` debloat list is now safer (less likely to remove something you'd want to keep).
- [[#65](https://github.com/0x192/universal-android-debloater/issues/65)] ADB commands now run in parallel and asynchronously! This means no more UI freeze when performing long/many actions! :rocket:
- UI now updates itself in real time when performing ADB actions (thanks to async & multithreading). Before, it waited for the end of all actions.
### Fixed
- Miscellaneous minor issues in some package descriptions.
- Several bad recommendations.
- [[#50](https://github.com/0x192/universal-android-debloater/issues/50)] Resync button flipping theme back to `Lupin`.
## [0.3] - 2021-10-10

View file

@ -10,6 +10,9 @@ use std::process::Command;
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
#[dynamic]
static RE: Regex = Regex::new(r"\n([[:ascii:]]+)\s+device").unwrap();
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Phone {
pub model: String,
@ -121,15 +124,24 @@ pub fn action_handler(
package: &PackageRow,
phone: &Phone,
settings: &SettingsPhone,
) -> Result<bool, bool> {
let actions: Vec<String> = match package.state {
) -> Vec<String> {
// https://github.com/0x192/universal-android-debloater/wiki/ADB-reference
match package.state {
PackageState::Enabled => match settings.disable_mode {
true => {
if phone.android_sdk < 21 {
// < Android Ice Cream Sandwich (4.0)
if phone.android_sdk < 14 {
vec![
format!("pm disable {}", package.name),
format!("pm clear {}", package.name),
]
}
// < Android Lollipop (5.0)
else if phone.android_sdk < 17 {
vec![
format!("am force-stop {}", package.name),
format!("pm disable-user {}", package.name),
format!("pm clear {}", package.name),
format!("pm hide {}", package.name),
]
} else if settings.multi_user_mode {
phone
@ -137,15 +149,15 @@ pub fn action_handler(
.iter()
.flat_map(|u| {
[
format!("am force-stop --user {} {}", u.id, package.name),
format!("pm disable-user --user {} {}", u.id, package.name),
format!("am force-stop --user {} {}", u.id, package.name),
format!("pm clear --user {} {}", u.id, package.name),
//format!("pm hide --user {} {}", u.id, package.name),
]
})
.collect()
} else {
vec![
format!("am force-stop --user {} {}", selected_user.id, package.name),
format!(
"pm disable-user --user {} {}",
selected_user.id, package.name
@ -209,23 +221,7 @@ pub fn action_handler(
}
}
PackageState::All => vec![], // This can't happen (like... never)
};
for action in actions {
match adb_shell_command(true, &action) {
Ok(_) => {
info!("[{}] {}", package.removal, action);
}
Err(err) => {
if err.contains("[not installed for") {
} else {
error!("[{}] {} -> {}", package.removal, action, err);
return Err(false);
}
}
}
}
Ok(true)
}
pub fn get_phone_model() -> String {
@ -276,15 +272,12 @@ pub fn get_user_list() -> Vec<User> {
}
// getprop ro.serialno
pub fn get_device_list() -> Vec<Phone> {
#[dynamic]
static RE: Regex = Regex::new(r"\n([[:ascii:]]+)\s+device").unwrap();
pub async fn get_device_list() -> Vec<Phone> {
match adb_shell_command(false, "devices") {
Ok(devices) => {
let mut device_list: Vec<Phone> = vec![];
for device in RE.captures_iter(&devices) {
env::set_var("ANDROID_SERIAL", device[1].to_string());
env::set_var("ANDROID_SERIAL", &device[1]);
device_list.push(Phone {
model: get_phone_brand(),

View file

@ -62,7 +62,8 @@ pub enum Message {
AppsAction(AppsMessage),
SettingsAction(SettingsMessage),
RefreshButtonPressed,
Init,
LoadDeviceList(Vec<Phone>),
Init(Vec<Phone>),
}
impl Application for UadGui {
@ -73,21 +74,24 @@ impl Application for UadGui {
fn new(_flags: ()) -> (Self, Command<Message>) {
(
Self::default(),
Command::perform(Self::init(), |_| Message::Init),
Command::perform(get_device_list(), Message::Init),
)
}
fn title(&self) -> String {
String::from("UadGui")
String::from("Universal Android Debloater")
}
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::Init => {
Message::Init(device_list) => {
self.device_list = device_list;
self.selected_device = Some(Phone::default());
Command::perform(Self::refresh(10), Message::LoadDevices)
}
Message::RefreshButtonPressed => {
Message::LoadDeviceList(device_list) => {
self.device_list = device_list;
// Save the current selected device
let i = self
.device_list
@ -97,9 +101,15 @@ impl Application for UadGui {
self.selected_device = Some(Phone::default());
Command::perform(Self::refresh(i), Message::LoadDevices)
}
Message::RefreshButtonPressed => {
self.apps_view.ready = false;
Command::batch([
Command::perform(Self::please_wait(), Message::AppsAction),
Command::perform(Self::device_lists(), Message::LoadDeviceList),
])
}
Message::LoadDevices(last_selected_device) => {
self.settings_view.phone = SettingsPhone::default();
self.device_list = get_device_list();
// Try to reload last selected phone
if !self.device_list.is_empty() {
@ -110,19 +120,12 @@ impl Application for UadGui {
None => Some(Phone::default()),
},
};
env::set_var(
"ANDROID_SERIAL",
self.selected_device.clone().unwrap().adb_id,
);
info!("{:-^65}", "-");
info!(
"ANDROID_SDK: {} | PHONE: {}",
self.selected_device.as_ref().unwrap().android_sdk,
self.selected_device.as_ref().unwrap().model
);
self.apps_view = AppsView::default();
self.view = View::List;
Command::perform(Self::load_phone_packages(), Message::AppsAction)
Command::perform(
Self::load_phone_packages(self.selected_device.clone().unwrap()),
Message::AppsAction,
)
} else {
self.selected_device = None;
Command::none()
@ -154,19 +157,12 @@ impl Application for UadGui {
}
Message::DeviceSelected(device) => {
self.selected_device = Some(device);
env::set_var(
"ANDROID_SERIAL",
self.selected_device.clone().unwrap().adb_id,
);
info!("{:-^65}", "-");
info!(
"ANDROID_SDK: {} | PHONE: {}",
self.selected_device.as_ref().unwrap().android_sdk,
self.selected_device.as_ref().unwrap().model
);
self.apps_view = AppsView::default();
self.view = View::List;
Command::perform(Self::load_phone_packages(), Message::AppsAction)
Command::perform(
Self::load_phone_packages(self.selected_device.clone().unwrap()),
Message::AppsAction,
)
}
}
}
@ -267,17 +263,25 @@ impl UadGui {
Self::run(settings).unwrap_err();
}
pub async fn load_phone_packages() -> AppsMessage {
pub async fn load_phone_packages(phone: Phone) -> AppsMessage {
env::set_var("ANDROID_SERIAL", phone.adb_id);
info!("{:-^65}", "-");
info!(
"ANDROID_SDK: {} | PHONE: {}",
phone.android_sdk, phone.model
);
AppsMessage::LoadPackages
}
pub async fn init() -> Message {
Message::Init
}
pub async fn refresh(i: usize) -> usize {
i
}
pub async fn device_lists() -> Vec<Phone> {
get_device_list().await
}
pub async fn please_wait() -> AppsMessage {
AppsMessage::Nothing
}
}
fn refresh_icon() -> Text {

View file

@ -1,4 +1,4 @@
use crate::core::sync::{action_handler, Phone, User};
use crate::core::sync::{action_handler, adb_shell_command, Phone, User};
use crate::core::uad_lists::{
load_debloat_lists, Opposite, Package, PackageState, Removal, UadList,
};
@ -51,7 +51,7 @@ pub enum Action {
#[derive(Default, Debug, Clone)]
pub struct List {
ready: bool,
pub ready: bool,
phone_packages: Vec<Vec<PackageRow>>, // packages of all users of the phone
filtered_packages: Vec<usize>, // phone_packages indexes of the selected user (= what you see on screen)
pub selection: Selection,
@ -87,6 +87,9 @@ pub enum Message {
ExportSelectionPressed,
List(usize, RowMessage),
ExportedSelection(Result<bool, String>),
ActionIsDone(usize),
PackagesLoaded(Vec<Vec<PackageRow>>),
Nothing,
}
impl List {
@ -98,21 +101,19 @@ impl List {
) -> Command<Message> {
let i_user = &self.selected_user.unwrap_or(User { id: 0, index: 0 }).index; // for readability
match message {
Message::Nothing => Command::none(),
Message::LoadPackages => {
self.selected_package_state = Some(PackageState::Enabled);
self.selected_list = Some(UadList::All);
self.selected_removal = Some(Removal::Recommended);
self.selected_user = Some(User { id: 0, index: 0 });
match phone.user_list.len() {
0 | 1 => self.phone_packages.push(fetch_packages(&UAD_LISTS, &None)),
_ => {
for user in &phone.user_list {
self.phone_packages
.push(fetch_packages(&UAD_LISTS, &Some(user)));
}
}
}
Command::perform(
Self::load_packages(phone.user_list.clone()),
Message::PackagesLoaded,
)
}
Message::PackagesLoaded(packages) => {
self.phone_packages = packages;
self.filtered_packages = (0..self.phone_packages[*i_user].len()).collect();
Self::filter_package_lists(self);
@ -148,79 +149,48 @@ impl List {
.update(row_message.clone())
.map(move |row_message| Message::List(i, row_message));
let package = &mut self.phone_packages[*i_user][i];
match row_message {
RowMessage::ToggleSelection(toggle) => {
if self.phone_packages[*i_user][i].removal == Removal::Unsafe
&& !settings.expert_mode
{
self.phone_packages[*i_user][i].selected = false;
if package.removal == Removal::Unsafe && !settings.expert_mode {
package.selected = false;
} else {
self.phone_packages[*i_user][i].selected = toggle;
package.selected = toggle;
if self.phone_packages[*i_user][i].selected {
if package.selected {
self.selection.selected_packages.push(i);
update_selection_count(
&mut self.selection,
self.phone_packages[*i_user][i].state,
true,
);
} else {
self.selection
.selected_packages
.drain_filter(|s_i| *s_i == i);
update_selection_count(
&mut self.selection,
self.phone_packages[*i_user][i].state,
false,
);
}
update_selection_count(
&mut self.selection,
package.state,
package.selected,
);
}
Command::none()
}
RowMessage::ActionPressed => {
let success = action_handler(
&self.selected_user.unwrap(),
&self.phone_packages[*i_user][i],
phone,
settings,
)
.unwrap_or_else(|err| err);
let mut commands = vec![];
let actions =
action_handler(&self.selected_user.unwrap(), package, phone, settings);
if success {
let i = self.phone_packages[*i_user]
.iter()
.position(|p| p.name == self.phone_packages[*i_user][i].name)
.unwrap();
if self.phone_packages[*i_user][i].selected {
update_selection_count(
&mut self.selection,
self.phone_packages[*i_user][i].state,
false,
);
}
self.phone_packages[*i_user][i].state = self.phone_packages[*i_user][i]
.state
.opposite(settings.disable_mode);
if settings.multi_user_mode {
for u in &phone.user_list {
self.phone_packages[u.index][i].state =
self.phone_packages[*i_user][i].state;
self.phone_packages[u.index][i].selected = false;
}
}
self.selection
.selected_packages
.drain_filter(|s_i| *s_i == i);
self.phone_packages[*i_user][i].selected = false;
Self::filter_package_lists(self);
for action in actions {
commands.push(Command::perform(
Self::perform_commands(action, i, package.removal),
Message::ActionIsDone,
));
}
Command::batch(commands)
}
RowMessage::PackagePressed => {
self.description = self.phone_packages[*i_user][i].clone().description;
self.description = package.clone().description;
Command::none()
}
}
Command::none()
}
Message::ApplyActionOnSelection(action) => {
let mut selected_packages = self.selection.selected_packages.clone();
@ -237,42 +207,26 @@ impl List {
});
}
}
let mut commands = vec![];
for i in selected_packages {
let success = action_handler(
let actions = action_handler(
&self.selected_user.unwrap(),
&self.phone_packages[*i_user][i],
phone,
settings,
)
.unwrap_or_else(|err| err);
if success {
update_selection_count(
&mut self.selection,
self.phone_packages[*i_user][i].state,
false,
);
if !settings.multi_user_mode {
self.phone_packages[*i_user][i].state = self.phone_packages[*i_user][i]
.state
.opposite(settings.disable_mode);
self.phone_packages[*i_user][i].selected = false;
}
for u in &phone.user_list {
self.phone_packages[u.index][i].state = self.phone_packages[u.index][i]
.state
.opposite(settings.disable_mode);
self.phone_packages[u.index][i].selected = false;
}
self.selection
.selected_packages
.drain_filter(|s_i| *s_i == i);
);
for action in actions {
commands.push(Command::perform(
Self::perform_commands(
action,
i,
self.phone_packages[*i_user][i].removal,
),
Message::ActionIsDone,
));
}
}
Self::filter_package_lists(self);
Command::none()
Command::batch(commands)
}
Message::SelectAllPressed => {
for i in self.filtered_packages.clone() {
@ -311,6 +265,28 @@ impl List {
Self::filter_package_lists(self);
Command::none()
}
Message::ActionIsDone(i) => {
let package = &mut self.phone_packages[*i_user][i];
update_selection_count(&mut self.selection, package.state, false);
if !settings.multi_user_mode {
package.state = package.state.opposite(settings.disable_mode);
package.selected = false;
} else {
for u in &phone.user_list {
self.phone_packages[u.index][i].state = self.phone_packages[u.index][i]
.state
.opposite(settings.disable_mode);
self.phone_packages[u.index][i].selected = false;
}
}
self.selection
.selected_packages
.drain_filter(|s_i| *s_i == i);
Self::filter_package_lists(self);
Command::none()
}
}
}
@ -510,6 +486,31 @@ impl List {
.map(|(i, _)| i)
.collect();
}
async fn perform_commands(action: String, i: usize, recommendation: Removal) -> usize {
match adb_shell_command(true, &action) {
Ok(_) => info!("[{}] {}", recommendation, action),
Err(err) => {
if !err.contains("[not installed for") {
error!("[{}] {} -> {}", recommendation, action, err);
}
}
};
i
}
async fn load_packages(user_list: Vec<User>) -> Vec<Vec<PackageRow>> {
let mut temp = vec![];
match user_list.len() {
0 | 1 => temp.push(fetch_packages(&UAD_LISTS, &None)),
_ => {
for user in &user_list {
temp.push(fetch_packages(&UAD_LISTS, &Some(user)));
}
}
}
temp
}
}
fn loading_data<'a>(settings: &Settings) -> Element<'a, Message> {