mirror of
https://github.com/flectar/mail.git
synced 2026-11-11 17:57:54 +00:00
feat: open the mail store without the window on Android
This commit is contained in:
1 parent
f6a6e6c256
commit
df13f1f41b
8 files changed
+484
-29
No files matched your search
@@ -16,7 +16,10 @@ fn main() {
|
||||
user_notifications: { any(target_os = "macos", target_os = "ios") },
|
||||
// The system suspends the app in the background and wakes it to
|
||||
// check for new mail.
|
||||
background_checks: { target_os = "ios" },
|
||||
background_checks: { any(target_os = "ios", target_os = "android") },
|
||||
// The system may start the app without its window, for a check for
|
||||
// new mail or a button on a notification.
|
||||
background_engine: { target_os = "android" },
|
||||
}
|
||||
|
||||
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("ios") {
|
||||
|
||||
+2
-1
@@ -26,7 +26,8 @@ these aliases with [`cfg_aliases`](https://crates.io/crates/cfg_aliases):
|
||||
| `linux_desktop` | A Linux desktop session (portals, X11/Wayland, Secret Service). Excludes OpenHarmony, which also reports `target_os = "linux"`. |
|
||||
| `glibc` | Linux with the GNU C library. |
|
||||
| `user_notifications` | New-mail notifications through Apple's User Notifications framework (macOS, iOS). |
|
||||
| `background_checks` | The system suspends the app in the background and wakes it to check for new mail (iOS). |
|
||||
| `background_checks` | The system suspends the app in the background and wakes it to check for new mail (iOS, Android). |
|
||||
| `background_engine` | The system may start the app without its window, for a check for new mail or a button on a notification (Android). |
|
||||
| `rename_noreplace` | Core only: kernels with an atomic no-replace directory rename. |
|
||||
| `secret_service` | Core only: desktops where `keyring` stores secrets through the freedesktop Secret Service (Linux, FreeBSD and OpenBSD, not OpenHarmony). |
|
||||
|
||||
|
||||
+17
-3
@@ -42,9 +42,15 @@ mod memory;
|
||||
mod message_appearance;
|
||||
mod move_picker;
|
||||
mod notifications;
|
||||
/// What Android asks of the app without its window.
|
||||
#[cfg(background_engine)]
|
||||
pub use notifications::engine::{
|
||||
EngineServices, check_for_mail, respond_in_background, stop_check, window_ended,
|
||||
window_starting,
|
||||
};
|
||||
pub use notifications::{
|
||||
Act, Alert, AlertId, Availability, Failure, FixedWords, HostFuture, NewMailNotification,
|
||||
NotificationHost, Response, Route, parse_payload, payload, respond,
|
||||
Act, Alert, AlertId, Availability, Badge, Controls, Failure, FixedWords, HostFuture,
|
||||
NewMailNotification, NotificationHost, Response, Route, parse_payload, payload, respond,
|
||||
};
|
||||
mod number_format;
|
||||
mod oauth_browser;
|
||||
@@ -1468,7 +1474,7 @@ impl PlatformContext {
|
||||
oauth_redirects: flectar_mail_core::oauth::redirect::OAuthRedirectBrokerHandle,
|
||||
) -> Self {
|
||||
Self {
|
||||
paths: Paths::new(data_root.join("data"), cache_dir),
|
||||
paths: Self::app_private_paths(data_root, cache_dir),
|
||||
external_urls: ExternalUrlOpener::default(),
|
||||
notifications: None,
|
||||
documents,
|
||||
@@ -1476,6 +1482,12 @@ impl PlatformContext {
|
||||
oauth_redirects,
|
||||
}
|
||||
}
|
||||
|
||||
/// The profile in a host-owned sandbox, as [`Self::app_private`] keeps it:
|
||||
/// what a process that runs without the window opens too.
|
||||
pub fn app_private_paths(data_root: PathBuf, cache_dir: PathBuf) -> Paths {
|
||||
Paths::new(data_root.join("data"), cache_dir)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(desktop)]
|
||||
@@ -7272,6 +7284,8 @@ pub fn run(platform: PlatformContext) -> Result<(), Box<dyn std::error::Error>>
|
||||
composer::save_open_messages();
|
||||
let core = state.borrow().core.as_ref().map(CoreMailSource::file_core);
|
||||
notifications::withdraw_before_exit(&runtime, core, platform.notifications.clone());
|
||||
// The runtime ends with this function.
|
||||
notifications::core_closed();
|
||||
event_loop_result.map_err(|error| {
|
||||
renderer_preferences::startup_error(error, use_wgpu && !gpu_startup_completed.get())
|
||||
})?;
|
||||
|
||||
+76
-6
@@ -123,6 +123,10 @@ pub trait NotificationHost: Send + Sync + 'static {
|
||||
fn set_badge(&self, _badge: Option<Badge>) -> HostFuture<()> {
|
||||
Box::pin(std::future::ready(()))
|
||||
}
|
||||
/// Whether the user wants what a check in the background brings, as it
|
||||
/// changes: notifications, or the count on the app icon. Hosts that ask
|
||||
/// the system for such checks schedule or stop them here.
|
||||
fn checks_wanted(&self, _wanted: bool) {}
|
||||
}
|
||||
|
||||
/// The count of unread mail on the app icon.
|
||||
@@ -484,6 +488,25 @@ fn with_core(errand: impl FnOnce(&tokio::runtime::Handle, &Arc<Core>) + Send + '
|
||||
errand(&runtime, &core);
|
||||
}
|
||||
|
||||
/// The mail store closed with the run that opened it. Errands from now on
|
||||
/// wait for the next one, such as a check in the background opens.
|
||||
pub(crate) fn core_closed() {
|
||||
ERRANDS.lock().unwrap_or_else(PoisonError::into_inner).ready = None;
|
||||
}
|
||||
|
||||
/// The mail store could not open: give up the errands that waited for it,
|
||||
/// which tells whoever waits on them.
|
||||
#[cfg(background_engine)]
|
||||
fn drop_waiting_errands() {
|
||||
let waiting = std::mem::take(
|
||||
&mut ERRANDS
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.waiting,
|
||||
);
|
||||
drop(waiting);
|
||||
}
|
||||
|
||||
/// The mail store opened: run the errands that waited for it, in order.
|
||||
fn core_opened(runtime: &tokio::runtime::Handle, core: &Arc<Core>) {
|
||||
let waiting = {
|
||||
@@ -498,12 +521,26 @@ fn core_opened(runtime: &tokio::runtime::Handle, core: &Arc<Core>) {
|
||||
|
||||
/// The user brought the app to the foreground, where the Inbox shows what
|
||||
/// came while it wasn't running: that is no news for a check in the
|
||||
/// background ([`flectar_mail_core::Core::forget_missed`]).
|
||||
/// background ([`flectar_mail_core::Core::forget_missed`]). The store open
|
||||
/// now forgets it, and so does the window's as it opens. A store a check
|
||||
/// opens later doesn't: the user hasn't seen what came since.
|
||||
#[cfg(background_checks)]
|
||||
pub(crate) fn app_became_active() {
|
||||
with_core(|_, core| core.forget_missed());
|
||||
FORGET_WHEN_THE_WINDOW_OPENS.store(true, Ordering::Relaxed);
|
||||
let ready = ERRANDS
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.ready
|
||||
.clone();
|
||||
if let Some((_, core)) = ready {
|
||||
core.forget_missed();
|
||||
}
|
||||
}
|
||||
|
||||
/// The app came to the front before the window's store opened.
|
||||
#[cfg(background_checks)]
|
||||
static FORGET_WHEN_THE_WINDOW_OPENS: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Stops a [`check_in_background`].
|
||||
#[cfg_attr(not(background_checks), allow(dead_code))]
|
||||
pub(crate) struct BackgroundCheck(tokio::sync::watch::Sender<bool>);
|
||||
@@ -602,6 +639,21 @@ pub(crate) fn serve_requests(handler: impl Fn(Request) + Send + Sync + 'static)
|
||||
}
|
||||
}
|
||||
|
||||
/// The body of an account's summary of new mail: the first senders it
|
||||
/// names, then how many others, as `more` words it.
|
||||
fn summary_body(senders: &[String], more: impl FnOnce(String, i32) -> String) -> String {
|
||||
let named = senders
|
||||
.iter()
|
||||
.take(SUMMARY_SENDERS)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
match senders.len().saturating_sub(SUMMARY_SENDERS) {
|
||||
0 => named,
|
||||
others => more(named, i32::try_from(others).unwrap_or(i32::MAX)),
|
||||
}
|
||||
}
|
||||
|
||||
/// More new messages than this for one account at once are summed up in one
|
||||
/// notification. Peers sum up from three (Proton) to five (Mailspring).
|
||||
const SUMMARY_AFTER: usize = 3;
|
||||
@@ -632,8 +684,9 @@ pub(crate) fn preferences_changed() {
|
||||
static BACKGROUND_CHECKS_WANTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Whether to ask the system to wake the app now and then to check for new
|
||||
/// mail.
|
||||
#[cfg(background_checks)]
|
||||
/// mail, for iOS, which asks as the app leaves the front. Android hosts hear
|
||||
/// it as it changes ([`NotificationHost::checks_wanted`]).
|
||||
#[cfg(all(user_notifications, background_checks))]
|
||||
pub(crate) fn background_checks_wanted() -> bool {
|
||||
BACKGROUND_CHECKS_WANTED.load(Ordering::Relaxed)
|
||||
}
|
||||
@@ -665,6 +718,10 @@ pub(crate) fn spawn_dispatcher(
|
||||
core: Arc<Core>,
|
||||
host: Option<Arc<dyn NotificationHost>>,
|
||||
) {
|
||||
#[cfg(background_checks)]
|
||||
if FORGET_WHEN_THE_WINDOW_OPENS.swap(false, Ordering::Relaxed) {
|
||||
core.forget_missed();
|
||||
}
|
||||
dispatch_through(runtime, core, host, ui::Live);
|
||||
}
|
||||
|
||||
@@ -684,6 +741,7 @@ fn dispatch_through(
|
||||
let mut pruned_at = None;
|
||||
let mut enabled = notifications_enabled(&core).await;
|
||||
let mut badge = None;
|
||||
let mut told_wanted = None;
|
||||
let mut flushed = 0;
|
||||
loop {
|
||||
let flushing = FLUSHES_ASKED.load(Ordering::Acquire);
|
||||
@@ -716,8 +774,14 @@ fn dispatch_through(
|
||||
}
|
||||
}
|
||||
}
|
||||
BACKGROUND_CHECKS_WANTED
|
||||
.store(host.is_some() && (enabled || counting), Ordering::Relaxed);
|
||||
let wanted = host.is_some() && (enabled || counting);
|
||||
BACKGROUND_CHECKS_WANTED.store(wanted, Ordering::Relaxed);
|
||||
if let Some(host) = host.as_deref()
|
||||
&& told_wanted != Some(wanted)
|
||||
{
|
||||
host.checks_wanted(wanted);
|
||||
told_wanted = Some(wanted);
|
||||
}
|
||||
flushed = flushing;
|
||||
FLUSHES_SERVED.send_replace(flushed);
|
||||
tokio::select! {
|
||||
@@ -1179,6 +1243,12 @@ pub(crate) mod ui;
|
||||
#[path = "notifications/words.rs"]
|
||||
mod words;
|
||||
|
||||
/// The mail store without the window, for what Android asks of the app
|
||||
/// after its process had ended.
|
||||
#[cfg(background_engine)]
|
||||
#[path = "notifications/engine.rs"]
|
||||
pub mod engine;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
//! The mail store for what Android asks of the app without its window: a
|
||||
//! check for new mail, or a button on a notification after the app's process
|
||||
//! had ended. While the window runs, its store serves both. Otherwise one
|
||||
//! opens for the errand and closes once nothing holds it. Only one store is
|
||||
//! ever open, as two would sync the same accounts.
|
||||
|
||||
use super::{NotificationHost, Response, Window, Words, parse_payload, words::Speech};
|
||||
use flectar_mail_core::{
|
||||
Core, accounts::credentials::CredentialStoreHandle, config::Paths,
|
||||
oauth::redirect::OAuthRedirectBrokerHandle,
|
||||
};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
sync::{Arc, Mutex, MutexGuard, PoisonError, mpsc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
/// What opens the mail store without the window.
|
||||
pub struct EngineServices {
|
||||
pub paths: Paths,
|
||||
pub credentials: CredentialStoreHandle,
|
||||
pub oauth_redirects: OAuthRedirectBrokerHandle,
|
||||
pub host: Arc<dyn NotificationHost>,
|
||||
}
|
||||
|
||||
enum State {
|
||||
/// No store is open.
|
||||
Closed,
|
||||
/// The window runs, with the store it opened.
|
||||
Window,
|
||||
/// A store opened for errands, held this many times.
|
||||
Open {
|
||||
runtime: tokio::runtime::Runtime,
|
||||
held: usize,
|
||||
generation: u64,
|
||||
},
|
||||
}
|
||||
|
||||
static STATE: Mutex<State> = Mutex::new(State::Closed);
|
||||
/// Tells the stores opened for errands apart, so a late release or timer
|
||||
/// of one never touches the next.
|
||||
static GENERATION: Mutex<u64> = Mutex::new(0);
|
||||
/// The checks in progress, by the job that runs each.
|
||||
static CHECKS: Mutex<BTreeMap<i32, super::BackgroundCheck>> = Mutex::new(BTreeMap::new());
|
||||
|
||||
/// How long a store opened for errands stays open after the last one ends,
|
||||
/// for errands that come in bursts, such as several buttons in a row.
|
||||
const LINGER: Duration = Duration::from_secs(5);
|
||||
/// How long the window waits for such a store to close.
|
||||
const HAND_OVER: Duration = Duration::from_secs(3);
|
||||
/// How long a button's receiver waits for its action to be saved, within
|
||||
/// the time Android gives a receiver.
|
||||
const SAVE_LIMIT: Duration = Duration::from_secs(8);
|
||||
|
||||
fn state() -> MutexGuard<'static, State> {
|
||||
STATE.lock().unwrap_or_else(PoisonError::into_inner)
|
||||
}
|
||||
|
||||
/// Keeps the store an errand uses open while it lives. The window's store
|
||||
/// needs no holding.
|
||||
pub(crate) struct Hold(Option<u64>);
|
||||
|
||||
impl Drop for Hold {
|
||||
fn drop(&mut self) {
|
||||
let Some(generation) = self.0 else {
|
||||
return;
|
||||
};
|
||||
let mut state = state();
|
||||
if let State::Open {
|
||||
held,
|
||||
generation: current,
|
||||
..
|
||||
} = &mut *state
|
||||
&& *current == generation
|
||||
{
|
||||
*held -= 1;
|
||||
if *held == 0 {
|
||||
drop(state);
|
||||
// Not on a runtime thread: closing waits for the runtime.
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(LINGER);
|
||||
close_unheld(generation);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hold whichever store is open.
|
||||
pub(crate) fn hold() -> Hold {
|
||||
match &mut *state() {
|
||||
State::Open {
|
||||
held, generation, ..
|
||||
} => {
|
||||
*held += 1;
|
||||
Hold(Some(*generation))
|
||||
}
|
||||
State::Closed | State::Window => Hold(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Open a store for errands unless one is open, and hold it.
|
||||
fn open(services: impl FnOnce() -> Result<EngineServices, String>) -> Result<Hold, String> {
|
||||
let mut state = state();
|
||||
match &mut *state {
|
||||
State::Window => return Ok(Hold(None)),
|
||||
State::Open {
|
||||
held, generation, ..
|
||||
} => {
|
||||
*held += 1;
|
||||
return Ok(Hold(Some(*generation)));
|
||||
}
|
||||
State::Closed => {}
|
||||
}
|
||||
let EngineServices {
|
||||
paths,
|
||||
credentials,
|
||||
oauth_redirects,
|
||||
host,
|
||||
} = services()?;
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(2)
|
||||
.enable_all()
|
||||
.build()
|
||||
.map_err(|error| error.to_string())?;
|
||||
let handle = runtime.handle().clone();
|
||||
runtime.spawn(async move {
|
||||
let core =
|
||||
match Core::start_mail_ui_with_platform(paths, credentials, oauth_redirects).await {
|
||||
Ok(core) => core,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "notifications: could not open the mail store");
|
||||
super::drop_waiting_errands();
|
||||
return;
|
||||
}
|
||||
};
|
||||
let speech = Speech::new(language(&core).await);
|
||||
// Names the channels, in a process the window never ran in.
|
||||
host.set_words(&speech.fixed());
|
||||
// Runs the errands that waited for the store.
|
||||
super::dispatch_through(&handle, Arc::new(core), Some(host), Headless(speech));
|
||||
});
|
||||
let generation = {
|
||||
let mut generation = GENERATION.lock().unwrap_or_else(PoisonError::into_inner);
|
||||
*generation += 1;
|
||||
*generation
|
||||
};
|
||||
*state = State::Open {
|
||||
runtime,
|
||||
held: 1,
|
||||
generation,
|
||||
};
|
||||
Ok(Hold(Some(generation)))
|
||||
}
|
||||
|
||||
/// The UI's language, as the window chooses it.
|
||||
async fn language(core: &Core) -> &'static str {
|
||||
let preference = core
|
||||
.get_settings()
|
||||
.await
|
||||
.map(|settings| settings.language)
|
||||
.unwrap_or_default();
|
||||
match crate::normalized_language_preference(&preference) {
|
||||
"system" => sys_locale::get_locale()
|
||||
.as_deref()
|
||||
.and_then(crate::bundled_language_for_locale)
|
||||
.unwrap_or("en"),
|
||||
language => language,
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the store opened for errands once nothing holds it.
|
||||
fn close_unheld(generation: u64) {
|
||||
let mut state = state();
|
||||
let unheld = matches!(
|
||||
&*state,
|
||||
State::Open { held: 0, generation: current, .. } if *current == generation
|
||||
);
|
||||
if !unheld {
|
||||
return;
|
||||
}
|
||||
let State::Open { runtime, .. } = std::mem::replace(&mut *state, State::Closed) else {
|
||||
return;
|
||||
};
|
||||
super::core_closed();
|
||||
drop(state);
|
||||
runtime.shutdown_timeout(HAND_OVER);
|
||||
}
|
||||
|
||||
/// The window is about to open its own store: a store opened for errands
|
||||
/// closes first, and the checks in it stop to run again later. Errands from
|
||||
/// now on use the window's store.
|
||||
pub fn window_starting() {
|
||||
let previous = std::mem::replace(&mut *state(), State::Window);
|
||||
if let State::Open { runtime, .. } = previous {
|
||||
for check in CHECKS
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.values()
|
||||
{
|
||||
check.cancel();
|
||||
}
|
||||
super::core_closed();
|
||||
runtime.shutdown_timeout(HAND_OVER);
|
||||
}
|
||||
}
|
||||
|
||||
/// The window closed its store.
|
||||
pub fn window_ended() {
|
||||
let mut state = state();
|
||||
if matches!(*state, State::Window) {
|
||||
*state = State::Closed;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check every account for new mail and tell of it, as a job Android runs
|
||||
/// does: with the window's store, or one opened for the check. Returns once
|
||||
/// done, with whether every account answered: `false` when no store could
|
||||
/// open or the job was stopped.
|
||||
pub fn check_for_mail(services: impl FnOnce() -> Result<EngineServices, String>, job: i32) -> bool {
|
||||
let hold = match open(services) {
|
||||
Ok(hold) => hold,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "notifications: no mail store to check");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let (sender, done) = mpsc::channel();
|
||||
let check = super::check_in_background(move |every| {
|
||||
let _ = sender.send(every);
|
||||
});
|
||||
CHECKS
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.insert(job, check);
|
||||
// A store that closes under the check drops its sender.
|
||||
let every = done.recv().unwrap_or(false);
|
||||
CHECKS
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.remove(&job);
|
||||
drop(hold);
|
||||
every
|
||||
}
|
||||
|
||||
/// Android wants the job's time back.
|
||||
pub fn stop_check(job: i32) {
|
||||
if let Some(check) = CHECKS
|
||||
.lock()
|
||||
.unwrap_or_else(PoisonError::into_inner)
|
||||
.get(&job)
|
||||
{
|
||||
check.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// Carry out a button on a notification, as its receiver asks: returns once
|
||||
/// the action is saved, or after [`SAVE_LIMIT`]. The store stays open while
|
||||
/// the action goes to the server.
|
||||
pub fn respond_in_background(
|
||||
services: impl FnOnce() -> Result<EngineServices, String>,
|
||||
request: &str,
|
||||
) {
|
||||
let Some((route, act)) = parse_payload(request) else {
|
||||
return;
|
||||
};
|
||||
let hold = match open(services) {
|
||||
Ok(hold) => hold,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "notifications: no mail store for the button");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let (sender, saved) = mpsc::channel();
|
||||
super::respond_then(
|
||||
Response {
|
||||
route,
|
||||
act,
|
||||
activation_token: None,
|
||||
},
|
||||
move || {
|
||||
let _ = sender.send(());
|
||||
},
|
||||
);
|
||||
let _ = saved.recv_timeout(SAVE_LIMIT);
|
||||
drop(hold);
|
||||
}
|
||||
|
||||
/// What the dispatcher asks of the window, without one: nothing is on
|
||||
/// screen, and the words come from the catalogs.
|
||||
struct Headless(Speech);
|
||||
|
||||
impl Window for Headless {
|
||||
async fn on_screen(&self, _accounts: BTreeSet<i64>) -> Option<BTreeSet<i64>> {
|
||||
Some(BTreeSet::new())
|
||||
}
|
||||
|
||||
async fn words(&self, summaries: Vec<(u32, Vec<String>)>) -> Option<Words> {
|
||||
let summaries = summaries
|
||||
.into_iter()
|
||||
.map(|(count, senders)| {
|
||||
(
|
||||
self.0
|
||||
.new_messages(i32::try_from(count).unwrap_or(i32::MAX)),
|
||||
super::summary_body(&senders, |named, more| self.0.more_senders(&named, more)),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
Some(Words {
|
||||
fixed: self.0.fixed(),
|
||||
summaries,
|
||||
})
|
||||
}
|
||||
|
||||
async fn badge_description(&self, count: u32) -> Option<String> {
|
||||
Some(
|
||||
self.0
|
||||
.unread_count(i32::try_from(count).unwrap_or(i32::MAX)),
|
||||
)
|
||||
}
|
||||
}
|
||||
+4
-16
@@ -165,22 +165,10 @@ async fn words(summaries: Vec<(u32, Vec<String>)>) -> Option<Words> {
|
||||
let title = i18n
|
||||
.invoke_notification_new_messages(i32::try_from(count).unwrap_or(i32::MAX))
|
||||
.into();
|
||||
let named = senders
|
||||
.iter()
|
||||
.take(super::SUMMARY_SENDERS)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let more = senders.len().saturating_sub(super::SUMMARY_SENDERS);
|
||||
let body = if more == 0 {
|
||||
named
|
||||
} else {
|
||||
i18n.invoke_notification_more_senders(
|
||||
named.into(),
|
||||
i32::try_from(more).unwrap_or(i32::MAX),
|
||||
)
|
||||
.into()
|
||||
};
|
||||
let body = super::summary_body(&senders, |named, more| {
|
||||
i18n.invoke_notification_more_senders(named.into(), more)
|
||||
.into()
|
||||
});
|
||||
(title, body)
|
||||
})
|
||||
.collect();
|
||||
|
||||
+59
-1
@@ -2,7 +2,13 @@
|
||||
//! JNI services live in that shell; this module only holds what the shared
|
||||
//! application needs at runtime.
|
||||
use flectar_mail_core::models::AppIcon;
|
||||
use std::{path::PathBuf, sync::OnceLock};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
OnceLock,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
static PDFIUM_LIBRARY: OnceLock<PathBuf> = OnceLock::new();
|
||||
/// Hands the chosen icon's name to the activity.
|
||||
@@ -111,3 +117,55 @@ pub(crate) fn show_app_icon(icon: AppIcon) {
|
||||
pub(crate) fn email_fonts() -> Option<&'static super::EmailFonts> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Whether the activity is in front of the user, as its host reports it.
|
||||
static ACTIVE: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// The activity was resumed (`true`) or paused. Coming to the front, the
|
||||
/// user sees what came while the app wasn't running, and with an account set
|
||||
/// up it is the moment to ask for permission to notify.
|
||||
pub fn set_app_active(active: bool) {
|
||||
ACTIVE.store(active, Ordering::Relaxed);
|
||||
if active {
|
||||
crate::notifications::app_became_active();
|
||||
let _ = slint::invoke_from_event_loop(crate::notifications::ui::came_to_front);
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the activity is in front of the user.
|
||||
pub(crate) fn app_is_active() -> bool {
|
||||
ACTIVE.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// What the host reports and opens about the app's work in the background.
|
||||
pub struct BackgroundBridge {
|
||||
/// Whether Android lets the app check for mail in the background now.
|
||||
pub state: Box<dyn Fn() -> super::BackgroundRefresh + Send + Sync>,
|
||||
/// The app's page in Settings, where its background use is allowed.
|
||||
pub open_settings: Box<dyn Fn() + Send + Sync>,
|
||||
}
|
||||
|
||||
static BACKGROUND: OnceLock<BackgroundBridge> = OnceLock::new();
|
||||
|
||||
/// Supply the bridge to the host's background state, once.
|
||||
pub fn configure_background(bridge: BackgroundBridge) {
|
||||
let _ = BACKGROUND.set(bridge);
|
||||
}
|
||||
|
||||
pub(crate) fn background_refresh() -> super::BackgroundRefresh {
|
||||
BACKGROUND
|
||||
.get()
|
||||
.map_or(super::BackgroundRefresh::On, |bridge| (bridge.state)())
|
||||
}
|
||||
|
||||
pub(crate) fn open_app_settings() {
|
||||
if let Some(bridge) = BACKGROUND.get() {
|
||||
(bridge.open_settings)();
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps a mail store opened for a background errand open while the guard
|
||||
/// lives, such as while a button's action goes to the server.
|
||||
pub(crate) fn stay_awake() -> crate::notifications::engine::Hold {
|
||||
crate::notifications::engine::hold()
|
||||
}
|
||||
+1
-1
@@ -25,7 +25,7 @@ mod web_authentication;
|
||||
/// Whether the system lets the app check for new mail in the background.
|
||||
#[cfg(background_checks)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum BackgroundRefresh {
|
||||
pub enum BackgroundRefresh {
|
||||
On,
|
||||
/// Turned off for the app, or not allowed on the device.
|
||||
Off,
|
||||
|
||||
Reference in new issue
Block a user