A custom OS for the xteink x4 ebook reader
1// app lifecycle manager: nav stack, dispatch, font propagation, draw
2//
3// all dispatch is static (monomorphized via with_app!); no dyn, no vtable
4// loading indicator is drawn between app content and overlays so it
5// sits on top of page content but under quick menu and button bumps
6
7use crate::apps::files::FilesApp;
8use crate::apps::home::HomeApp;
9use crate::apps::reader::ReaderApp;
10use crate::apps::settings::SettingsApp;
11use crate::apps::stats::StatsApp;
12use crate::apps::{
13 App, AppContext, AppId, DeferredPersistenceReason, Launcher, PendingSetting, Redraw, Transition,
14};
15use esp_hal::delay::Delay;
16
17use crate::apps::widgets::quick_menu::{MAX_APP_ACTIONS, QuickMenuResult};
18use crate::apps::widgets::{ButtonFeedback, QuickMenu};
19use crate::board::action::{Action, ActionEvent, ButtonMapper};
20use crate::board::{Epd, SCREEN_H, SCREEN_W};
21use crate::drivers::input::Event;
22use crate::drivers::sdcard::SdStorage;
23use crate::drivers::strip::StripBuffer;
24use crate::fonts;
25use crate::kernel::KernelHandle;
26use crate::kernel::app::AppLayer;
27use crate::kernel::bookmarks::BookmarkCache;
28use crate::kernel::config::{SystemSettings, WifiConfig};
29use crate::kernel::input_policy::SemanticInput;
30use crate::ui::Region;
31
32// monomorphized dispatch from AppId to concrete app type
33macro_rules! with_app {
34 ($id:expr, $mgr:expr, |$app:ident| $body:expr) => {
35 match $id {
36 AppId::Home => {
37 let $app = &mut *$mgr.home;
38 $body
39 }
40 AppId::Files => {
41 let $app = &mut *$mgr.files;
42 $body
43 }
44 AppId::Reader => {
45 let $app = &mut *$mgr.reader;
46 $body
47 }
48 AppId::Settings => {
49 let $app = &mut *$mgr.settings;
50 $body
51 }
52 AppId::Stats => {
53 let $app = &mut *$mgr.stats;
54 $body
55 }
56 AppId::Upload => {
57 unreachable!("Upload mode is handled outside the app dispatch loop");
58 }
59 }
60 };
61}
62
63// shared-ref variant for read-only dispatch (draw, quick_actions)
64macro_rules! with_app_ref {
65 ($id:expr, $mgr:expr, |$app:ident| $body:expr) => {
66 match $id {
67 AppId::Home => {
68 let $app = &*$mgr.home;
69 $body
70 }
71 AppId::Files => {
72 let $app = &*$mgr.files;
73 $body
74 }
75 AppId::Reader => {
76 let $app = &*$mgr.reader;
77 $body
78 }
79 AppId::Settings => {
80 let $app = &*$mgr.settings;
81 $body
82 }
83 AppId::Stats => {
84 let $app = &*$mgr.stats;
85 $body
86 }
87 AppId::Upload => {
88 unreachable!("Upload mode is handled outside the app dispatch loop");
89 }
90 }
91 };
92}
93
94#[allow(unused_imports)]
95pub(crate) use with_app;
96#[allow(unused_imports)]
97pub(crate) use with_app_ref;
98
99pub struct AppManager {
100 pub launcher: &'static mut Launcher,
101
102 pub home: &'static mut HomeApp,
103 pub files: &'static mut FilesApp,
104 pub reader: &'static mut ReaderApp,
105 pub settings: &'static mut SettingsApp,
106 pub stats: &'static mut StatsApp,
107
108 pub quick_menu: &'static mut QuickMenu,
109 pub bumps: &'static mut ButtonFeedback,
110
111 pub mapper: ButtonMapper,
112}
113
114impl AppManager {
115 #[allow(clippy::too_many_arguments)]
116 pub fn new(
117 launcher: &'static mut Launcher,
118 home: &'static mut HomeApp,
119 files: &'static mut FilesApp,
120 reader: &'static mut ReaderApp,
121 settings: &'static mut SettingsApp,
122 stats: &'static mut StatsApp,
123 quick_menu: &'static mut QuickMenu,
124 bumps: &'static mut ButtonFeedback,
125 mapper: ButtonMapper,
126 ) -> Self {
127 Self {
128 launcher,
129 home,
130 files,
131 reader,
132 settings,
133 stats,
134 quick_menu,
135 bumps,
136 mapper,
137 }
138 }
139
140 #[inline]
141 pub fn active(&self) -> AppId {
142 self.launcher.active()
143 }
144
145 #[inline]
146 pub fn ctx(&self) -> &AppContext {
147 &self.launcher.ctx
148 }
149
150 #[inline]
151 pub fn ctx_mut(&mut self) -> &mut AppContext {
152 &mut self.launcher.ctx
153 }
154
155 #[inline]
156 pub fn has_redraw(&self) -> bool {
157 self.launcher.ctx.has_redraw()
158 }
159
160 #[inline]
161 pub fn take_redraw(&mut self) -> Redraw {
162 self.launcher.ctx.take_redraw()
163 }
164
165 #[inline]
166 pub fn request_full_redraw(&mut self) {
167 self.launcher.ctx.request_full_redraw();
168 }
169
170 #[inline]
171 pub fn apply_nav(&mut self, transition: Transition) -> Option<crate::apps::NavEvent> {
172 self.launcher.apply(transition)
173 }
174
175 pub fn load_eager_settings(&mut self, k: &mut KernelHandle<'_>) {
176 self.settings.load_eager(k);
177 self.propagate_fonts();
178 self.sync_button_config();
179 }
180
181 // sync button mapper and label widget from settings
182 fn sync_button_config(&mut self) {
183 let swap = self.settings.system_settings().swap_buttons;
184 self.mapper.set_swap(swap);
185 if self.bumps.set_swap(swap) {
186 // labels changed, need to redraw the button bar
187 self.launcher.ctx.mark_dirty(crate::ui::Region::new(
188 0,
189 crate::board::SCREEN_H - crate::ui::BUTTON_BAR_H,
190 crate::board::SCREEN_W,
191 crate::ui::BUTTON_BAR_H,
192 ));
193 }
194 }
195
196 pub fn load_home_recent(&mut self, k: &mut KernelHandle<'_>) {
197 self.home.load_recent(k);
198 }
199
200 pub fn enter_initial(&mut self, k: &mut KernelHandle<'_>) {
201 self.home.on_enter(&mut self.launcher.ctx, k);
202 }
203
204 // save active app state to bookmark cache before sleep
205 pub fn save_active_state(&mut self, bm: &mut crate::kernel::bookmarks::BookmarkCache) {
206 let active = self.launcher.active();
207 with_app!(active, self, |app| app.save_state(bm));
208 }
209
210 // collect session state to RTC memory struct before sleep
211 pub fn collect_session(&self, session: &mut crate::kernel::rtc_session::RtcSession) {
212 use crate::kernel::rtc_session::MAX_NAV_STACK;
213
214 // save navigation stack
215 session.nav_depth = self.launcher.depth() as u8;
216 for i in 0..MAX_NAV_STACK {
217 session.nav_stack[i] = if i < self.launcher.depth() {
218 self.launcher.stack_at(i) as u8
219 } else {
220 0
221 };
222 }
223
224 // save reader state
225 session.reader_filename_len = self.reader.filename_len() as u8;
226 let len = session.reader_filename_len as usize;
227 session.reader_filename[..len].copy_from_slice(self.reader.filename_bytes());
228 session.reader_is_epub = self.reader.is_epub() as u8;
229 session.reader_chapter = self.reader.chapter();
230 session.reader_page = self.reader.page() as u16;
231 session.reader_byte_offset = self.reader.byte_offset();
232 session.reader_font_size = self.reader.font_size_idx();
233
234 // save files state
235 session.files_scroll = self.files.scroll() as u16;
236 session.files_selected = self.files.selected() as u8;
237 session.files_total = self.files.total() as u16;
238
239 // save home state
240 session.home_state = self.home.state_id();
241 session.home_selected = self.home.selected() as u8;
242 session.home_bm_selected = self.home.bm_selected() as u8;
243 session.home_bm_scroll = self.home.bm_scroll() as u8;
244
245 // save settings cache
246 let ss = self.settings.system_settings();
247 session.settings_sleep_timeout = ss.sleep_timeout;
248 session.settings_ghost_clear = ss.ghost_clear_every;
249 session.settings_book_font = ss.book_font_size_idx;
250 session.settings_ui_font = ss.ui_font_size_idx;
251 session.settings_valid = 1;
252
253 log::debug!(
254 "session: collected nav_depth={} active={:?}",
255 session.nav_depth,
256 self.launcher.active()
257 );
258 }
259
260 // restore session from RTC memory; returns true if successful
261 //
262 // IMPORTANT: this does NOT call on_enter() on any app. the previous
263 // code called on_enter() after restore_state(), which clobbered the
264 // restored values (home.selected, files.scroll, reader.chapter, etc).
265 // instead, each app's restore_state() is now responsible for setting
266 // up ALL state needed to resume, and the reader enters at NeedBookmark
267 // with its chapter/offset pre-populated from the RTC session.
268 pub fn apply_session(
269 &mut self,
270 session: &crate::kernel::rtc_session::RtcSession,
271 k: &mut KernelHandle<'_>,
272 ) -> bool {
273 // validate session data
274 if session.nav_depth == 0 || session.nav_depth > 4 {
275 log::warn!("session: invalid nav_depth {}", session.nav_depth);
276 return false;
277 }
278
279 // restore navigation stack
280 self.launcher.restore_stack(
281 session.nav_depth as usize,
282 &session.nav_stack,
283 |id| match id {
284 0 => AppId::Home,
285 1 => AppId::Files,
286 2 => AppId::Reader,
287 3 => AppId::Settings,
288 4 => AppId::Stats,
289 _ => AppId::Home,
290 },
291 );
292
293 log::debug!(
294 "session: restored nav stack depth={} active={:?}",
295 session.nav_depth,
296 self.launcher.active()
297 );
298
299 // restore home state (always in stack)
300 // restore_state sets state/selected/bm cursors without
301 // resetting them like on_enter() would
302 self.home.restore_state(
303 session.home_state,
304 session.home_selected as usize,
305 session.home_bm_selected as usize,
306 session.home_bm_scroll as usize,
307 );
308 // battery percentage for status display
309 self.home.set_battery(k.battery_mv());
310
311 // restore files state if in stack
312 if self.launcher.contains(AppId::Files) {
313 self.files.restore_state(
314 session.files_scroll as usize,
315 session.files_selected as usize,
316 session.files_total as usize,
317 );
318 }
319
320 // restore reader state if active or in stack
321 if self.launcher.active() == AppId::Reader || self.launcher.contains(AppId::Reader) {
322 let filename = &session.reader_filename[..session.reader_filename_len as usize];
323 self.reader.restore_state(
324 filename,
325 session.reader_is_epub != 0,
326 session.reader_chapter,
327 session.reader_page as usize,
328 session.reader_byte_offset,
329 session.reader_font_size,
330 );
331
332 // Wake-to-reader should use the rich cover loading screen on
333 // the very first frame instead of flashing a generic resume
334 // screen before background restore reloads cached assets.
335 if self.launcher.active() == AppId::Reader {
336 self.reader.prepare_restore_loading_screen(k);
337 }
338 }
339
340 // propagate fonts (uses settings already loaded)
341 self.propagate_fonts();
342
343 // set loading indicator for reader if it's the active app,
344 // so the first frame shows "Opening" instead of blank content
345 if self.launcher.active() == AppId::Reader {
346 self.launcher
347 .ctx
348 .set_loading(crate::apps::reader::LOADING_REGION, "Resuming", 0);
349 }
350
351 // mark full redraw needed — the next render will draw the
352 // active app's content using the restored state
353 self.launcher.ctx.request_full_redraw();
354
355 log::debug!(
356 "session: restore complete, active={:?}",
357 self.launcher.active()
358 );
359
360 true
361 }
362
363 /// Open the quick menu for the active app.
364 fn open_quick_menu(&mut self) {
365 let active = self.launcher.active();
366 let actions: &[_] = with_app!(active, self, |app| app.quick_actions());
367 self.quick_menu.show(actions);
368 self.launcher.ctx.mark_dirty(self.quick_menu.region());
369 }
370
371 /// Close the quick menu, propagating any changed cycle values
372 /// and pending settings to the active app.
373 ///
374 /// Idempotent: safe to call even if `hide()` was already called
375 /// (e.g. from `QuickMenu::on_action`).
376 fn close_quick_menu(&mut self) {
377 let region = self.quick_menu.region();
378 self.quick_menu.hide();
379 self.sync_quick_menu();
380 self.launcher.ctx.mark_dirty(region);
381 }
382
383 /// Handle a semantic input from the input policy layer.
384 pub fn dispatch_semantic_input(&mut self, input: SemanticInput) -> Transition {
385 match input {
386 SemanticInput::MenuTap => {
387 if self.quick_menu.open {
388 self.close_quick_menu();
389 } else {
390 self.open_quick_menu();
391 }
392 Transition::None
393 }
394 }
395 }
396
397 // power-button long-press must be intercepted by the scheduler
398 // before calling this method
399 pub fn dispatch_event(&mut self, hw_event: Event, bm_cache: &mut BookmarkCache) -> Transition {
400 let event = self.mapper.map_event(hw_event);
401
402 if self.quick_menu.open {
403 return self.handle_quick_menu(event, bm_cache);
404 }
405
406 if matches!(event, ActionEvent::Press(Action::Menu)) {
407 self.open_quick_menu();
408 return Transition::None;
409 }
410
411 let active = self.launcher.active();
412 with_app!(active, self, |app| {
413 app.on_event(event, &mut self.launcher.ctx)
414 })
415 }
416
417 fn handle_quick_menu(
418 &mut self,
419 event: ActionEvent,
420 bm_cache: &mut BookmarkCache,
421 ) -> Transition {
422 let action = match event {
423 ActionEvent::Press(a) | ActionEvent::Repeat(a) => a,
424 _ => return Transition::None,
425 };
426
427 let result = self.quick_menu.on_action(action);
428
429 match result {
430 QuickMenuResult::Consumed => {
431 if self.quick_menu.dirty {
432 self.launcher.ctx.mark_dirty(self.quick_menu.region());
433 self.quick_menu.dirty = false;
434 }
435 Transition::None
436 }
437
438 QuickMenuResult::Close => {
439 // hide() already called by on_action; close_quick_menu
440 // is idempotent and handles sync + dirty marking
441 self.close_quick_menu();
442 Transition::None
443 }
444
445 QuickMenuResult::RefreshScreen => {
446 self.close_quick_menu();
447 self.launcher.ctx.request_full_redraw();
448 Transition::None
449 }
450
451 QuickMenuResult::GoHome => {
452 self.close_quick_menu();
453 Transition::Home
454 }
455
456 QuickMenuResult::AppTrigger(id) => {
457 let active = self.launcher.active();
458 self.close_quick_menu();
459
460 with_app!(active, self, |app| {
461 app.on_quick_trigger(id, &mut self.launcher.ctx);
462 // Save app state after trigger (e.g. font change
463 // may invalidate the reader's current page offset).
464 app.save_state(bm_cache);
465 });
466
467 Transition::None
468 }
469 }
470 }
471
472 /// Flush deferred persistence for all app singletons.
473 ///
474 /// Dispatches to every concrete app so that failed flushes can
475 /// retry even when the owning app is suspended (e.g. reader
476 /// dirty state retries while Home is active).
477 pub fn flush_deferred_persistence(
478 &mut self,
479 k: &mut KernelHandle<'_>,
480 reason: DeferredPersistenceReason,
481 ) -> crate::error::Result<()> {
482 // today only ReaderApp does real work; others inherit the
483 // default no-op. iterate all singletons for retry semantics.
484 let mut first_error = None;
485 for &id in &[
486 AppId::Home,
487 AppId::Files,
488 AppId::Reader,
489 AppId::Settings,
490 AppId::Stats,
491 ] {
492 let result = with_app!(id, self, |app| app.flush_deferred_persistence(k, reason));
493 if let Err(e) = result {
494 log::warn!("flush_deferred_persistence({:?}): {}", id, e);
495 first_error.get_or_insert(e);
496 }
497 }
498
499 if let Some(err) = first_error {
500 Err(err)
501 } else {
502 Ok(())
503 }
504 }
505
506 pub fn apply_transition(&mut self, transition: Transition, k: &mut KernelHandle<'_>) {
507 if let Some(nav) = self.launcher.apply(transition) {
508 log::debug!("app: {:?} -> {:?}", nav.from, nav.to);
509
510 if nav.from != AppId::Upload {
511 with_app!(nav.from, self, |app| app.save_state(k.bookmark_cache_mut()));
512
513 // force flush deferred persistence for all app singletons
514 // before leaving the current app so inactive retries still
515 // run and reader dirty state gets one last chance before the
516 // singleton is potentially reused for another book.
517 if let Err(e) =
518 self.flush_deferred_persistence(k, DeferredPersistenceReason::Transition)
519 {
520 log::warn!("flush on leave {:?}: {}", nav.from, e);
521 }
522
523 with_app!(nav.from, self, |app| {
524 if nav.suspend {
525 app.on_suspend();
526 } else {
527 app.on_exit();
528 }
529 });
530 }
531
532 self.propagate_fonts();
533 self.launcher.ctx.clear_loading();
534
535 if nav.to != AppId::Upload {
536 if nav.resume {
537 with_app!(nav.to, self, |app| {
538 app.on_resume(&mut self.launcher.ctx, k)
539 });
540 } else {
541 with_app!(nav.to, self, |app| {
542 app.on_enter(&mut self.launcher.ctx, k)
543 });
544 }
545 }
546
547 if nav.resume {
548 self.launcher
549 .ctx
550 .mark_dirty(Region::new(0, 0, SCREEN_W, SCREEN_H));
551 } else {
552 self.launcher.ctx.request_full_redraw();
553 }
554 }
555 }
556
557 pub async fn run_background(&mut self, k: &mut KernelHandle<'_>) {
558 let active = self.launcher.active();
559 with_app!(active, self, |app| {
560 app.background(&mut self.launcher.ctx, k).await
561 });
562
563 for &id in &[AppId::Home, AppId::Files, AppId::Reader, AppId::Settings] {
564 if id != active {
565 with_app!(id, self, |app| {
566 if app.has_background_when_suspended() {
567 app.background_suspended(k);
568 }
569 });
570 }
571 }
572 }
573
574 pub fn draw(&self, strip: &mut StripBuffer) {
575 let active = self.launcher.active();
576 with_app_ref!(active, self, |app| app.draw(strip));
577
578 // loading indicator: after app content, before overlays.
579 // the reader draws its own centered loading screen while a
580 // book is opening, so suppress the generic indicator there.
581 let suppress_loading = active == AppId::Reader && self.reader.shows_loading_screen();
582 if self.launcher.ctx.loading_active() && !suppress_loading {
583 let region = self.launcher.ctx.loading_region();
584 if region.intersects(strip.logical_window()) {
585 crate::ui::LoadingIndicator::new(
586 region,
587 self.launcher.ctx.loading_msg(),
588 self.launcher.ctx.loading_pct(),
589 )
590 .draw(strip);
591 }
592 }
593
594 if self.quick_menu.open {
595 self.quick_menu.draw(strip);
596 }
597
598 let hide = with_app_ref!(active, self, |app| app.hide_button_bar());
599 if !hide {
600 self.bumps.draw(strip);
601 }
602 }
603
604 pub fn propagate_fonts(&mut self) {
605 let ss = self.settings.system_settings();
606 let ui_idx = ss.ui_font_size_idx;
607 let book_idx = ss.book_font_size_idx;
608 let theme_idx = ss.reading_theme;
609 let reader_status = ss.reader_status;
610 let text_alignment = ss.text_alignment;
611
612 self.home.set_ui_font_size(ui_idx);
613 self.files.set_ui_font_size(ui_idx);
614 self.settings.set_ui_font_size(ui_idx);
615 self.stats.set_ui_font_size(ui_idx);
616 self.reader.set_book_font_size(book_idx);
617 self.reader.set_reading_theme(theme_idx);
618 self.reader.set_show_chrome(reader_status);
619 self.reader.set_text_alignment(text_alignment);
620
621 let chrome = fonts::chrome_font();
622 self.reader.set_chrome_font(chrome);
623 self.quick_menu.set_chrome_font(chrome);
624 self.bumps.set_chrome_font(chrome);
625 }
626
627 fn sync_quick_menu(&mut self) {
628 let active = self.launcher.active();
629
630 for id in 0..MAX_APP_ACTIONS as u8 {
631 if let Some(value) = self.quick_menu.app_cycle_value(id) {
632 with_app!(active, self, |app| {
633 app.on_quick_cycle_update(id, value, &mut self.launcher.ctx);
634 });
635 }
636 }
637
638 let pending = with_app!(active, self, |app| app.pending_setting());
639 if let Some(setting) = pending {
640 match setting {
641 PendingSetting::BookFontSize(idx) => {
642 let ss = self.settings.system_settings_mut();
643 if ss.book_font_size_idx != idx {
644 ss.book_font_size_idx = idx;
645 self.settings.mark_save_needed();
646 }
647 }
648 }
649 }
650 }
651
652 #[inline]
653 pub fn system_settings(&self) -> &crate::kernel::config::SystemSettings {
654 self.settings.system_settings()
655 }
656
657 #[inline]
658 pub fn settings_loaded(&self) -> bool {
659 self.settings.is_loaded()
660 }
661
662 #[inline]
663 pub fn settings_generation(&self) -> u32 {
664 self.settings.generation()
665 }
666
667 #[inline]
668 pub fn wifi_config(&self) -> &crate::kernel::config::WifiConfig {
669 self.settings.wifi_config()
670 }
671
672 pub fn ghost_clear_every(&self) -> u32 {
673 if self.settings.is_loaded() {
674 self.settings.system_settings().ghost_clear_every as u32
675 } else {
676 crate::kernel::DEFAULT_GHOST_CLEAR_EVERY
677 }
678 }
679}
680
681impl AppLayer for AppManager {
682 type Id = AppId;
683
684 #[inline]
685 fn active(&self) -> AppId {
686 self.launcher.active()
687 }
688
689 fn dispatch_event(&mut self, event: Event, bm: &mut BookmarkCache) -> Transition {
690 AppManager::dispatch_event(self, event, bm)
691 }
692
693 fn dispatch_semantic(&mut self, input: SemanticInput) -> Transition {
694 AppManager::dispatch_semantic_input(self, input)
695 }
696
697 fn apply_transition(&mut self, t: Transition, k: &mut KernelHandle<'_>) {
698 AppManager::apply_transition(self, t, k);
699 }
700
701 async fn run_background(&mut self, k: &mut KernelHandle<'_>) {
702 AppManager::run_background(self, k).await;
703 }
704
705 fn draw(&self, strip: &mut StripBuffer) {
706 AppManager::draw(self, strip);
707 }
708
709 #[inline]
710 fn has_redraw(&self) -> bool {
711 self.launcher.ctx.has_redraw()
712 }
713
714 #[inline]
715 fn take_redraw(&mut self) -> Redraw {
716 self.launcher.ctx.take_redraw()
717 }
718
719 #[inline]
720 fn request_full_redraw(&mut self) {
721 self.launcher.ctx.request_full_redraw();
722 }
723
724 #[inline]
725 fn ctx_mut(&mut self) -> &mut AppContext {
726 &mut self.launcher.ctx
727 }
728
729 fn system_settings(&self) -> &SystemSettings {
730 self.settings.system_settings()
731 }
732
733 fn settings_loaded(&self) -> bool {
734 self.settings.is_loaded()
735 }
736
737 fn settings_generation(&self) -> u32 {
738 self.settings.generation()
739 }
740
741 fn on_swap_buttons_changed(&mut self, swap: bool) {
742 self.mapper.set_swap(swap);
743 if self.bumps.set_swap(swap) {
744 self.launcher.ctx.mark_dirty(crate::ui::Region::new(
745 0,
746 crate::board::SCREEN_H - crate::ui::BUTTON_BAR_H,
747 crate::board::SCREEN_W,
748 crate::ui::BUTTON_BAR_H,
749 ));
750 }
751 }
752
753 fn ghost_clear_every(&self) -> u32 {
754 AppManager::ghost_clear_every(self)
755 }
756
757 fn wants_grayscale(&self) -> bool {
758 self.launcher.active() == AppId::Reader
759 && !self.quick_menu.open
760 && self.reader.wants_grayscale()
761 }
762
763 fn wifi_config(&self) -> &WifiConfig {
764 self.settings.wifi_config()
765 }
766
767 fn load_eager_settings(&mut self, k: &mut KernelHandle<'_>) {
768 AppManager::load_eager_settings(self, k);
769 }
770
771 fn load_initial_state(&mut self, k: &mut KernelHandle<'_>) {
772 AppManager::load_home_recent(self, k);
773 }
774
775 fn enter_initial(&mut self, k: &mut KernelHandle<'_>) {
776 AppManager::enter_initial(self, k);
777 }
778
779 fn save_active_state(&mut self, bm: &mut crate::kernel::bookmarks::BookmarkCache) {
780 AppManager::save_active_state(self, bm);
781 }
782
783 fn flush_deferred_persistence(
784 &mut self,
785 k: &mut KernelHandle<'_>,
786 reason: DeferredPersistenceReason,
787 ) -> crate::error::Result<()> {
788 AppManager::flush_deferred_persistence(self, k, reason)
789 }
790
791 fn collect_session(&self, session: &mut crate::kernel::rtc_session::RtcSession) {
792 AppManager::collect_session(self, session);
793 }
794
795 fn apply_session(
796 &mut self,
797 session: &crate::kernel::rtc_session::RtcSession,
798 k: &mut KernelHandle<'_>,
799 ) -> bool {
800 AppManager::apply_session(self, session, k)
801 }
802
803 fn needs_special_mode(&self) -> bool {
804 self.launcher.active() == AppId::Upload
805 }
806
807 async fn run_special_mode(
808 &mut self,
809 epd: &mut Epd,
810 strip: &mut StripBuffer,
811 delay: &mut Delay,
812 sd: &SdStorage,
813 ) {
814 // Safety: WIFI is not owned by any other driver. Upload mode
815 // runs in isolation (the scheduler exits the main dispatch loop
816 // first) and tears down the radio stack before returning. The
817 // peripheral is not accessed again until the next upload session.
818 let wifi = unsafe { esp_hal::peripherals::WIFI::steal() };
819
820 crate::apps::upload::run_upload_mode(
821 wifi,
822 epd,
823 strip,
824 delay,
825 sd,
826 self.settings.system_settings().ui_font_size_idx,
827 &*self.bumps,
828 self.settings.wifi_config(),
829 )
830 .await;
831 }
832
833 fn suppress_deferred_input(&self) -> bool {
834 self.quick_menu.open
835 }
836}