From 3ca294c4348766df74cd44183576dc309fdc1656 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sat, 2 Mar 2024 01:04:03 +0100 Subject: [PATCH] feat: disable background io per account, available apis in rust and jsonrpc api: rust: add `set_disable_background_io` and `get_disable_background_io` to a `Accounts` (the account manager) api: jsonrpc: add `set_disable_background_io` method and `Account::Configured.background_io_disabled` property api: select account now starts and stops io for "background disabled" accounts This is partly a port of the desktop "disable sync all" option, the other part is making it per account. So you can define that an account should be excluded from background syncing. This can be useful for advanced users that want to control their network traffic (don't connect a specific account in the background unless it is actively selected, like when the network is monitored and you want to hide your other accounts by disabling their background syncing.) --- deltachat-jsonrpc/src/api.rs | 39 +++++++---- deltachat-jsonrpc/src/api/types/account.rs | 48 ++++++++----- src/accounts.rs | 78 +++++++++++++++++++++- 3 files changed, 133 insertions(+), 32 deletions(-) diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index 4d100b27d..afae00a70 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -210,16 +210,16 @@ impl CommandApi { /// Get a list of all configured accounts. async fn get_all_accounts(&self) -> Result> { let mut accounts = Vec::new(); - for id in self.accounts.read().await.get_all() { - let context_option = self.accounts.read().await.get_account(id); - if let Some(ctx) = context_option { - accounts.push(Account::from_context(&ctx, id).await?) - } + let manager = self.accounts.read().await; + for id in manager.get_all() { + accounts.push(Account::load(&manager, id).await?) } Ok(accounts) } /// Starts background tasks for all accounts. + /// + /// Acounts with `disable_background_io` are not started, unless the account is the selected one async fn start_io_for_all_accounts(&self) -> Result<()> { self.accounts.write().await.start_io().await; Ok(()) @@ -236,6 +236,8 @@ impl CommandApi { /// The `AccountsBackgroundFetchDone` event is emitted at the end even in case of timeout. /// Process all events until you get this one and you can safely return to the background /// without forgetting to create notifications caused by timing race conditions. + /// + /// Acounts with `disable_background_io` are not fetched async fn accounts_background_fetch(&self, timeout_in_seconds: f64) -> Result<()> { self.accounts .write() @@ -245,6 +247,22 @@ impl CommandApi { Ok(()) } + /// Set disable_background_io for an account, when enabled, + /// io is stopped unless the account is selected and background fetch is also disabled for the account + /// + /// This automatically stops/starts io when account is in the background + pub async fn set_disable_background_io( + &self, + account_id: u32, + disable_background_io: bool, + ) -> Result<()> { + self.accounts + .write() + .await + .set_disable_background_io(account_id, disable_background_io) + .await + } + // --------------------------------------------- // Methods that work on individual accounts // --------------------------------------------- @@ -265,15 +283,8 @@ impl CommandApi { /// Get top-level info for an account. async fn get_account_info(&self, account_id: u32) -> Result { - let context_option = self.accounts.read().await.get_account(account_id); - if let Some(ctx) = context_option { - Ok(Account::from_context(&ctx, account_id).await?) - } else { - Err(anyhow!( - "account with id {} doesn't exist anymore", - account_id - )) - } + let manager = &self.accounts.read().await; + Account::load(manager, account_id).await } /// Get the combined filesize of an account in bytes diff --git a/deltachat-jsonrpc/src/api/types/account.rs b/deltachat-jsonrpc/src/api/types/account.rs index b2909109d..9312a6e63 100644 --- a/deltachat-jsonrpc/src/api/types/account.rs +++ b/deltachat-jsonrpc/src/api/types/account.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{anyhow, Result}; use deltachat::config::Config; use deltachat::contact::{Contact, ContactId}; use serde::Serialize; @@ -17,29 +17,43 @@ pub enum Account { // size: u32, profile_image: Option, // TODO: This needs to be converted to work with blob http server. color: String, + + /// Account IO is disabled when this account is not selected + /// + /// this means IO is stopped unless this account is selected + /// and background fetch is also disabled for this account + background_io_disabled: bool, }, #[serde(rename_all = "camelCase")] Unconfigured { id: u32 }, } impl Account { - pub async fn from_context(ctx: &deltachat::context::Context, id: u32) -> Result { - if ctx.is_configured().await? { - let display_name = ctx.get_config(Config::Displayname).await?; - let addr = ctx.get_config(Config::Addr).await?; - let profile_image = ctx.get_config(Config::Selfavatar).await?; - let color = color_int_to_hex_string( - Contact::get_by_id(ctx, ContactId::SELF).await?.get_color(), - ); - Ok(Account::Configured { - id, - display_name, - addr, - profile_image, - color, - }) + pub async fn load(accounts: &deltachat::accounts::Accounts, id: u32) -> Result { + if let Some(ctx) = &accounts.get_account(id) { + if ctx.is_configured().await? { + let display_name = ctx.get_config(Config::Displayname).await?; + let addr = ctx.get_config(Config::Addr).await?; + let profile_image = ctx.get_config(Config::Selfavatar).await?; + let color = color_int_to_hex_string( + Contact::get_by_id(ctx, ContactId::SELF).await?.get_color(), + ); + Ok(Account::Configured { + id: ctx.get_id(), + display_name, + addr, + profile_image, + color, + background_io_disabled: accounts.get_disable_background_io(id).unwrap_or(false), + }) + } else { + Ok(Account::Unconfigured { id }) + } } else { - Ok(Account::Unconfigured { id }) + Err(anyhow!( + "account with id {} doesn't exist anymore", + id + )) } } } diff --git a/src/accounts.rs b/src/accounts.rs index 626d279ef..c164c3c22 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -108,8 +108,21 @@ impl Accounts { /// Selects the given account. pub async fn select_account(&mut self, id: u32) -> Result<()> { + let previous_account_id = self.config.get_selected_account(); + if let Some(true) = self.config.get_disable_background_io(previous_account_id) { + if let Some(previous_account_ctx) = self.get_account(previous_account_id) { + previous_account_ctx.stop_io().await; + } + } + self.config.select_account(id).await?; + if let Some(true) = self.config.get_disable_background_io(id) { + if let Some(previous_account_ctx) = self.get_account(id) { + previous_account_ctx.start_io().await; + } + } + Ok(()) } @@ -264,7 +277,9 @@ impl Accounts { /// Starts background tasks such as IMAP and SMTP loops for all accounts. pub async fn start_io(&mut self) { for account in self.accounts.values_mut() { - account.start_io().await; + if let Some(false) = self.config.get_disable_background_io(account.id) { + account.start_io().await; + } } } @@ -278,6 +293,33 @@ impl Accounts { } } + /// Set disable_background_io for an account, when enabled, + /// io is stopped unless the account is selected and background fetch is also disabled for the account + /// + /// This automatically stops/starts io when account is in the background + pub async fn set_disable_background_io( + &mut self, + id: u32, + disable_background_io: bool, + ) -> Result<()> { + self.config + .set_disable_background_io(id, disable_background_io) + .await?; + if let Some(account) = self.get_account(id) { + if disable_background_io { + account.stop_io().await; + } else { + account.start_io().await; + } + } + Ok(()) + } + + // Checks if background io is disabled + pub fn get_disable_background_io(&self, id: u32) -> Option { + self.config.get_disable_background_io(id) + } + /// Notifies all accounts that the network may have become available. pub async fn maybe_network(&self) { for account in self.accounts.values() { @@ -296,6 +338,8 @@ impl Accounts { /// /// This is an auxiliary function and not part of public API. /// Use [Accounts::background_fetch] instead. + /// + /// Acounts with `disable_background_io` are not fetched async fn background_fetch_without_timeout(&self) { async fn background_fetch_and_log_error(account: Context) { if let Err(error) = account.background_fetch().await { @@ -306,6 +350,7 @@ impl Accounts { join_all( self.accounts .values() + .filter(|account| self.config.get_disable_background_io(account.id) != Some(true)) .cloned() .map(background_fetch_and_log_error), ) @@ -317,6 +362,8 @@ impl Accounts { /// The `AccountsBackgroundFetchDone` event is emitted at the end, /// process all events until you get this one and you can safely return to the background /// without forgetting to create notifications caused by timing race conditions. + /// + /// Acounts with `disable_background_io` are not fetched pub async fn background_fetch(&self, timeout: std::time::Duration) { if let Err(_err) = tokio::time::timeout(timeout, self.background_fetch_without_timeout()).await @@ -558,6 +605,7 @@ impl Config { id, dir: target_dir, uuid, + disable_background_io: false, }); self.inner.next_id += 1; id @@ -620,6 +668,28 @@ impl Config { self.sync().await?; Ok(()) } + + pub(crate) async fn set_disable_background_io(&mut self, id: u32, value: bool) -> Result<()> { + let position = self + .inner + .accounts + .iter() + .position(|e| e.id == id) + .context("account not found")?; + self.inner + .accounts + .get_mut(position) + .context("account not found")? + .disable_background_io = value; + + self.sync().await?; + Ok(()) + } + + // Checks if background io is disabled + pub fn get_disable_background_io(&self, id: u32) -> Option { + Some(self.get_account(id)?.disable_background_io) + } } /// Spend up to 1 minute trying to do the operation. @@ -666,6 +736,12 @@ struct AccountConfig { /// Universally unique account identifier. pub uuid: Uuid, + + /// Disable account io when it is not selected + /// + /// this means io is stopped unless the account is selected + /// and background fetch is also disabled for the account + pub disable_background_io: bool, } impl AccountConfig {