use std::fmt::Display;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use serde::Serialize;
use tauri::{AppHandle, Emitter, Manager, Wry};
use crate::{ShellState, EVENT_IDLE};
pub const DEFAULT_SUSPEND_AFTER: Duration = Duration::from_secs(30);
pub const MAX_SUSPEND_AFTER: Duration = Duration::from_secs(24 * 60 * 60);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ControllerHide {
#[default]
Immediate,
OnTimer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum IdleEvents {
#[default]
Always,
NativeSuspendOnly,
}
impl IdleEvents {
pub fn emits(self, native_suspend: bool) -> bool {
match self {
IdleEvents::Always => true,
IdleEvents::NativeSuspendOnly => native_suspend,
}
}
}
#[derive(Clone)]
pub struct SuspendAfterProvider(Arc<dyn Fn() -> Option<Duration> + Send + Sync>);
impl std::fmt::Debug for SuspendAfterProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("SuspendAfterProvider(..)")
}
}
impl PartialEq for SuspendAfterProvider {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
impl Eq for SuspendAfterProvider {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IdleOptions {
pub suspend_after: Duration,
pub start_on_minimize: bool,
pub controller_hide: ControllerHide,
pub events: IdleEvents,
suspend_after_fn: Option<SuspendAfterProvider>,
}
impl Default for IdleOptions {
fn default() -> Self {
Self {
suspend_after: DEFAULT_SUSPEND_AFTER,
start_on_minimize: false,
controller_hide: ControllerHide::Immediate,
events: IdleEvents::Always,
suspend_after_fn: None,
}
}
}
impl IdleOptions {
pub fn suspend_after(mut self, suspend_after: Duration) -> Self {
self.suspend_after = suspend_after;
self
}
pub fn start_on_minimize(mut self, start_on_minimize: bool) -> Self {
self.start_on_minimize = start_on_minimize;
self
}
pub fn controller_hide(mut self, controller_hide: ControllerHide) -> Self {
self.controller_hide = controller_hide;
self
}
pub fn events(mut self, events: IdleEvents) -> Self {
self.events = events;
self
}
pub fn suspend_after_with(
mut self,
provider: impl Fn() -> Option<Duration> + Send + Sync + 'static,
) -> Self {
self.suspend_after_fn = Some(SuspendAfterProvider(Arc::new(provider)));
self
}
pub fn hide_delay(&self, host_default: Duration, per_hide: Option<Duration>) -> Duration {
effective_delay(host_default, per_hide.or_else(|| self.provided_delay()))
}
fn provided_delay(&self) -> Option<Duration> {
let provider = self.suspend_after_fn.as_ref()?;
catch_unwind(AssertUnwindSafe(|| (*provider.0)()))
.ok()
.flatten()
}
}
pub fn effective_delay(host_default: Duration, per_hide: Option<Duration>) -> Duration {
per_hide.unwrap_or(host_default).min(MAX_SUSPEND_AFTER)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HidePlan {
pub now: bool,
pub on_timer: bool,
}
pub fn hide_plan(mode: ControllerHide, controller_already_hidden: bool) -> HidePlan {
HidePlan {
now: mode == ControllerHide::Immediate && !controller_already_hidden,
on_timer: mode == ControllerHide::OnTimer && !controller_already_hidden,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MinimizeStep {
Start,
End,
None,
}
#[derive(Debug, Default)]
pub struct MinimizeTracker {
active: AtomicBool,
}
impl MinimizeTracker {
pub fn step(&self, minimized: bool) -> MinimizeStep {
let was = self.active.swap(minimized, Ordering::SeqCst);
match (was, minimized) {
(false, true) => MinimizeStep::Start,
(true, false) => MinimizeStep::End,
_ => MinimizeStep::None,
}
}
pub fn clear(&self) {
self.active.store(false, Ordering::SeqCst);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum IdlePhase {
Suspend,
Resume,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct IdleFailure {
pub phase: IdlePhase,
pub window: String,
pub error: String,
}
pub type IdleErrorCallback = Arc<dyn Fn(&IdleFailure) + Send + Sync>;
#[derive(Clone)]
pub(crate) struct Reporter {
window: String,
sink: Option<IdleErrorCallback>,
}
impl Reporter {
#[cfg_attr(not(windows), allow(dead_code))]
pub(crate) fn fail(&self, phase: IdlePhase, error: impl Display) {
if let Some(sink) = &self.sink {
sink(&IdleFailure {
phase,
window: self.window.clone(),
error: error.to_string(),
});
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IdleAction {
HideControllerAndStartTimer,
ShowController,
SuspendNow,
ResumeAndShowController,
None,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct IdleState {
hidden: bool,
suspended: bool,
generation: u64,
}
impl IdleState {
pub fn on_hidden(&mut self) -> (u64, IdleAction) {
if self.suspended {
self.hidden = true;
return (self.generation, IdleAction::None);
}
self.hidden = true;
self.generation = self.generation.wrapping_add(1);
(self.generation, IdleAction::HideControllerAndStartTimer)
}
pub fn on_visible(&mut self) -> IdleAction {
let was_hidden = self.hidden;
self.hidden = false;
self.generation = self.generation.wrapping_add(1);
if std::mem::take(&mut self.suspended) {
IdleAction::ResumeAndShowController
} else if was_hidden {
IdleAction::ShowController
} else {
IdleAction::None
}
}
pub fn on_timer(&mut self, generation: u64) -> IdleAction {
if self.hidden && !self.suspended && self.generation == generation {
self.suspended = true;
IdleAction::SuspendNow
} else {
IdleAction::None
}
}
pub fn on_suspend_failed(&mut self) {
self.suspended = false;
}
pub fn snapshot(&self) -> (bool, bool) {
(self.hidden, self.suspended)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum IdleTransitionKind {
Suspended,
Resumed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct IdleTransition {
pub kind: IdleTransitionKind,
pub window: String,
pub elapsed: Duration,
}
pub type IdleTransitionCallback = Arc<dyn Fn(&IdleTransition) + Send + Sync>;
pub(crate) trait Controller {
fn set_visible(&mut self, visible: bool) -> Result<(), String>;
fn try_suspend(&mut self) -> Result<(), String>;
fn resume(&mut self) -> Result<(), String>;
}
#[derive(Debug, Default)]
pub(crate) struct Applied {
pub(crate) hidden: bool,
pub(crate) suspended: bool,
suspended_at: Option<Instant>,
resumed_for: Option<Duration>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct StepFailure {
pub(crate) phase: IdlePhase,
pub(crate) error: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ResumeStep {
Nothing,
Shown,
Resumed(Duration),
}
impl Applied {
pub(crate) fn hide(&mut self, c: &mut dyn Controller) -> Result<(), StepFailure> {
if self.hidden {
return Ok(());
}
c.set_visible(false).map_err(|error| StepFailure {
phase: IdlePhase::Suspend,
error: format!("SetIsVisible(false): {error}"),
})?;
self.hidden = true;
Ok(())
}
pub(crate) fn hide_and_suspend(
&mut self,
c: &mut dyn Controller,
now: Instant,
) -> Result<(), StepFailure> {
self.hide(c)?;
if self.suspended {
return Ok(());
}
c.try_suspend().map_err(|error| StepFailure {
phase: IdlePhase::Suspend,
error: format!("TrySuspend: {error}"),
})?;
self.suspended = true;
self.suspended_at = Some(now);
Ok(())
}
pub(crate) fn resume_and_show(
&mut self,
c: &mut dyn Controller,
now: Instant,
) -> Result<ResumeStep, StepFailure> {
let mut resumed = None;
if self.suspended {
c.resume().map_err(|error| StepFailure {
phase: IdlePhase::Resume,
error: format!("Resume: {error}"),
})?;
self.suspended = false;
let for_ = self
.suspended_at
.take()
.map(|at| now.saturating_duration_since(at))
.unwrap_or_default();
self.resumed_for = Some(for_);
}
if self.hidden {
c.set_visible(true).map_err(|error| StepFailure {
phase: IdlePhase::Resume,
error: format!("SetIsVisible(true): {error}"),
})?;
self.hidden = false;
}
if let Some(for_) = self.resumed_for.take() {
resumed = Some(for_);
}
Ok(match resumed {
Some(for_) => ResumeStep::Resumed(for_),
None => ResumeStep::Nothing,
})
}
}
impl Applied {
fn classify(&self, before_hidden: bool, step: ResumeStep) -> ResumeStep {
match step {
ResumeStep::Nothing if before_hidden => ResumeStep::Shown,
other => other,
}
}
}
pub struct IdleHost {
pub suspend_after: Duration,
pub options: IdleOptions,
pub minimize: MinimizeTracker,
pub state: Mutex<IdleState>,
pub counters: Mutex<(u32, u32)>,
pub on_error: Option<IdleErrorCallback>,
pub on_transition: Option<IdleTransitionCallback>,
pub(crate) applied: Mutex<Applied>,
}
impl IdleHost {
pub fn new(suspend_after: Duration) -> Self {
Self {
suspend_after,
options: IdleOptions {
suspend_after,
..IdleOptions::default()
},
minimize: MinimizeTracker::default(),
state: Mutex::new(IdleState::default()),
counters: Mutex::new((0, 0)),
on_error: None,
on_transition: None,
applied: Mutex::new(Applied::default()),
}
}
pub fn with_options(mut self, options: IdleOptions) -> Self {
self.suspend_after = options.suspend_after.min(MAX_SUSPEND_AFTER);
self.options = IdleOptions {
suspend_after: self.suspend_after,
..options
};
self
}
pub fn with_on_error(mut self, on_error: Option<IdleErrorCallback>) -> Self {
self.on_error = on_error;
self
}
pub fn with_on_transition(mut self, on_transition: Option<IdleTransitionCallback>) -> Self {
self.on_transition = on_transition;
self
}
pub fn report_failure(&self, phase: IdlePhase, window: &str, error: &str) {
self.reporter(window).fail(phase, error);
}
pub(crate) fn reporter(&self, window: &str) -> Reporter {
Reporter {
window: window.to_owned(),
sink: self.on_error.clone(),
}
}
pub(crate) fn transition(&self, kind: IdleTransitionKind, window: &str, elapsed: Duration) {
if let Some(sink) = &self.on_transition {
sink(&IdleTransition {
kind,
window: window.to_owned(),
elapsed,
});
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IdleStatus {
pub enabled: bool,
pub hidden: bool,
pub suspended: bool,
pub suspend_after_ms: u64,
pub suspends: u32,
pub resumes: u32,
pub native_suspend: bool,
}
pub fn status(app: &AppHandle<Wry>) -> IdleStatus {
let state = app.state::<ShellState>();
match &state.idle {
Some(h) => {
let (hidden, suspended) = h.state.lock().unwrap_or_else(|e| e.into_inner()).snapshot();
let (suspends, resumes) = *h.counters.lock().unwrap_or_else(|e| e.into_inner());
IdleStatus {
enabled: true,
hidden,
suspended,
suspend_after_ms: h.suspend_after.as_millis() as u64,
suspends,
resumes,
native_suspend: cfg!(windows),
}
}
None => IdleStatus {
enabled: false,
hidden: false,
suspended: false,
suspend_after_ms: 0,
suspends: 0,
resumes: 0,
native_suspend: cfg!(windows),
},
}
}
fn emit(app: &AppHandle<Wry>, host: &IdleHost, state: &str) {
if host.options.events.emits(cfg!(windows)) {
let _ = app.emit(EVENT_IDLE, serde_json::json!({"state": state}));
}
}
pub fn note_hidden(app: &AppHandle<Wry>, controller_already_hidden: bool) {
note_hidden_after(app, controller_already_hidden, None);
}
pub fn note_hidden_after(
app: &AppHandle<Wry>,
controller_already_hidden: bool,
suspend_after: Option<Duration>,
) {
let state = app.state::<ShellState>();
let Some(host) = &state.idle else { return };
let (generation, action) = host
.state
.lock()
.unwrap_or_else(|e| e.into_inner())
.on_hidden();
if action != IdleAction::HideControllerAndStartTimer {
return;
}
emit(app, host, "hidden");
let hidden_at = Instant::now();
let plan = hide_plan(host.options.controller_hide, controller_already_hidden);
if plan.now {
hide_controller(app);
}
let delay = host.options.hide_delay(host.suspend_after, suspend_after);
let app = app.clone();
let _ = std::thread::Builder::new()
.name("rk-shell-idle".into())
.spawn(move || {
std::thread::sleep(delay);
suspend_controller(&app, generation, hidden_at);
});
}
fn hide_controller(app: &AppHandle<Wry>) {
let state = app.state::<ShellState>();
let Some(host) = &state.idle else { return };
let reporter = host.reporter(&state.main_label);
let app = app.clone();
let run = platform::with_controller(&app.clone(), move |c| {
let state = app.state::<ShellState>();
let Some(host) = &state.idle else { return };
let mut applied = host.applied.lock().unwrap_or_else(|e| e.into_inner());
let still_hidden = host
.state
.lock()
.unwrap_or_else(|e| e.into_inner())
.snapshot()
.0;
if !still_hidden {
return;
}
if let Err(f) = applied.hide(c) {
host.reporter(&state.main_label).fail(f.phase, f.error);
}
});
if let Err(e) = run {
reporter.fail(IdlePhase::Suspend, e);
}
}
fn suspend_controller(app: &AppHandle<Wry>, generation: u64, hidden_at: Instant) {
let state = app.state::<ShellState>();
let Some(host) = &state.idle else { return };
let reporter = host.reporter(&state.main_label);
let handle = app.clone();
let run = platform::with_controller(app, move |c| {
let state = handle.state::<ShellState>();
let Some(host) = &state.idle else { return };
let mut applied = host.applied.lock().unwrap_or_else(|e| e.into_inner());
let action = host
.state
.lock()
.unwrap_or_else(|e| e.into_inner())
.on_timer(generation);
if action != IdleAction::SuspendNow {
return;
}
let now = Instant::now();
match applied.hide_and_suspend(c, now) {
Ok(()) => {
host.counters.lock().unwrap_or_else(|e| e.into_inner()).0 += 1;
emit(&handle, host, "suspended");
host.transition(
IdleTransitionKind::Suspended,
&state.main_label,
now.saturating_duration_since(hidden_at),
);
}
Err(f) => {
host.state
.lock()
.unwrap_or_else(|e| e.into_inner())
.on_suspend_failed();
host.reporter(&state.main_label).fail(f.phase, f.error);
}
}
});
if let Err(e) = run {
reporter.fail(IdlePhase::Suspend, e);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MinimizeAction {
Hide {
delay: Duration,
},
Visible,
None,
}
pub fn minimize_action(
host_default: Duration,
step: MinimizeStep,
per_hide: Option<Duration>,
) -> MinimizeAction {
match step {
MinimizeStep::Start => MinimizeAction::Hide {
delay: effective_delay(host_default, per_hide),
},
MinimizeStep::End => MinimizeAction::Visible,
MinimizeStep::None => MinimizeAction::None,
}
}
pub fn note_minimized(app: &AppHandle<Wry>, minimized: bool) {
note_minimized_after(app, minimized, None);
}
pub fn note_minimized_after(
app: &AppHandle<Wry>,
minimized: bool,
suspend_after: Option<Duration>,
) {
let state = app.state::<ShellState>();
let Some(host) = &state.idle else { return };
if !host.options.start_on_minimize {
return;
}
let step = host.minimize.step(minimized);
let per_hide = match step {
MinimizeStep::Start => Some(host.options.hide_delay(host.suspend_after, suspend_after)),
MinimizeStep::End | MinimizeStep::None => suspend_after,
};
match minimize_action(host.suspend_after, step, per_hide) {
MinimizeAction::Hide { delay } => note_hidden_after(app, false, Some(delay)),
MinimizeAction::Visible => note_visible(app),
MinimizeAction::None => {}
}
}
pub fn note_visible(app: &AppHandle<Wry>) {
let state = app.state::<ShellState>();
let Some(host) = &state.idle else { return };
host.minimize.clear();
let action = host
.state
.lock()
.unwrap_or_else(|e| e.into_inner())
.on_visible();
let reporter = host.reporter(&state.main_label);
let handle = app.clone();
let run = platform::with_controller(app, move |c| {
let state = handle.state::<ShellState>();
let Some(host) = &state.idle else { return };
let mut applied = host.applied.lock().unwrap_or_else(|e| e.into_inner());
let before_hidden = applied.hidden;
let now = Instant::now();
match applied.resume_and_show(c, now) {
Ok(step) => match applied.classify(before_hidden, step) {
ResumeStep::Resumed(for_) => {
host.counters.lock().unwrap_or_else(|e| e.into_inner()).1 += 1;
emit(&handle, host, "resumed");
host.transition(IdleTransitionKind::Resumed, &state.main_label, for_);
}
ResumeStep::Shown => emit(&handle, host, "visible"),
ResumeStep::Nothing => {
if matches!(
action,
IdleAction::ShowController | IdleAction::ResumeAndShowController
) {
emit(&handle, host, "visible");
}
}
},
Err(f) => host.reporter(&state.main_label).fail(f.phase, f.error),
}
});
if let Err(e) = run {
reporter.fail(IdlePhase::Resume, e);
}
}
#[cfg(windows)]
mod platform {
use crate::winapi::webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2_3;
use crate::winapi::windows::core::Interface;
use tauri::{AppHandle, Manager, Wry};
use super::Controller;
use crate::ShellState;
struct Wv2<'a> {
visible: &'a dyn Fn(bool) -> Result<(), String>,
suspend: &'a dyn Fn() -> Result<(), String>,
resume: &'a dyn Fn() -> Result<(), String>,
}
impl Controller for Wv2<'_> {
fn set_visible(&mut self, visible: bool) -> Result<(), String> {
(self.visible)(visible)
}
fn try_suspend(&mut self) -> Result<(), String> {
(self.suspend)()
}
fn resume(&mut self) -> Result<(), String> {
(self.resume)()
}
}
pub fn with_controller(
app: &AppHandle<Wry>,
f: impl FnOnce(&mut dyn Controller) + Send + 'static,
) -> Result<(), String> {
let label = app.state::<ShellState>().main_label.clone();
let w = app
.get_webview_window(&label)
.ok_or_else(|| "main window not found".to_string())?;
let report = app
.state::<ShellState>()
.idle
.as_ref()
.map(|h| h.reporter(&label));
w.with_webview(move |webview| {
let controller = webview.controller();
let visible = |v: bool| -> Result<(), String> {
unsafe { controller.SetIsVisible(v) }.map_err(|e| e.to_string())
};
let core3 = || -> Result<ICoreWebView2_3, String> {
let core = unsafe { controller.CoreWebView2() }.map_err(|e| e.to_string())?;
core.cast::<ICoreWebView2_3>().map_err(|e| e.to_string())
};
let suspend = || -> Result<(), String> {
let core3 = core3()?;
let completion = report.clone();
let handler = crate::winapi::webview2_com::TrySuspendCompletedHandler::create(
Box::new(move |error, _suspended| {
if let (Err(e), Some(r)) = (error, &completion) {
r.fail(super::IdlePhase::Suspend, e);
}
Ok(())
}),
);
unsafe { core3.TrySuspend(&handler) }.map_err(|e| e.to_string())
};
let resume = || -> Result<(), String> {
let core3 = core3()?;
unsafe { core3.Resume() }.map_err(|e| e.to_string())
};
let mut c = Wv2 {
visible: &visible,
suspend: &suspend,
resume: &resume,
};
f(&mut c);
})
.map_err(|e| e.to_string())
}
}
#[cfg(not(windows))]
mod platform {
use tauri::{AppHandle, Wry};
use super::Controller;
struct Noop;
impl Controller for Noop {
fn set_visible(&mut self, _: bool) -> Result<(), String> {
Ok(())
}
fn try_suspend(&mut self) -> Result<(), String> {
Ok(())
}
fn resume(&mut self) -> Result<(), String> {
Ok(())
}
}
pub fn with_controller(
_: &AppHandle<Wry>,
f: impl FnOnce(&mut dyn Controller) + Send + 'static,
) -> Result<(), String> {
f(&mut Noop);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
use std::sync::atomic::AtomicUsize;
#[derive(Default)]
struct Fake {
calls: Vec<&'static str>,
failing: HashSet<&'static str>,
}
impl Fake {
fn failing(names: &[&'static str]) -> Self {
Self {
failing: names.iter().copied().collect(),
..Self::default()
}
}
fn call(&mut self, name: &'static str) -> Result<(), String> {
self.calls.push(name);
if self.failing.contains(name) {
Err(format!("{name} failed"))
} else {
Ok(())
}
}
}
impl Controller for Fake {
fn set_visible(&mut self, visible: bool) -> Result<(), String> {
self.call(if visible { "show" } else { "hide" })
}
fn try_suspend(&mut self) -> Result<(), String> {
self.call("suspend")
}
fn resume(&mut self) -> Result<(), String> {
self.call("resume")
}
}
#[test]
fn suspend_is_hide_then_suspend() {
let (mut a, mut c) = (Applied::default(), Fake::default());
a.hide_and_suspend(&mut c, Instant::now()).unwrap();
assert_eq!(c.calls, ["hide", "suspend"]);
assert!(a.hidden && a.suspended);
}
#[test]
fn a_failed_hide_means_no_suspend_and_a_running_webview() {
let (mut a, mut c) = (Applied::default(), Fake::failing(&["hide"]));
let f = a.hide_and_suspend(&mut c, Instant::now()).unwrap_err();
assert_eq!(f.phase, IdlePhase::Suspend);
assert_eq!(c.calls, ["hide"], "TrySuspend must not run");
assert!(!a.hidden && !a.suspended);
}
#[test]
fn an_immediate_hide_is_not_repeated_by_the_suspend() {
let (mut a, mut c) = (Applied::default(), Fake::default());
a.hide(&mut c).unwrap();
a.hide_and_suspend(&mut c, Instant::now()).unwrap();
assert_eq!(c.calls, ["hide", "suspend"]);
}
#[test]
fn a_failed_immediate_hide_is_retried_by_the_suspend() {
let (mut a, mut c) = (Applied::default(), Fake::failing(&["hide"]));
assert!(a.hide(&mut c).is_err());
c.failing.clear();
a.hide_and_suspend(&mut c, Instant::now()).unwrap();
assert_eq!(c.calls, ["hide", "hide", "suspend"]);
}
#[test]
fn a_failed_try_suspend_leaves_the_controller_hidden_not_suspended() {
let (mut a, mut c) = (Applied::default(), Fake::failing(&["suspend"]));
assert!(a.hide_and_suspend(&mut c, Instant::now()).is_err());
assert!(a.hidden && !a.suspended);
c.calls.clear();
assert_eq!(
a.resume_and_show(&mut c, Instant::now()).unwrap(),
ResumeStep::Nothing
);
assert_eq!(c.calls, ["show"]);
}
#[test]
fn resume_is_resume_then_show() {
let (mut a, mut c) = (Applied::default(), Fake::default());
let t0 = Instant::now();
a.hide_and_suspend(&mut c, t0).unwrap();
c.calls.clear();
let step = a
.resume_and_show(&mut c, t0 + Duration::from_secs(5))
.unwrap();
assert_eq!(c.calls, ["resume", "show"]);
assert_eq!(step, ResumeStep::Resumed(Duration::from_secs(5)));
assert!(!a.hidden && !a.suspended);
}
#[test]
fn a_failed_resume_means_no_show_and_an_error_then_retries() {
let (mut a, mut c) = (Applied::default(), Fake::default());
a.hide_and_suspend(&mut c, Instant::now()).unwrap();
c.calls.clear();
c.failing.insert("resume");
let f = a.resume_and_show(&mut c, Instant::now()).unwrap_err();
assert_eq!(f.phase, IdlePhase::Resume);
assert_eq!(c.calls, ["resume"], "no show after a failed resume");
assert!(a.hidden && a.suspended, "still pending");
c.failing.clear();
c.calls.clear();
assert!(matches!(
a.resume_and_show(&mut c, Instant::now()),
Ok(ResumeStep::Resumed(_))
));
assert_eq!(c.calls, ["resume", "show"]);
}
#[test]
fn a_failed_show_is_surfaced_and_retried_on_the_next_request_without_resuming_again() {
let (mut a, mut c) = (Applied::default(), Fake::default());
a.hide_and_suspend(&mut c, Instant::now()).unwrap();
c.calls.clear();
c.failing.insert("show");
let f = a.resume_and_show(&mut c, Instant::now()).unwrap_err();
assert_eq!(f.phase, IdlePhase::Resume);
assert_eq!(c.calls, ["resume", "show"]);
assert!(a.hidden && !a.suspended);
c.failing.clear();
c.calls.clear();
let step = a.resume_and_show(&mut c, Instant::now()).unwrap();
assert_eq!(c.calls, ["show"]);
assert!(
matches!(step, ResumeStep::Resumed(_)),
"resumed hook still fires once"
);
c.calls.clear();
assert_eq!(
a.resume_and_show(&mut c, Instant::now()).unwrap(),
ResumeStep::Nothing
);
assert!(c.calls.is_empty());
}
#[test]
fn resume_with_nothing_suspended_makes_no_call() {
let (mut a, mut c) = (Applied::default(), Fake::default());
assert_eq!(
a.resume_and_show(&mut c, Instant::now()).unwrap(),
ResumeStep::Nothing
);
assert!(c.calls.is_empty());
}
#[test]
fn a_hide_alone_is_undone_by_a_show_alone() {
let (mut a, mut c) = (Applied::default(), Fake::default());
a.hide(&mut c).unwrap();
c.calls.clear();
let step = a.resume_and_show(&mut c, Instant::now()).unwrap();
assert_eq!(c.calls, ["show"]);
assert_eq!(a.classify(true, step), ResumeStep::Shown);
}
#[test]
fn a_failed_suspend_is_forgotten_by_the_state_machine() {
let mut s = IdleState::default();
let (g, _) = s.on_hidden();
assert_eq!(s.on_timer(g), IdleAction::SuspendNow);
s.on_suspend_failed();
assert_eq!(s.snapshot(), (true, false));
let (_, action) = s.on_hidden();
assert_eq!(action, IdleAction::HideControllerAndStartTimer);
}
#[test]
fn the_transition_hook_fires_once_per_call_and_is_silent_when_unset() {
let count = Arc::new(AtomicUsize::new(0));
let seen = Arc::new(Mutex::new(Vec::new()));
let (c2, s2) = (count.clone(), seen.clone());
let host = IdleHost::new(Duration::from_secs(1)).with_on_transition(Some(Arc::new(
move |t: &IdleTransition| {
c2.fetch_add(1, Ordering::SeqCst);
s2.lock().unwrap().push(t.clone());
},
)));
host.transition(
IdleTransitionKind::Suspended,
"main",
Duration::from_secs(30),
);
host.transition(IdleTransitionKind::Resumed, "main", Duration::from_secs(4));
assert_eq!(count.load(Ordering::SeqCst), 2);
let seen = seen.lock().unwrap();
assert_eq!(seen[0].kind, IdleTransitionKind::Suspended);
assert_eq!(seen[0].window, "main");
assert_eq!(seen[0].elapsed, Duration::from_secs(30));
assert_eq!(seen[1].kind, IdleTransitionKind::Resumed);
IdleHost::new(Duration::from_secs(1)).transition(
IdleTransitionKind::Suspended,
"main",
Duration::ZERO,
);
}
#[test]
fn the_minimise_trigger_honours_a_per_hide_delay() {
let host = Duration::from_secs(30);
assert_eq!(
minimize_action(host, MinimizeStep::Start, None),
MinimizeAction::Hide { delay: host }
);
assert_eq!(
minimize_action(host, MinimizeStep::Start, Some(Duration::from_secs(2))),
MinimizeAction::Hide {
delay: Duration::from_secs(2)
}
);
assert_eq!(
minimize_action(
host,
MinimizeStep::Start,
Some(Duration::from_secs(10 * 24 * 3600))
),
MinimizeAction::Hide {
delay: MAX_SUSPEND_AFTER
}
);
assert_eq!(
minimize_action(host, MinimizeStep::End, Some(Duration::ZERO)),
MinimizeAction::Visible
);
assert_eq!(
minimize_action(host, MinimizeStep::None, None),
MinimizeAction::None
);
}
}