A custom OS for the xteink x4 ebook reader
at main 836 lines 27 kB view raw
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}