A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: implement ActivityManager (#1016)

## Summary

Ref comment:
https://github.com/crosspoint-reader/crosspoint-reader/pull/1010#pullrequestreview-3828854640

This PR introduces `ActivityManager`, which mirrors the same concept of
Activity in Android, where an activity represents a single screen of the
UI. The manager is responsible for launching activities, and ensuring
that only one activity is active at a time.

Main differences from Android's ActivityManager:
- No concept of Bundle or Intent extras
- No onPause/onResume, since we don't have a concept of background
activities
- onActivityResult is implemented via a callback instead of a separate
method, for simplicity

## Key changes

- Single `renderTask` shared across all activities
- No more sub-activity, we manage them using a stack; Results can be
passed via `startActivityForResult` and `setResult`
- Activity can call `finish()` to destroy themself, but the actual
deletion will be handled by `ActivityManager` to avoid `delete this`
pattern

As a bonus: the manager will automatically call `requestUpdate()` when
returning from another activity

## Example usage

**BEFORE**:

```cpp
// caller
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
[this](const bool connected) { onWifiSelectionComplete(connected); }));

// subactivity
onComplete(true); // will eventually call exitActivity(), which deletes the caller instance (dangerous behavior)
```

**AFTER**: (mirrors the `startActivityForResult` and `setResult` from
android)

```cpp
// caller
startActivityForResult(new NetworkModeSelectionActivity(renderer, mappedInput),
[this](const ActivityResult& result) { onNetworkModeSelected(result.selectedNetworkMode); });

// subactivity
ActivityResult result;
result.isCancelled = false;
result.selectedNetworkMode = mode;
setResult(result);
finish(); // signals to ActivityManager to go back to last activity AFTER this function returns
```

TODO:
- [x] Reconsider if the `Intent` is really necessary or it should be
removed (note: it's inspired by
[Intent](https://developer.android.com/guide/components/intents-common)
from Android API) ==> I decided to keep this pattern fr clarity
- [x] Verify if behavior is still correct (i.e. back from sub-activity)
- [x] Refactor the `ActivityWithSubactivity` to just simple `Activity`
--> We are using a stack for keeping track of sub-activity now
- [x] Use single task for rendering --> avoid allocating 8KB stack per
activity
- [x] Implement the idea of [Activity
result](https://developer.android.com/training/basics/intents/result)
--> Allow sub-activity like Wifi to report back the status (connected,
failed, etc)

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **PARTIALLY**, some
repetitive migrations are done by Claude, but I'm the one how ultimately
approve it

---------

Co-authored-by: Zach Nelson <zach@zdnelson.com>

authored by

Xuan-Son Nguyen
Zach Nelson
and committed by
GitHub
c4fc4eff 5b11e45a

+1095 -1180
+14 -47
src/activities/Activity.cpp
··· 1 1 #include "Activity.h" 2 2 3 - #include <HalPowerManager.h> 3 + #include "ActivityManager.h" 4 4 5 - void Activity::renderTaskTrampoline(void* param) { 6 - auto* self = static_cast<Activity*>(param); 7 - self->renderTaskLoop(); 8 - } 5 + void Activity::onEnter() { LOG_DBG("ACT", "Entering activity: %s", name.c_str()); } 9 6 10 - void Activity::renderTaskLoop() { 11 - while (true) { 12 - ulTaskNotifyTake(pdTRUE, portMAX_DELAY); 13 - { 14 - HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering 15 - RenderLock lock(*this); 16 - render(std::move(lock)); 17 - } 18 - } 19 - } 7 + void Activity::onExit() { LOG_DBG("ACT", "Exiting activity: %s", name.c_str()); } 20 8 21 - void Activity::onEnter() { 22 - xTaskCreate(&renderTaskTrampoline, name.c_str(), 23 - 8192, // Stack size 24 - this, // Parameters 25 - 1, // Priority 26 - &renderTaskHandle // Task handle 27 - ); 28 - assert(renderTaskHandle != nullptr && "Failed to create render task"); 29 - LOG_DBG("ACT", "Entering activity: %s", name.c_str()); 30 - } 31 - 32 - void Activity::onExit() { 33 - RenderLock lock(*this); // Ensure we don't delete the task while it's rendering 34 - if (renderTaskHandle) { 35 - vTaskDelete(renderTaskHandle); 36 - renderTaskHandle = nullptr; 37 - } 38 - 39 - LOG_DBG("ACT", "Exiting activity: %s", name.c_str()); 40 - } 41 - 42 - void Activity::requestUpdate() { 43 - // Using direct notification to signal the render task to update 44 - // Increment counter so multiple rapid calls won't be lost 45 - if (renderTaskHandle) { 46 - xTaskNotify(renderTaskHandle, 1, eIncrement); 47 - } 48 - } 9 + void Activity::requestUpdate(bool immediate) { activityManager.requestUpdate(immediate); } 49 10 50 11 void Activity::requestUpdateAndWait() { 51 12 // FIXME @ngxson : properly implement this using freeRTOS notification 13 + activityManager.requestUpdate(true); 52 14 delay(100); 53 15 } 54 16 55 - // RenderLock 17 + void Activity::onGoHome() { activityManager.goHome(); } 56 18 57 - Activity::RenderLock::RenderLock(Activity& activity) : activity(activity) { 58 - xSemaphoreTake(activity.renderingMutex, portMAX_DELAY); 19 + void Activity::onSelectBook(const std::string& path) { activityManager.goToReader(path); } 20 + 21 + void Activity::startActivityForResult(std::unique_ptr<Activity>&& activity, ActivityResultHandler resultHandler) { 22 + this->resultHandler = std::move(resultHandler); 23 + activityManager.pushActivity(std::move(activity)); 59 24 } 60 25 61 - Activity::RenderLock::~RenderLock() { xSemaphoreGive(activity.renderingMutex); } 26 + void Activity::setResult(ActivityResult&& result) { this->result = std::move(result); } 27 + 28 + void Activity::finish() { activityManager.popActivity(); }
+28 -30
src/activities/Activity.h
··· 1 1 #pragma once 2 - #include <HardwareSerial.h> 3 2 #include <Logging.h> 4 - #include <freertos/FreeRTOS.h> 5 - #include <freertos/semphr.h> 6 - #include <freertos/task.h> 7 3 8 4 #include <cassert> 5 + #include <memory> 9 6 #include <string> 10 7 #include <utility> 11 8 9 + #include "ActivityManager.h" // for using the ActivityManager singleton 10 + #include "ActivityResult.h" 12 11 #include "GfxRenderer.h" 13 12 #include "MappedInputManager.h" 13 + #include "RenderLock.h" 14 14 15 15 class Activity { 16 16 protected: ··· 18 18 GfxRenderer& renderer; 19 19 MappedInputManager& mappedInput; 20 20 21 - // Task to render and display the activity 22 - TaskHandle_t renderTaskHandle = nullptr; 23 - [[noreturn]] static void renderTaskTrampoline(void* param); 24 - [[noreturn]] virtual void renderTaskLoop(); 21 + public: 22 + ActivityResultHandler resultHandler; 23 + ActivityResult result; 25 24 26 - // Mutex to protect rendering operations from being deleted mid-render 27 - SemaphoreHandle_t renderingMutex = nullptr; 28 - 29 - public: 30 25 explicit Activity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput) 31 - : name(std::move(name)), renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) { 32 - assert(renderingMutex != nullptr && "Failed to create rendering mutex"); 33 - } 34 - virtual ~Activity() { 35 - vSemaphoreDelete(renderingMutex); 36 - renderingMutex = nullptr; 37 - }; 38 - class RenderLock; 26 + : name(std::move(name)), renderer(renderer), mappedInput(mappedInput) {} 27 + virtual ~Activity() = default; 39 28 virtual void onEnter(); 40 29 virtual void onExit(); 41 30 virtual void loop() {} 42 31 43 32 virtual void render(RenderLock&&) {} 44 - virtual void requestUpdate(); 33 + 34 + // If immediate is true, the update will be triggered immediately. 35 + // Otherwise, it will be deferred until the end of the current loop iteration. 36 + virtual void requestUpdate(bool immediate = false); 37 + 38 + // Request an immediate render and block until it completes. 45 39 virtual void requestUpdateAndWait(); 46 40 47 41 virtual bool skipLoopDelay() { return false; } 48 42 virtual bool preventAutoSleep() { return false; } 49 43 virtual bool isReaderActivity() const { return false; } 50 44 51 - // RAII helper to lock rendering mutex for the duration of a scope. 52 - class RenderLock { 53 - Activity& activity; 45 + // Start a new activity without destroying the current one 46 + // Note: requestUpdate() will be invoked automatically once resultHandler finishes 47 + void startActivityForResult(std::unique_ptr<Activity>&& activity, ActivityResultHandler resultHandler); 48 + 49 + // Set the result to be passed back to the previous activity when this activity finishes 50 + void setResult(ActivityResult&& result); 51 + 52 + // Finish this activity and return to the previous one on the stack (if any) 53 + void finish(); 54 54 55 - public: 56 - explicit RenderLock(Activity& activity); 57 - RenderLock(const RenderLock&) = delete; 58 - RenderLock& operator=(const RenderLock&) = delete; 59 - ~RenderLock(); 60 - }; 55 + // Convenience method to facilitate API transition to ActivityManager 56 + // TODO: remove this in near future 57 + void onGoHome(); 58 + void onSelectBook(const std::string& path); 61 59 };
+252
src/activities/ActivityManager.cpp
··· 1 + #include "ActivityManager.h" 2 + 3 + #include <HalPowerManager.h> 4 + 5 + #include "boot_sleep/BootActivity.h" 6 + #include "boot_sleep/SleepActivity.h" 7 + #include "browser/OpdsBookBrowserActivity.h" 8 + #include "home/HomeActivity.h" 9 + #include "home/MyLibraryActivity.h" 10 + #include "home/RecentBooksActivity.h" 11 + #include "network/CrossPointWebServerActivity.h" 12 + #include "reader/ReaderActivity.h" 13 + #include "settings/SettingsActivity.h" 14 + #include "util/FullScreenMessageActivity.h" 15 + 16 + void ActivityManager::begin() { 17 + xTaskCreate(&renderTaskTrampoline, "ActivityManagerRender", 18 + 8192, // Stack size 19 + this, // Parameters 20 + 1, // Priority 21 + &renderTaskHandle // Task handle 22 + ); 23 + assert(renderTaskHandle != nullptr && "Failed to create render task"); 24 + } 25 + 26 + void ActivityManager::renderTaskTrampoline(void* param) { 27 + auto* self = static_cast<ActivityManager*>(param); 28 + self->renderTaskLoop(); 29 + } 30 + 31 + void ActivityManager::renderTaskLoop() { 32 + while (true) { 33 + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); 34 + // Acquire the lock before reading currentActivity to avoid a TOCTOU race 35 + // where the main task deletes the activity between the null-check and render(). 36 + RenderLock lock; 37 + if (currentActivity) { 38 + HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering 39 + currentActivity->render(std::move(lock)); 40 + } 41 + } 42 + } 43 + 44 + void ActivityManager::loop() { 45 + if (currentActivity) { 46 + // Note: do not hold a lock here, the loop() method must be responsible for acquire one if needed 47 + currentActivity->loop(); 48 + } 49 + 50 + while (pendingAction != PendingAction::None) { 51 + if (pendingAction == PendingAction::Pop) { 52 + RenderLock lock; 53 + 54 + if (!currentActivity) { 55 + // Should never happen in practice 56 + LOG_ERR("ACT", "Pop set but currentActivity is null; ignoring pop request"); 57 + pendingAction = PendingAction::None; 58 + continue; 59 + } 60 + 61 + ActivityResult pendingResult = std::move(currentActivity->result); 62 + 63 + // Destroy the current activity 64 + exitActivity(lock); 65 + pendingAction = PendingAction::None; 66 + 67 + if (stackActivities.empty()) { 68 + LOG_DBG("ACT", "No more activities on stack, going home"); 69 + lock.unlock(); // goHome may acquire its own lock 70 + goHome(); 71 + continue; // Will launch goHome immediately 72 + 73 + } else { 74 + currentActivity = std::move(stackActivities.back()); 75 + stackActivities.pop_back(); 76 + LOG_DBG("ACT", "Popped from activity stack, new size = %zu", stackActivities.size()); 77 + // Handle result if necessary 78 + if (currentActivity->resultHandler) { 79 + LOG_DBG("ACT", "Handling result for popped activity"); 80 + 81 + // Move it here to avoid the case where handler calling another startActivityForResult() 82 + auto handler = std::move(currentActivity->resultHandler); 83 + currentActivity->resultHandler = nullptr; 84 + lock.unlock(); // Handler may acquire its own lock 85 + handler(pendingResult); 86 + } 87 + 88 + // Request an update to ensure the popped activity gets re-rendered 89 + if (pendingAction == PendingAction::None) { 90 + requestUpdate(); 91 + } 92 + 93 + // Handler may request another pending action, we will handle it in the next loop iteration 94 + continue; 95 + } 96 + 97 + } else if (pendingActivity) { 98 + // Current activity has requested a new activity to be launched 99 + RenderLock lock; 100 + 101 + if (pendingAction == PendingAction::Replace) { 102 + // Destroy the current activity 103 + exitActivity(lock); 104 + // Clear the stack 105 + while (!stackActivities.empty()) { 106 + stackActivities.back()->onExit(); 107 + stackActivities.pop_back(); 108 + } 109 + } else if (pendingAction == PendingAction::Push) { 110 + // Move current activity to stack 111 + stackActivities.push_back(std::move(currentActivity)); 112 + LOG_DBG("ACT", "Pushed to activity stack, new size = %zu", stackActivities.size()); 113 + } 114 + pendingAction = PendingAction::None; 115 + currentActivity = std::move(pendingActivity); 116 + 117 + lock.unlock(); // onEnter may acquire its own lock 118 + currentActivity->onEnter(); 119 + 120 + // onEnter may request another pending action, we will handle it in the next loop iteration 121 + continue; 122 + } 123 + } 124 + 125 + if (requestedUpdate) { 126 + requestedUpdate = false; 127 + // Using direct notification to signal the render task to update 128 + // Increment counter so multiple rapid calls won't be lost 129 + if (renderTaskHandle) { 130 + xTaskNotify(renderTaskHandle, 1, eIncrement); 131 + } 132 + } 133 + } 134 + 135 + void ActivityManager::exitActivity(const RenderLock& lock) { 136 + // Note: lock must be held by the caller 137 + if (currentActivity) { 138 + currentActivity->onExit(); 139 + currentActivity.reset(); 140 + } 141 + } 142 + 143 + void ActivityManager::replaceActivity(std::unique_ptr<Activity>&& newActivity) { 144 + // Note: no lock here, this is usually called by loop() and we may run into deadlock 145 + if (currentActivity) { 146 + // Defer launch if we're currently in an activity, to avoid deleting the current activity 147 + // leading to the "delete this" problem 148 + pendingActivity = std::move(newActivity); 149 + pendingAction = PendingAction::Replace; 150 + } else { 151 + // No current activity, safe to launch immediately 152 + currentActivity = std::move(newActivity); 153 + currentActivity->onEnter(); 154 + } 155 + } 156 + 157 + void ActivityManager::goToFileTransfer() { 158 + replaceActivity(std::make_unique<CrossPointWebServerActivity>(renderer, mappedInput)); 159 + } 160 + 161 + void ActivityManager::goToSettings() { replaceActivity(std::make_unique<SettingsActivity>(renderer, mappedInput)); } 162 + 163 + void ActivityManager::goToMyLibrary(std::string path) { 164 + replaceActivity(std::make_unique<MyLibraryActivity>(renderer, mappedInput, std::move(path))); 165 + } 166 + 167 + void ActivityManager::goToRecentBooks() { 168 + replaceActivity(std::make_unique<RecentBooksActivity>(renderer, mappedInput)); 169 + } 170 + 171 + void ActivityManager::goToBrowser() { 172 + replaceActivity(std::make_unique<OpdsBookBrowserActivity>(renderer, mappedInput)); 173 + } 174 + 175 + void ActivityManager::goToReader(std::string path) { 176 + replaceActivity(std::make_unique<ReaderActivity>(renderer, mappedInput, std::move(path))); 177 + } 178 + 179 + void ActivityManager::goToSleep() { 180 + replaceActivity(std::make_unique<SleepActivity>(renderer, mappedInput)); 181 + loop(); // Important: sleep screen must be rendered immediately, the caller will go to sleep right after this returns 182 + } 183 + 184 + void ActivityManager::goToBoot() { replaceActivity(std::make_unique<BootActivity>(renderer, mappedInput)); } 185 + 186 + void ActivityManager::goToFullScreenMessage(std::string message, EpdFontFamily::Style style) { 187 + replaceActivity(std::make_unique<FullScreenMessageActivity>(renderer, mappedInput, std::move(message), style)); 188 + } 189 + 190 + void ActivityManager::goHome() { replaceActivity(std::make_unique<HomeActivity>(renderer, mappedInput)); } 191 + 192 + void ActivityManager::pushActivity(std::unique_ptr<Activity>&& activity) { 193 + if (pendingActivity) { 194 + // Should never happen in practice 195 + LOG_ERR("ACT", "pendingActivity while pushActivity is not expected"); 196 + pendingActivity.reset(); 197 + } 198 + pendingActivity = std::move(activity); 199 + pendingAction = PendingAction::Push; 200 + } 201 + 202 + void ActivityManager::popActivity() { 203 + if (pendingActivity) { 204 + // Should never happen in practice 205 + LOG_ERR("ACT", "pendingActivity while popActivity is not expected"); 206 + pendingActivity.reset(); 207 + } 208 + pendingAction = PendingAction::Pop; 209 + } 210 + 211 + bool ActivityManager::preventAutoSleep() const { return currentActivity && currentActivity->preventAutoSleep(); } 212 + 213 + bool ActivityManager::isReaderActivity() const { return currentActivity && currentActivity->isReaderActivity(); } 214 + 215 + bool ActivityManager::skipLoopDelay() const { return currentActivity && currentActivity->skipLoopDelay(); } 216 + 217 + void ActivityManager::requestUpdate(bool immediate) { 218 + if (immediate) { 219 + if (renderTaskHandle) { 220 + xTaskNotify(renderTaskHandle, 1, eIncrement); 221 + } 222 + } else { 223 + // Deferring the update until current loop is finished 224 + // This is to avoid multiple updates being requested in the same loop 225 + requestedUpdate = true; 226 + } 227 + } 228 + // RenderLock 229 + 230 + RenderLock::RenderLock() { 231 + xSemaphoreTake(activityManager.renderingMutex, portMAX_DELAY); 232 + isLocked = true; 233 + } 234 + 235 + RenderLock::RenderLock(Activity& /* unused */) { 236 + xSemaphoreTake(activityManager.renderingMutex, portMAX_DELAY); 237 + isLocked = true; 238 + } 239 + 240 + RenderLock::~RenderLock() { 241 + if (isLocked) { 242 + xSemaphoreGive(activityManager.renderingMutex); 243 + isLocked = false; 244 + } 245 + } 246 + 247 + void RenderLock::unlock() { 248 + if (isLocked) { 249 + xSemaphoreGive(activityManager.renderingMutex); 250 + isLocked = false; 251 + } 252 + }
+102
src/activities/ActivityManager.h
··· 1 + #pragma once 2 + 3 + #include <freertos/FreeRTOS.h> 4 + #include <freertos/semphr.h> 5 + #include <freertos/task.h> 6 + 7 + #include <cassert> 8 + #include <memory> 9 + #include <string> 10 + #include <vector> 11 + 12 + #include "GfxRenderer.h" 13 + #include "MappedInputManager.h" 14 + #include "RenderLock.h" 15 + 16 + class Activity; // forward declaration 17 + class RenderLock; // forward declaration 18 + 19 + /** 20 + * ActivityManager 21 + * 22 + * This mirrors the same concept of Activity in Android, where an activity represents a single screen of the UI. The 23 + * manager is responsible for launching activities, and ensuring that only one activity is active at a time. 24 + * 25 + * It also provides a stack mechanism to allow activities to launch sub-activities and get back the results when the 26 + * sub-activity is done. For example, the WebServer activity can launch a WifiSelect activity to let the user choose a 27 + * wifi network, and get back the selected network when the user is done. 28 + * 29 + * Main differences from Android's ActivityManager: 30 + * - No onPause/onResume, since we don't have a concept of background activities 31 + * - onActivityResult is implemented via a callback instead of a separate method, for simplicity 32 + */ 33 + class ActivityManager { 34 + protected: 35 + GfxRenderer& renderer; 36 + MappedInputManager& mappedInput; 37 + std::vector<std::unique_ptr<Activity>> stackActivities; 38 + std::unique_ptr<Activity> currentActivity; 39 + 40 + void exitActivity(const RenderLock& lock); 41 + 42 + // Pending activity to be launched on next loop iteration 43 + std::unique_ptr<Activity> pendingActivity; 44 + enum class PendingAction { None, Push, Pop, Replace }; 45 + PendingAction pendingAction = PendingAction::None; 46 + 47 + // Task to render and display the activity 48 + TaskHandle_t renderTaskHandle = nullptr; 49 + static void renderTaskTrampoline(void* param); 50 + [[noreturn]] virtual void renderTaskLoop(); 51 + 52 + // Whether to trigger a render after the current loop() 53 + // This variable must only be set by the main loop, to avoid race conditions 54 + bool requestedUpdate = false; 55 + 56 + public: 57 + explicit ActivityManager(GfxRenderer& renderer, MappedInputManager& mappedInput) 58 + : renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) { 59 + assert(renderingMutex != nullptr && "Failed to create rendering mutex"); 60 + stackActivities.reserve(10); 61 + } 62 + ~ActivityManager() { assert(false); /* should never be called */ }; 63 + 64 + // Mutex to protect rendering operations from race conditions 65 + // Must only be used via RenderLock 66 + SemaphoreHandle_t renderingMutex = nullptr; 67 + 68 + void begin(); 69 + void loop(); 70 + 71 + // Will replace currentActivity and drop all activities on stack 72 + void replaceActivity(std::unique_ptr<Activity>&& newActivity); 73 + 74 + // goTo... functions are convenient wrapper for replaceActivity() 75 + void goToFileTransfer(); 76 + void goToSettings(); 77 + void goToMyLibrary(std::string path = {}); 78 + void goToRecentBooks(); 79 + void goToBrowser(); 80 + void goToReader(std::string path); 81 + void goToSleep(); 82 + void goToBoot(); 83 + void goToFullScreenMessage(std::string message, EpdFontFamily::Style style = EpdFontFamily::REGULAR); 84 + void goHome(); 85 + 86 + // This will move current activity to stack instead of deleting it 87 + void pushActivity(std::unique_ptr<Activity>&& activity); 88 + 89 + // Remove the currentActivity, returning the last one on stack 90 + // Note: if popActivity() on last activity on the stack, we will goHome() 91 + void popActivity(); 92 + 93 + bool preventAutoSleep() const; 94 + bool isReaderActivity() const; 95 + bool skipLoopDelay() const; 96 + 97 + // If immediate is true, the update will be triggered immediately. 98 + // Otherwise, it will be deferred until the end of the current loop iteration. 99 + void requestUpdate(bool immediate = false); 100 + }; 101 + 102 + extern ActivityManager activityManager; // singleton, to be defined in main.cpp
+66
src/activities/ActivityResult.h
··· 1 + #pragma once 2 + 3 + #include <cstdint> 4 + #include <functional> 5 + #include <string> 6 + #include <type_traits> 7 + #include <utility> 8 + #include <variant> 9 + 10 + struct WifiResult { 11 + bool connected = false; 12 + std::string ssid; 13 + std::string ip; 14 + }; 15 + 16 + struct KeyboardResult { 17 + std::string text; 18 + }; 19 + 20 + struct MenuResult { 21 + int action = -1; 22 + uint8_t orientation = 0; 23 + }; 24 + 25 + struct ChapterResult { 26 + int spineIndex = 0; 27 + }; 28 + 29 + struct PercentResult { 30 + int percent = 0; 31 + }; 32 + 33 + struct PageResult { 34 + uint32_t page = 0; 35 + }; 36 + 37 + struct SyncResult { 38 + int spineIndex = 0; 39 + int page = 0; 40 + }; 41 + 42 + enum class NetworkMode; 43 + 44 + struct NetworkModeResult { 45 + NetworkMode mode; 46 + }; 47 + 48 + struct FootnoteResult { 49 + std::string href; 50 + }; 51 + 52 + using ResultVariant = std::variant<std::monostate, WifiResult, KeyboardResult, MenuResult, ChapterResult, PercentResult, 53 + PageResult, SyncResult, NetworkModeResult, FootnoteResult>; 54 + 55 + struct ActivityResult { 56 + bool isCancelled = false; 57 + ResultVariant data; 58 + 59 + explicit ActivityResult() = default; 60 + 61 + template <typename ResultType, typename = std::enable_if_t<std::is_constructible_v<ResultVariant, ResultType&&>>> 62 + // cppcheck-suppress noExplicitConstructor 63 + ActivityResult(ResultType&& result) : data{std::forward<ResultType>(result)} {} 64 + }; 65 + 66 + using ActivityResultHandler = std::function<void(const ActivityResult&)>;
-53
src/activities/ActivityWithSubactivity.cpp
··· 1 - #include "ActivityWithSubactivity.h" 2 - 3 - #include <HalPowerManager.h> 4 - 5 - void ActivityWithSubactivity::renderTaskLoop() { 6 - while (true) { 7 - ulTaskNotifyTake(pdTRUE, portMAX_DELAY); 8 - { 9 - HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering 10 - RenderLock lock(*this); 11 - if (!subActivity) { 12 - render(std::move(lock)); 13 - } 14 - // If subActivity is set, consume the notification but skip parent render 15 - // Note: the sub-activity will call its render() from its own display task 16 - } 17 - } 18 - } 19 - 20 - void ActivityWithSubactivity::exitActivity() { 21 - // No need to lock, since onExit() already acquires its own lock 22 - if (subActivity) { 23 - LOG_DBG("ACT", "Exiting subactivity..."); 24 - subActivity->onExit(); 25 - subActivity.reset(); 26 - } 27 - } 28 - 29 - void ActivityWithSubactivity::enterNewActivity(Activity* activity) { 30 - // Acquire lock to avoid 2 activities rendering at the same time during transition 31 - RenderLock lock(*this); 32 - subActivity.reset(activity); 33 - subActivity->onEnter(); 34 - } 35 - 36 - void ActivityWithSubactivity::loop() { 37 - if (subActivity) { 38 - subActivity->loop(); 39 - } 40 - } 41 - 42 - void ActivityWithSubactivity::requestUpdate() { 43 - if (!subActivity) { 44 - Activity::requestUpdate(); 45 - } 46 - // Sub-activity should call their own requestUpdate() from their loop() function 47 - } 48 - 49 - void ActivityWithSubactivity::onExit() { 50 - // No need to lock, onExit() already acquires its own lock 51 - exitActivity(); 52 - Activity::onExit(); 53 - }
-21
src/activities/ActivityWithSubactivity.h
··· 1 - #pragma once 2 - #include <memory> 3 - 4 - #include "Activity.h" 5 - 6 - class ActivityWithSubactivity : public Activity { 7 - protected: 8 - std::unique_ptr<Activity> subActivity = nullptr; 9 - void exitActivity(); 10 - void enterNewActivity(Activity* activity); 11 - [[noreturn]] void renderTaskLoop() override; 12 - 13 - public: 14 - explicit ActivityWithSubactivity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput) 15 - : Activity(std::move(name), renderer, mappedInput) {} 16 - void loop() override; 17 - // Note: when a subactivity is active, parent requestUpdate() calls are ignored; 18 - // the subactivity should request its own renders. This pauses parent rendering until exit. 19 - void requestUpdate() override; 20 - void onExit() override; 21 - };
+16
src/activities/RenderLock.h
··· 1 + #pragma once 2 + 3 + class Activity; // forward declaration 4 + 5 + // RAII helper to lock rendering mutex for the duration of a scope. 6 + class RenderLock { 7 + bool isLocked = false; 8 + 9 + public: 10 + explicit RenderLock(); 11 + explicit RenderLock(Activity&); // unused for now, but keep for compatibility 12 + RenderLock(const RenderLock&) = delete; 13 + RenderLock& operator=(const RenderLock&) = delete; 14 + ~RenderLock(); 15 + void unlock(); 16 + };
+10 -13
src/activities/browser/OpdsBookBrowserActivity.cpp
··· 21 21 } // namespace 22 22 23 23 void OpdsBookBrowserActivity::onEnter() { 24 - ActivityWithSubactivity::onEnter(); 24 + Activity::onEnter(); 25 25 26 26 state = BrowserState::CHECK_WIFI; 27 27 entries.clear(); ··· 37 37 } 38 38 39 39 void OpdsBookBrowserActivity::onExit() { 40 - ActivityWithSubactivity::onExit(); 40 + Activity::onExit(); 41 41 42 42 // Turn off WiFi when exiting 43 43 WiFi.mode(WIFI_OFF); ··· 49 49 void OpdsBookBrowserActivity::loop() { 50 50 // Handle WiFi selection subactivity 51 51 if (state == BrowserState::WIFI_SELECTION) { 52 - ActivityWithSubactivity::loop(); 52 + // Should already handled by the WifiSelectionActivity 53 53 return; 54 54 } 55 55 ··· 136 136 } 137 137 } 138 138 139 - void OpdsBookBrowserActivity::render(Activity::RenderLock&&) { 139 + void OpdsBookBrowserActivity::render(RenderLock&&) { 140 140 renderer.clearScreen(); 141 141 142 142 const auto pageWidth = renderer.getScreenWidth(); ··· 279 279 statusMessage = tr(STR_LOADING); 280 280 entries.clear(); 281 281 selectorIndex = 0; 282 - requestUpdate(); 282 + requestUpdate(true); // Force update to show loading state immediately before fetch 283 283 284 284 fetchFeed(currentPath); 285 285 } ··· 308 308 statusMessage = book.title; 309 309 downloadProgress = 0; 310 310 downloadTotal = 0; 311 - requestUpdate(); 311 + requestUpdate(true); 312 312 313 313 // Build full download URL 314 314 std::string downloadUrl = UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href); ··· 326 326 HttpDownloader::downloadToFile(downloadUrl, filename, [this](const size_t downloaded, const size_t total) { 327 327 downloadProgress = downloaded; 328 328 downloadTotal = total; 329 - requestUpdate(); 329 + requestUpdate(true); // Force update to refresh progress bar 330 330 }); 331 331 332 332 if (result == HttpDownloader::OK) { ··· 364 364 state = BrowserState::WIFI_SELECTION; 365 365 requestUpdate(); 366 366 367 - enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, 368 - [this](const bool connected) { onWifiSelectionComplete(connected); })); 367 + startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput), 368 + [this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); }); 369 369 } 370 370 371 371 void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) { 372 - exitActivity(); 373 - 374 372 if (connected) { 375 373 LOG_DBG("OPDS", "WiFi connected via selection, fetching feed"); 376 374 state = BrowserState::LOADING; 377 375 statusMessage = tr(STR_LOADING); 378 - requestUpdate(); 376 + requestUpdate(true); // Force update to show loading state immediately before fetch 379 377 fetchFeed(currentPath); 380 378 } else { 381 379 LOG_DBG("OPDS", "WiFi selection cancelled/failed"); ··· 385 383 WiFi.mode(WIFI_OFF); 386 384 state = BrowserState::ERROR; 387 385 errorMessage = tr(STR_WIFI_CONN_FAILED); 388 - requestUpdate(); 389 386 } 390 387 }
+5 -8
src/activities/browser/OpdsBookBrowserActivity.h
··· 5 5 #include <string> 6 6 #include <vector> 7 7 8 - #include "../ActivityWithSubactivity.h" 8 + #include "../Activity.h" 9 9 #include "util/ButtonNavigator.h" 10 10 11 11 /** ··· 13 13 * Supports navigation through catalog hierarchy and downloading EPUBs. 14 14 * When WiFi connection fails, launches WiFi selection to let user connect. 15 15 */ 16 - class OpdsBookBrowserActivity final : public ActivityWithSubactivity { 16 + class OpdsBookBrowserActivity final : public Activity { 17 17 public: 18 18 enum class BrowserState { 19 19 CHECK_WIFI, // Checking WiFi connection ··· 24 24 ERROR // Error state with message 25 25 }; 26 26 27 - explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 28 - const std::function<void()>& onGoHome) 29 - : ActivityWithSubactivity("OpdsBookBrowser", renderer, mappedInput), onGoHome(onGoHome) {} 27 + explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) 28 + : Activity("OpdsBookBrowser", renderer, mappedInput) {} 30 29 31 30 void onEnter() override; 32 31 void onExit() override; 33 32 void loop() override; 34 - void render(Activity::RenderLock&&) override; 33 + void render(RenderLock&&) override; 35 34 36 35 private: 37 36 ButtonNavigator buttonNavigator; ··· 44 43 std::string statusMessage; 45 44 size_t downloadProgress = 0; 46 45 size_t downloadTotal = 0; 47 - 48 - const std::function<void()> onGoHome; 49 46 50 47 void checkAndConnectWifi(); 51 48 void launchWifiSelection();
+13 -1
src/activities/home/HomeActivity.cpp
··· 211 211 } 212 212 } 213 213 214 - void HomeActivity::render(Activity::RenderLock&&) { 214 + void HomeActivity::render(RenderLock&&) { 215 215 const auto& metrics = UITheme::getInstance().getMetrics(); 216 216 const auto pageWidth = renderer.getScreenWidth(); 217 217 const auto pageHeight = renderer.getScreenHeight(); ··· 258 258 loadRecentCovers(metrics.homeCoverHeight); 259 259 } 260 260 } 261 + 262 + void HomeActivity::onSelectBook(const std::string& path) { activityManager.goToReader(path); } 263 + 264 + void HomeActivity::onMyLibraryOpen() { activityManager.goToMyLibrary(); } 265 + 266 + void HomeActivity::onRecentsOpen() { activityManager.goToRecentBooks(); } 267 + 268 + void HomeActivity::onSettingsOpen() { activityManager.goToSettings(); } 269 + 270 + void HomeActivity::onFileTransferOpen() { activityManager.goToFileTransfer(); } 271 + 272 + void HomeActivity::onOpdsBrowserOpen() { activityManager.goToBrowser(); }
+9 -19
src/activities/home/HomeActivity.h
··· 20 20 bool coverBufferStored = false; // Track if cover buffer is stored 21 21 uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image 22 22 std::vector<RecentBook> recentBooks; 23 - const std::function<void(const std::string& path)> onSelectBook; 24 - const std::function<void()> onMyLibraryOpen; 25 - const std::function<void()> onRecentsOpen; 26 - const std::function<void()> onSettingsOpen; 27 - const std::function<void()> onFileTransferOpen; 28 - const std::function<void()> onOpdsBrowserOpen; 23 + void onSelectBook(const std::string& path); 24 + void onMyLibraryOpen(); 25 + void onRecentsOpen(); 26 + void onSettingsOpen(); 27 + void onFileTransferOpen(); 28 + void onOpdsBrowserOpen(); 29 29 30 30 int getMenuItemCount() const; 31 31 bool storeCoverBuffer(); // Store frame buffer for cover image ··· 35 35 void loadRecentCovers(int coverHeight); 36 36 37 37 public: 38 - explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 39 - const std::function<void(const std::string& path)>& onSelectBook, 40 - const std::function<void()>& onMyLibraryOpen, const std::function<void()>& onRecentsOpen, 41 - const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen, 42 - const std::function<void()>& onOpdsBrowserOpen) 43 - : Activity("Home", renderer, mappedInput), 44 - onSelectBook(onSelectBook), 45 - onMyLibraryOpen(onMyLibraryOpen), 46 - onRecentsOpen(onRecentsOpen), 47 - onSettingsOpen(onSettingsOpen), 48 - onFileTransferOpen(onFileTransferOpen), 49 - onOpdsBrowserOpen(onOpdsBrowserOpen) {} 38 + explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) 39 + : Activity("Home", renderer, mappedInput) {} 50 40 void onEnter() override; 51 41 void onExit() override; 52 42 void loop() override; 53 - void render(Activity::RenderLock&&) override; 43 + void render(RenderLock&&) override; 54 44 };
+1 -1
src/activities/home/MyLibraryActivity.cpp
··· 196 196 return filename.substr(0, pos); 197 197 } 198 198 199 - void MyLibraryActivity::render(Activity::RenderLock&&) { 199 + void MyLibraryActivity::render(RenderLock&&) { 200 200 renderer.clearScreen(); 201 201 202 202 const auto pageWidth = renderer.getScreenWidth();
+3 -13
src/activities/home/MyLibraryActivity.h
··· 17 17 std::string basepath = "/"; 18 18 std::vector<std::string> files; 19 19 20 - // Callbacks 21 - const std::function<void(const std::string& path)> onSelectBook; 22 - const std::function<void()> onGoHome; 23 - 24 20 // Data loading 25 21 void loadFiles(); 26 22 size_t findEntry(const std::string& name) const; 27 23 28 24 public: 29 - explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 30 - const std::function<void()>& onGoHome, 31 - const std::function<void(const std::string& path)>& onSelectBook, 32 - std::string initialPath = "/") 33 - : Activity("MyLibrary", renderer, mappedInput), 34 - basepath(initialPath.empty() ? "/" : std::move(initialPath)), 35 - onSelectBook(onSelectBook), 36 - onGoHome(onGoHome) {} 25 + explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialPath = "/") 26 + : Activity("MyLibrary", renderer, mappedInput), basepath(initialPath.empty() ? "/" : std::move(initialPath)) {} 37 27 void onEnter() override; 38 28 void onExit() override; 39 29 void loop() override; 40 - void render(Activity::RenderLock&&) override; 30 + void render(RenderLock&&) override; 41 31 };
+1 -1
src/activities/home/RecentBooksActivity.cpp
··· 83 83 }); 84 84 } 85 85 86 - void RecentBooksActivity::render(Activity::RenderLock&&) { 86 + void RecentBooksActivity::render(RenderLock&&) { 87 87 renderer.clearScreen(); 88 88 89 89 const auto pageWidth = renderer.getScreenWidth();
+3 -9
src/activities/home/RecentBooksActivity.h
··· 18 18 // Recent tab state 19 19 std::vector<RecentBook> recentBooks; 20 20 21 - // Callbacks 22 - const std::function<void(const std::string& path)> onSelectBook; 23 - const std::function<void()> onGoHome; 24 - 25 21 // Data loading 26 22 void loadRecentBooks(); 27 23 28 24 public: 29 - explicit RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 30 - const std::function<void()>& onGoHome, 31 - const std::function<void(const std::string& path)>& onSelectBook) 32 - : Activity("RecentBooks", renderer, mappedInput), onSelectBook(onSelectBook), onGoHome(onGoHome) {} 25 + explicit RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) 26 + : Activity("RecentBooks", renderer, mappedInput) {} 33 27 void onEnter() override; 34 28 void onExit() override; 35 29 void loop() override; 36 - void render(Activity::RenderLock&&) override; 30 + void render(RenderLock&&) override; 37 31 };
+14 -20
src/activities/network/CalibreConnectActivity.cpp
··· 16 16 } // namespace 17 17 18 18 void CalibreConnectActivity::onEnter() { 19 - ActivityWithSubactivity::onEnter(); 19 + Activity::onEnter(); 20 20 21 21 requestUpdate(); 22 22 state = CalibreConnectState::WIFI_SELECTION; ··· 32 32 exitRequested = false; 33 33 34 34 if (WiFi.status() != WL_CONNECTED) { 35 - enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, 36 - [this](const bool connected) { onWifiSelectionComplete(connected); })); 35 + startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput), 36 + [this](const ActivityResult& result) { 37 + if (!result.isCancelled) { 38 + const auto& wifi = std::get<WifiResult>(result.data); 39 + connectedIP = wifi.ip; 40 + connectedSSID = wifi.ssid; 41 + } 42 + onWifiSelectionComplete(!result.isCancelled); 43 + }); 37 44 } else { 38 45 connectedIP = WiFi.localIP().toString().c_str(); 39 46 connectedSSID = WiFi.SSID().c_str(); ··· 42 49 } 43 50 44 51 void CalibreConnectActivity::onExit() { 45 - ActivityWithSubactivity::onExit(); 52 + Activity::onExit(); 46 53 47 54 stopWebServer(); 48 55 MDNS.end(); ··· 56 63 57 64 void CalibreConnectActivity::onWifiSelectionComplete(const bool connected) { 58 65 if (!connected) { 59 - exitActivity(); 60 - onComplete(); 66 + activityManager.popActivity(); 61 67 return; 62 68 } 63 69 64 - if (subActivity) { 65 - connectedIP = static_cast<WifiSelectionActivity*>(subActivity.get())->getConnectedIP(); 66 - } else { 67 - connectedIP = WiFi.localIP().toString().c_str(); 68 - } 69 - connectedSSID = WiFi.SSID().c_str(); 70 - exitActivity(); 71 70 startWebServer(); 72 71 } 73 72 ··· 100 99 } 101 100 102 101 void CalibreConnectActivity::loop() { 103 - if (subActivity) { 104 - subActivity->loop(); 105 - return; 106 - } 107 - 108 102 if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 109 103 exitRequested = true; 110 104 } ··· 168 162 } 169 163 170 164 if (exitRequested) { 171 - onComplete(); 165 + activityManager.popActivity(); 172 166 return; 173 167 } 174 168 } 175 169 176 - void CalibreConnectActivity::render(Activity::RenderLock&&) { 170 + void CalibreConnectActivity::render(RenderLock&&) { 177 171 const auto& metrics = UITheme::getInstance().getMetrics(); 178 172 const auto pageWidth = renderer.getScreenWidth(); 179 173 const auto pageHeight = renderer.getScreenHeight();
+5 -7
src/activities/network/CalibreConnectActivity.h
··· 4 4 #include <memory> 5 5 #include <string> 6 6 7 - #include "activities/ActivityWithSubactivity.h" 7 + #include "activities/Activity.h" 8 8 #include "network/CrossPointWebServer.h" 9 9 10 10 enum class CalibreConnectState { WIFI_SELECTION, SERVER_STARTING, SERVER_RUNNING, ERROR }; ··· 13 13 * CalibreConnectActivity starts the file transfer server in STA mode, 14 14 * but renders Calibre-specific instructions instead of the web transfer UI. 15 15 */ 16 - class CalibreConnectActivity final : public ActivityWithSubactivity { 16 + class CalibreConnectActivity final : public Activity { 17 17 CalibreConnectState state = CalibreConnectState::WIFI_SELECTION; 18 - const std::function<void()> onComplete; 19 18 20 19 std::unique_ptr<CrossPointWebServer> webServer; 21 20 std::string connectedIP; ··· 36 35 void stopWebServer(); 37 36 38 37 public: 39 - explicit CalibreConnectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 40 - const std::function<void()>& onComplete) 41 - : ActivityWithSubactivity("CalibreConnect", renderer, mappedInput), onComplete(onComplete) {} 38 + explicit CalibreConnectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) 39 + : Activity("CalibreConnect", renderer, mappedInput) {} 42 40 void onEnter() override; 43 41 void onExit() override; 44 42 void loop() override; 45 - void render(Activity::RenderLock&&) override; 43 + void render(RenderLock&&) override; 46 44 bool skipLoopDelay() override { return webServer && webServer->isRunning(); } 47 45 bool preventAutoSleep() override { return webServer && webServer->isRunning(); } 48 46 };
+47 -43
src/activities/network/CrossPointWebServerActivity.cpp
··· 33 33 } // namespace 34 34 35 35 void CrossPointWebServerActivity::onEnter() { 36 - ActivityWithSubactivity::onEnter(); 36 + Activity::onEnter(); 37 37 38 38 LOG_DBG("WEBACT", "Free heap at onEnter: %d bytes", ESP.getFreeHeap()); 39 39 ··· 48 48 49 49 // Launch network mode selection subactivity 50 50 LOG_DBG("WEBACT", "Launching NetworkModeSelectionActivity..."); 51 - enterNewActivity(new NetworkModeSelectionActivity( 52 - renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); }, 53 - [this]() { onGoBack(); } // Cancel goes back to home 54 - )); 51 + startActivityForResult(std::make_unique<NetworkModeSelectionActivity>(renderer, mappedInput), 52 + [this](const ActivityResult& result) { 53 + if (result.isCancelled) { 54 + onGoHome(); 55 + } else { 56 + onNetworkModeSelected(std::get<NetworkModeResult>(result.data).mode); 57 + } 58 + }); 55 59 } 56 60 57 61 void CrossPointWebServerActivity::onExit() { 58 - ActivityWithSubactivity::onExit(); 62 + Activity::onExit(); 59 63 60 64 LOG_DBG("WEBACT", "Free heap at onExit start: %d bytes", ESP.getFreeHeap()); 61 65 ··· 107 111 networkMode = mode; 108 112 isApMode = (mode == NetworkMode::CREATE_HOTSPOT); 109 113 110 - // Exit mode selection subactivity 111 - exitActivity(); 114 + if (mode == NetworkMode::CONNECT_CALIBRE) { 115 + startActivityForResult( 116 + std::make_unique<CalibreConnectActivity>(renderer, mappedInput), [this](const ActivityResult& result) { 117 + state = WebServerActivityState::MODE_SELECTION; 112 118 113 - if (mode == NetworkMode::CONNECT_CALIBRE) { 114 - exitActivity(); 115 - enterNewActivity(new CalibreConnectActivity(renderer, mappedInput, [this] { 116 - exitActivity(); 117 - state = WebServerActivityState::MODE_SELECTION; 118 - enterNewActivity(new NetworkModeSelectionActivity( 119 - renderer, mappedInput, [this](const NetworkMode nextMode) { onNetworkModeSelected(nextMode); }, 120 - [this]() { onGoBack(); })); 121 - })); 119 + startActivityForResult(std::make_unique<NetworkModeSelectionActivity>(renderer, mappedInput), 120 + [this](const ActivityResult& result) { 121 + if (result.isCancelled) { 122 + onGoHome(); 123 + } else { 124 + onNetworkModeSelected(std::get<NetworkModeResult>(result.data).mode); 125 + } 126 + }); 127 + }); 122 128 return; 123 129 } 124 130 ··· 129 135 130 136 state = WebServerActivityState::WIFI_SELECTION; 131 137 LOG_DBG("WEBACT", "Launching WifiSelectionActivity..."); 132 - enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, 133 - [this](const bool connected) { onWifiSelectionComplete(connected); })); 138 + startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput), 139 + [this](const ActivityResult& result) { 140 + if (!result.isCancelled) { 141 + const auto& wifi = std::get<WifiResult>(result.data); 142 + connectedIP = wifi.ip; 143 + connectedSSID = wifi.ssid; 144 + } 145 + onWifiSelectionComplete(!result.isCancelled); 146 + }); 134 147 } else { 135 148 // AP mode - start access point 136 149 state = WebServerActivityState::AP_STARTING; ··· 144 157 145 158 if (connected) { 146 159 // Get connection info before exiting subactivity 147 - connectedIP = static_cast<WifiSelectionActivity*>(subActivity.get())->getConnectedIP(); 148 - connectedSSID = WiFi.SSID().c_str(); 149 160 isApMode = false; 150 - 151 - exitActivity(); 152 161 153 162 // Start mDNS for hostname resolution 154 163 if (MDNS.begin(AP_HOSTNAME)) { ··· 159 168 startWebServer(); 160 169 } else { 161 170 // User cancelled - go back to mode selection 162 - exitActivity(); 163 171 state = WebServerActivityState::MODE_SELECTION; 164 - enterNewActivity(new NetworkModeSelectionActivity( 165 - renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); }, 166 - [this]() { onGoBack(); })); 172 + 173 + startActivityForResult(std::make_unique<NetworkModeSelectionActivity>(renderer, mappedInput), 174 + [this](const ActivityResult& result) { 175 + if (result.isCancelled) { 176 + onGoHome(); 177 + } else { 178 + onNetworkModeSelected(std::get<NetworkModeResult>(result.data).mode); 179 + } 180 + }); 167 181 } 168 182 } 169 183 ··· 186 200 187 201 if (!apStarted) { 188 202 LOG_ERR("WEBACT", "ERROR: Failed to start Access Point!"); 189 - onGoBack(); 203 + onGoHome(); 190 204 return; 191 205 } 192 206 ··· 236 250 237 251 // Force an immediate render since we're transitioning from a subactivity 238 252 // that had its own rendering task. We need to make sure our display is shown. 239 - { 240 - RenderLock lock(*this); 241 - render(std::move(lock)); 242 - } 243 - LOG_DBG("WEBACT", "Rendered File Transfer screen"); 253 + requestUpdate(); 244 254 } else { 245 255 LOG_ERR("WEBACT", "ERROR: Failed to start web server!"); 246 256 webServer.reset(); 247 257 // Go back on error 248 - onGoBack(); 258 + onGoHome(); 249 259 } 250 260 } 251 261 ··· 259 269 } 260 270 261 271 void CrossPointWebServerActivity::loop() { 262 - if (subActivity) { 263 - // Forward loop to subactivity 264 - subActivity->loop(); 265 - return; 266 - } 267 - 268 272 // Handle different states 269 273 if (state == WebServerActivityState::SERVER_RUNNING) { 270 274 // Handle DNS requests for captive portal (AP mode only) ··· 322 326 mappedInput.update(); 323 327 // Check for exit button inside loop for responsiveness 324 328 if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 325 - onGoBack(); 329 + onGoHome(); 326 330 return; 327 331 } 328 332 } ··· 332 336 333 337 // Handle exit on Back button (also check outside loop) 334 338 if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 335 - onGoBack(); 339 + onGoHome(); 336 340 return; 337 341 } 338 342 } 339 343 } 340 344 341 - void CrossPointWebServerActivity::render(Activity::RenderLock&&) { 345 + void CrossPointWebServerActivity::render(RenderLock&&) { 342 346 // Only render our own UI when server is running 343 347 // Subactivities handle their own rendering 344 348 if (state == WebServerActivityState::SERVER_RUNNING || state == WebServerActivityState::AP_STARTING) {
+5 -7
src/activities/network/CrossPointWebServerActivity.h
··· 5 5 #include <string> 6 6 7 7 #include "NetworkModeSelectionActivity.h" 8 - #include "activities/ActivityWithSubactivity.h" 8 + #include "activities/Activity.h" 9 9 #include "network/CrossPointWebServer.h" 10 10 11 11 // Web server activity states ··· 27 27 * - Handles client requests in its loop() function 28 28 * - Cleans up the server and shuts down WiFi on exit 29 29 */ 30 - class CrossPointWebServerActivity final : public ActivityWithSubactivity { 30 + class CrossPointWebServerActivity final : public Activity { 31 31 WebServerActivityState state = WebServerActivityState::MODE_SELECTION; 32 - const std::function<void()> onGoBack; 33 32 34 33 // Network mode 35 34 NetworkMode networkMode = NetworkMode::JOIN_NETWORK; ··· 54 53 void stopWebServer(); 55 54 56 55 public: 57 - explicit CrossPointWebServerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 58 - const std::function<void()>& onGoBack) 59 - : ActivityWithSubactivity("CrossPointWebServer", renderer, mappedInput), onGoBack(onGoBack) {} 56 + explicit CrossPointWebServerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) 57 + : Activity("CrossPointWebServer", renderer, mappedInput) {} 60 58 void onEnter() override; 61 59 void onExit() override; 62 60 void loop() override; 63 - void render(Activity::RenderLock&&) override; 61 + void render(RenderLock&&) override; 64 62 bool skipLoopDelay() override { return webServer && webServer->isRunning(); } 65 63 bool preventAutoSleep() override { return webServer && webServer->isRunning(); } 66 64 };
+13 -1
src/activities/network/NetworkModeSelectionActivity.cpp
··· 54 54 }); 55 55 } 56 56 57 - void NetworkModeSelectionActivity::render(Activity::RenderLock&&) { 57 + void NetworkModeSelectionActivity::render(RenderLock&&) { 58 58 renderer.clearScreen(); 59 59 60 60 const auto& metrics = UITheme::getInstance().getMetrics(); ··· 83 83 84 84 renderer.displayBuffer(); 85 85 } 86 + 87 + void NetworkModeSelectionActivity::onModeSelected(NetworkMode mode) { 88 + setResult(NetworkModeResult{mode}); 89 + finish(); 90 + } 91 + 92 + void NetworkModeSelectionActivity::onCancel() { 93 + ActivityResult result; 94 + result.isCancelled = true; 95 + setResult(std::move(result)); 96 + finish(); 97 + }
+6 -9
src/activities/network/NetworkModeSelectionActivity.h
··· 5 5 #include "../Activity.h" 6 6 #include "util/ButtonNavigator.h" 7 7 8 - // Enum for network mode selection 9 8 enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT }; 10 9 11 10 /** ··· 22 21 23 22 int selectedIndex = 0; 24 23 25 - const std::function<void(NetworkMode)> onModeSelected; 26 - const std::function<void()> onCancel; 27 - 28 24 public: 29 - explicit NetworkModeSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 30 - const std::function<void(NetworkMode)>& onModeSelected, 31 - const std::function<void()>& onCancel) 32 - : Activity("NetworkModeSelection", renderer, mappedInput), onModeSelected(onModeSelected), onCancel(onCancel) {} 25 + explicit NetworkModeSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) 26 + : Activity("NetworkModeSelection", renderer, mappedInput) {} 33 27 void onEnter() override; 34 28 void onExit() override; 35 29 void loop() override; 36 - void render(Activity::RenderLock&&) override; 30 + void render(RenderLock&&) override; 31 + 32 + void onModeSelected(NetworkMode mode); 33 + void onCancel(); 37 34 };
+24 -20
src/activities/network/WifiSelectionActivity.cpp
··· 190 190 // Show password entry 191 191 state = WifiSelectionState::PASSWORD_ENTRY; 192 192 // Don't allow screen updates while changing activity 193 - enterNewActivity(new KeyboardEntryActivity( 194 - renderer, mappedInput, tr(STR_ENTER_WIFI_PASSWORD), 195 - "", // No initial text 196 - 64, // Max password length 197 - false, // Show password by default (hard keyboard to use) 198 - [this](const std::string& text) { 199 - enteredPassword = text; 200 - exitActivity(); 201 - }, 202 - [this] { 203 - state = WifiSelectionState::NETWORK_LIST; 204 - exitActivity(); 205 - requestUpdate(); 206 - })); 193 + startActivityForResult( 194 + std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_ENTER_WIFI_PASSWORD), 195 + "", // No initial text 196 + 64, // Max password length 197 + false // Show password by default (hard keyboard to use) 198 + ), 199 + [this](const ActivityResult& result) { 200 + if (result.isCancelled) { 201 + state = WifiSelectionState::NETWORK_LIST; 202 + } else { 203 + enteredPassword = std::get<KeyboardResult>(result.data).text; 204 + } 205 + }); 207 206 } else { 208 207 // Connect directly for open networks 209 208 attemptConnection(); ··· 291 290 } 292 291 293 292 void WifiSelectionActivity::loop() { 294 - if (subActivity) { 295 - subActivity->loop(); 296 - return; 297 - } 298 - 299 293 // Check scan progress 300 294 if (state == WifiSelectionState::SCANNING) { 301 295 processWifiScanResults(); ··· 467 461 return " |"; // Very weak 468 462 } 469 463 470 - void WifiSelectionActivity::render(Activity::RenderLock&&) { 464 + void WifiSelectionActivity::render(RenderLock&&) { 471 465 // Don't render if we're in PASSWORD_ENTRY state - we're just transitioning 472 466 // from the keyboard subactivity back to the main activity 473 467 if (state == WifiSelectionState::PASSWORD_ENTRY) { ··· 693 687 const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_LEFT), tr(STR_DIR_RIGHT)); 694 688 GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); 695 689 } 690 + 691 + void WifiSelectionActivity::onComplete(const bool connected) { 692 + ActivityResult result; 693 + result.isCancelled = !connected; 694 + if (connected) { 695 + result.data = WifiResult{true, selectedSSID, connectedIP}; 696 + } 697 + setResult(std::move(result)); 698 + finish(); 699 + }
+8 -12
src/activities/network/WifiSelectionActivity.h
··· 6 6 #include <string> 7 7 #include <vector> 8 8 9 - #include "activities/ActivityWithSubactivity.h" 9 + #include "activities/Activity.h" 10 10 #include "util/ButtonNavigator.h" 11 11 12 12 // Structure to hold WiFi network information ··· 15 15 int32_t rssi; 16 16 bool isEncrypted; 17 17 bool hasSavedPassword; // Whether we have saved credentials for this network 18 + std::string ipAddress; // Populated after connection for display 18 19 }; 19 20 20 21 // WiFi selection states ··· 41 42 * 42 43 * The onComplete callback receives true if connected successfully, false if cancelled. 43 44 */ 44 - class WifiSelectionActivity final : public ActivityWithSubactivity { 45 + class WifiSelectionActivity final : public Activity { 45 46 ButtonNavigator buttonNavigator; 46 47 47 48 WifiSelectionState state = WifiSelectionState::SCANNING; 48 49 size_t selectedNetworkIndex = 0; 49 50 std::vector<WifiNetworkInfo> networks; 50 - const std::function<void(bool connected)> onComplete; 51 51 52 52 // Selected network for connection 53 53 std::string selectedSSID; ··· 95 95 void checkConnectionStatus(); 96 96 std::string getSignalStrengthIndicator(int32_t rssi) const; 97 97 98 + void onComplete(bool connected); 99 + 98 100 public: 99 - explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 100 - const std::function<void(bool connected)>& onComplete, bool autoConnect = true) 101 - : ActivityWithSubactivity("WifiSelection", renderer, mappedInput), 102 - onComplete(onComplete), 103 - allowAutoConnect(autoConnect) {} 101 + explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, bool autoConnect = true) 102 + : Activity("WifiSelection", renderer, mappedInput), allowAutoConnect(autoConnect) {} 104 103 void onEnter() override; 105 104 void onExit() override; 106 105 void loop() override; 107 - void render(Activity::RenderLock&&) override; 108 - 109 - // Get the IP address after successful connection 110 - const std::string& getConnectedIP() const { return connectedIP; } 106 + void render(RenderLock&&) override; 111 107 };
+63 -162
src/activities/reader/EpubReaderActivity.cpp
··· 61 61 } // namespace 62 62 63 63 void EpubReaderActivity::onEnter() { 64 - ActivityWithSubactivity::onEnter(); 64 + Activity::onEnter(); 65 65 66 66 if (!epub) { 67 67 return; ··· 108 108 } 109 109 110 110 void EpubReaderActivity::onExit() { 111 - ActivityWithSubactivity::onExit(); 111 + Activity::onExit(); 112 112 113 113 // Reset orientation back to portrait for the rest of the UI 114 114 renderer.setOrientation(GfxRenderer::Orientation::Portrait); ··· 120 120 } 121 121 122 122 void EpubReaderActivity::loop() { 123 - // Pass input responsibility to sub activity if exists 124 - if (subActivity) { 125 - subActivity->loop(); 126 - // Deferred exit: process after subActivity->loop() returns to avoid use-after-free 127 - if (pendingSubactivityExit) { 128 - pendingSubactivityExit = false; 129 - exitActivity(); 130 - requestUpdate(); 131 - skipNextButtonCheck = true; // Skip button processing to ignore stale events 132 - } 133 - // Deferred go home: process after subActivity->loop() returns to avoid race condition 134 - if (pendingGoHome) { 135 - pendingGoHome = false; 136 - exitActivity(); 137 - if (onGoHome) { 138 - onGoHome(); 139 - } 140 - return; // Don't access 'this' after callback 141 - } 142 - return; 143 - } 144 - 145 - // Handle pending go home when no subactivity (e.g., from long press back) 146 - if (pendingGoHome) { 147 - pendingGoHome = false; 148 - if (onGoHome) { 149 - onGoHome(); 150 - } 151 - return; // Don't access 'this' after callback 152 - } 153 - 154 - // Skip button processing after returning from subactivity 155 - // This prevents stale button release events from triggering actions 156 - // We wait until: (1) all relevant buttons are released, AND (2) wasReleased events have been cleared 157 - if (skipNextButtonCheck) { 158 - const bool confirmCleared = !mappedInput.isPressed(MappedInputManager::Button::Confirm) && 159 - !mappedInput.wasReleased(MappedInputManager::Button::Confirm); 160 - const bool backCleared = !mappedInput.isPressed(MappedInputManager::Button::Back) && 161 - !mappedInput.wasReleased(MappedInputManager::Button::Back); 162 - if (confirmCleared && backCleared) { 163 - skipNextButtonCheck = false; 164 - } 123 + if (!epub) { 124 + // Should never happen 125 + finish(); 165 126 return; 166 127 } 167 128 ··· 170 131 const int currentPage = section ? section->currentPage + 1 : 0; 171 132 const int totalPages = section ? section->pageCount : 0; 172 133 float bookProgress = 0.0f; 173 - if (epub && epub->getBookSize() > 0 && section && section->pageCount > 0) { 134 + if (epub->getBookSize() > 0 && section && section->pageCount > 0) { 174 135 const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount); 175 136 bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; 176 137 } 177 138 const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f)); 178 - exitActivity(); 179 - enterNewActivity(new EpubReaderMenuActivity( 180 - this->renderer, this->mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, 181 - SETTINGS.orientation, !currentPageFootnotes.empty(), 182 - [this](const uint8_t orientation) { onReaderMenuBack(orientation); }, 183 - [this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); })); 139 + startActivityForResult(std::make_unique<EpubReaderMenuActivity>( 140 + renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, 141 + SETTINGS.orientation, !currentPageFootnotes.empty()), 142 + [this](const ActivityResult& result) { 143 + // Always apply orientation change even if the menu was cancelled 144 + const auto& menu = std::get<MenuResult>(result.data); 145 + applyOrientation(menu.orientation); 146 + if (!result.isCancelled) { 147 + onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action)); 148 + } 149 + }); 184 150 } 185 151 186 152 // Long press BACK (1s+) goes to file selection 187 153 if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) { 188 - onGoBack(); 154 + activityManager.goToMyLibrary(epub ? epub->getPath() : ""); 189 155 return; 190 156 } 191 157 ··· 274 240 } 275 241 } 276 242 277 - void EpubReaderActivity::onReaderMenuBack(const uint8_t orientation) { 278 - exitActivity(); 279 - // Apply the user-selected orientation when the menu is dismissed. 280 - // This ensures the menu can be navigated without immediately rotating the screen. 281 - applyOrientation(orientation); 282 - requestUpdate(); 283 - } 284 - 285 243 // Translate an absolute percent into a spine index plus a normalized position 286 244 // within that spine so we can jump after the section is loaded. 287 245 void EpubReaderActivity::jumpToPercent(int percent) { ··· 348 306 void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) { 349 307 switch (action) { 350 308 case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: { 351 - // Calculate values BEFORE we start destroying things 352 - const int currentP = section ? section->currentPage : 0; 353 - const int totalP = section ? section->pageCount : 0; 354 309 const int spineIdx = currentSpineIndex; 355 310 const std::string path = epub->getPath(); 356 - 357 - // 1. Close the menu 358 - exitActivity(); 359 - 360 - // 2. Open the Chapter Selector 361 - enterNewActivity(new EpubReaderChapterSelectionActivity( 362 - this->renderer, this->mappedInput, epub, path, spineIdx, currentP, totalP, 363 - [this] { 364 - exitActivity(); 365 - requestUpdate(); 366 - }, 367 - [this](const int newSpineIndex) { 368 - if (currentSpineIndex != newSpineIndex) { 369 - currentSpineIndex = newSpineIndex; 311 + startActivityForResult( 312 + std::make_unique<EpubReaderChapterSelectionActivity>(renderer, mappedInput, epub, path, spineIdx), 313 + [this](const ActivityResult& result) { 314 + if (!result.isCancelled && currentSpineIndex != std::get<ChapterResult>(result.data).spineIndex) { 315 + currentSpineIndex = std::get<ChapterResult>(result.data).spineIndex; 370 316 nextPageNumber = 0; 371 317 section.reset(); 372 318 } 373 - exitActivity(); 374 - requestUpdate(); 375 - }, 376 - [this](const int newSpineIndex, const int newPage) { 377 - if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) { 378 - currentSpineIndex = newSpineIndex; 379 - nextPageNumber = newPage; 380 - section.reset(); 381 - } 382 - exitActivity(); 383 - requestUpdate(); 384 - })); 385 - 319 + }); 386 320 break; 387 321 } 388 322 case EpubReaderMenuActivity::MenuAction::FOOTNOTES: { 389 - exitActivity(); 390 - enterNewActivity(new EpubReaderFootnotesActivity( 391 - this->renderer, this->mappedInput, currentPageFootnotes, 392 - [this] { 393 - // Go back from footnotes list 394 - exitActivity(); 395 - requestUpdate(); 396 - }, 397 - [this](const char* href) { 398 - // Navigate to selected footnote 399 - navigateToHref(href, true); 400 - exitActivity(); 401 - requestUpdate(); 402 - })); 323 + startActivityForResult(std::make_unique<EpubReaderFootnotesActivity>(renderer, mappedInput, currentPageFootnotes), 324 + [this](const ActivityResult& result) { 325 + if (!result.isCancelled) { 326 + const auto& footnoteResult = std::get<FootnoteResult>(result.data); 327 + navigateToHref(footnoteResult.href, true); 328 + } 329 + requestUpdate(); 330 + }); 403 331 break; 404 332 } 405 333 case EpubReaderMenuActivity::MenuAction::GO_TO_PERCENT: { 406 - // Launch the slider-based percent selector and return here on confirm/cancel. 407 334 float bookProgress = 0.0f; 408 335 if (epub && epub->getBookSize() > 0 && section && section->pageCount > 0) { 409 336 const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount); 410 337 bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; 411 338 } 412 339 const int initialPercent = clampPercent(static_cast<int>(bookProgress + 0.5f)); 413 - exitActivity(); 414 - enterNewActivity(new EpubReaderPercentSelectionActivity( 415 - renderer, mappedInput, initialPercent, 416 - [this](const int percent) { 417 - // Apply the new position and exit back to the reader. 418 - jumpToPercent(percent); 419 - exitActivity(); 420 - requestUpdate(); 421 - }, 422 - [this]() { 423 - // Cancel selection and return to the reader. 424 - exitActivity(); 425 - requestUpdate(); 426 - })); 340 + startActivityForResult( 341 + std::make_unique<EpubReaderPercentSelectionActivity>(renderer, mappedInput, initialPercent), 342 + [this](const ActivityResult& result) { 343 + if (!result.isCancelled) { 344 + jumpToPercent(std::get<PercentResult>(result.data).percent); 345 + } 346 + }); 427 347 break; 428 348 } 429 349 case EpubReaderMenuActivity::MenuAction::DISPLAY_QR: { ··· 444 364 } 445 365 } 446 366 if (!fullText.empty()) { 447 - exitActivity(); 448 - enterNewActivity(new QrDisplayActivity(renderer, mappedInput, fullText, [this]() { 449 - exitActivity(); 450 - requestUpdate(); 451 - })); 367 + startActivityForResult(std::make_unique<QrDisplayActivity>(renderer, mappedInput, fullText), 368 + [this](const ActivityResult& result) {}); 452 369 break; 453 370 } 454 371 } 455 372 } 456 373 // If no text or page loading failed, just close menu 457 - exitActivity(); 458 374 requestUpdate(); 459 375 break; 460 376 } 461 377 case EpubReaderMenuActivity::MenuAction::GO_HOME: { 462 - // Defer go home to avoid race condition with display task 463 - pendingGoHome = true; 464 - break; 378 + onGoHome(); 379 + return; 465 380 } 466 381 case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: { 467 382 { 468 383 RenderLock lock(*this); 469 - if (epub) { 470 - // 2. BACKUP: Read current progress 471 - // We use the current variables that track our position 384 + if (epub && section) { 472 385 uint16_t backupSpine = currentSpineIndex; 473 386 uint16_t backupPage = section->currentPage; 474 387 uint16_t backupPageCount = section->pageCount; 475 - 476 388 section.reset(); 477 - // 3. WIPE: Clear the cache directory 478 389 epub->clearCache(); 479 - 480 - // 4. RESTORE: Re-setup the directory and rewrite the progress file 481 390 epub->setupCacheDir(); 482 - 483 391 saveProgress(backupSpine, backupPage, backupPageCount); 484 392 } 485 393 } 486 - // Defer go home to avoid race condition with display task 487 - pendingGoHome = true; 488 - break; 394 + onGoHome(); 395 + return; 489 396 } 490 397 case EpubReaderMenuActivity::MenuAction::SCREENSHOT: { 491 398 { 492 399 RenderLock lock(*this); 493 400 pendingScreenshot = true; 494 401 } 495 - exitActivity(); 496 402 requestUpdate(); 497 403 break; 498 404 } ··· 500 406 if (KOREADER_STORE.hasCredentials()) { 501 407 const int currentPage = section ? section->currentPage : 0; 502 408 const int totalPages = section ? section->pageCount : 0; 503 - exitActivity(); 504 - enterNewActivity(new KOReaderSyncActivity( 505 - renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, currentPage, totalPages, 506 - [this]() { 507 - // On cancel - defer exit to avoid use-after-free 508 - pendingSubactivityExit = true; 509 - }, 510 - [this](int newSpineIndex, int newPage) { 511 - // On sync complete - update position and defer exit 512 - if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) { 513 - currentSpineIndex = newSpineIndex; 514 - nextPageNumber = newPage; 515 - section.reset(); 409 + startActivityForResult( 410 + std::make_unique<KOReaderSyncActivity>(renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, 411 + currentPage, totalPages), 412 + [this](const ActivityResult& result) { 413 + if (!result.isCancelled) { 414 + const auto& sync = std::get<SyncResult>(result.data); 415 + if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) { 416 + currentSpineIndex = sync.spineIndex; 417 + nextPageNumber = sync.page; 418 + section.reset(); 419 + } 516 420 } 517 - pendingSubactivityExit = true; 518 - })); 421 + }); 519 422 } 520 423 break; 521 424 } ··· 550 453 } 551 454 552 455 // TODO: Failure handling 553 - void EpubReaderActivity::render(Activity::RenderLock&& lock) { 456 + void EpubReaderActivity::render(RenderLock&& lock) { 554 457 if (!epub) { 555 458 return; 556 459 } ··· 785 688 GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title); 786 689 } 787 690 788 - void EpubReaderActivity::navigateToHref(const char* href, const bool savePosition) { 789 - if (!epub || !href) return; 691 + void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) { 692 + if (!epub) return; 790 693 791 694 // Push current position onto saved stack 792 695 if (savePosition && section && footnoteDepth < MAX_FOOTNOTE_DEPTH) { ··· 795 698 LOG_DBG("ERS", "Saved position [%d]: spine %d, page %d", footnoteDepth, currentSpineIndex, section->currentPage); 796 699 } 797 700 798 - std::string hrefStr(href); 799 - 800 701 // Check for same-file anchor reference (#anchor only) 801 702 bool sameFile = !hrefStr.empty() && hrefStr[0] == '#'; 802 703 ··· 809 710 } 810 711 811 712 if (targetSpineIndex < 0) { 812 - LOG_DBG("ERS", "Could not resolve href: %s", href); 713 + LOG_DBG("ERS", "Could not resolve href: %s", hrefStr.c_str()); 813 714 if (savePosition && footnoteDepth > 0) footnoteDepth--; // undo push 814 715 return; 815 716 } ··· 821 722 section.reset(); 822 723 } 823 724 requestUpdate(); 824 - LOG_DBG("ERS", "Navigated to spine %d for href: %s", targetSpineIndex, href); 725 + LOG_DBG("ERS", "Navigated to spine %d for href: %s", targetSpineIndex, hrefStr.c_str()); 825 726 } 826 727 827 728 void EpubReaderActivity::restoreSavedPosition() {
+7 -15
src/activities/reader/EpubReaderActivity.h
··· 4 4 #include <Epub/Section.h> 5 5 6 6 #include "EpubReaderMenuActivity.h" 7 - #include "activities/ActivityWithSubactivity.h" 7 + #include "activities/Activity.h" 8 8 9 - class EpubReaderActivity final : public ActivityWithSubactivity { 9 + class EpubReaderActivity final : public Activity { 10 10 std::shared_ptr<Epub> epub; 11 11 std::unique_ptr<Section> section = nullptr; 12 12 int currentSpineIndex = 0; ··· 19 19 bool pendingPercentJump = false; 20 20 // Normalized 0.0-1.0 progress within the target spine item, computed from book percentage. 21 21 float pendingSpineProgress = 0.0f; 22 - bool pendingSubactivityExit = false; // Defer subactivity exit to avoid use-after-free 23 - bool pendingGoHome = false; // Defer go home to avoid race condition with display task 24 22 bool pendingScreenshot = false; 25 23 bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit 26 - const std::function<void()> onGoBack; 27 - const std::function<void()> onGoHome; 28 24 29 25 // Footnote support 30 26 std::vector<FootnoteEntry> currentPageFootnotes; ··· 42 38 void saveProgress(int spineIndex, int currentPage, int pageCount); 43 39 // Jump to a percentage of the book (0-100), mapping it to spine and page. 44 40 void jumpToPercent(int percent); 45 - void onReaderMenuBack(uint8_t orientation); 46 41 void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action); 47 42 void applyOrientation(uint8_t orientation); 48 43 49 44 // Footnote navigation 50 - void navigateToHref(const char* href, bool savePosition = false); 45 + void navigateToHref(const std::string& href, bool savePosition = false); 51 46 void restoreSavedPosition(); 52 47 53 48 public: 54 - explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Epub> epub, 55 - const std::function<void()>& onGoBack, const std::function<void()>& onGoHome) 56 - : ActivityWithSubactivity("EpubReader", renderer, mappedInput), 57 - epub(std::move(epub)), 58 - onGoBack(onGoBack), 59 - onGoHome(onGoHome) {} 49 + explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Epub> epub) 50 + : Activity("EpubReader", renderer, mappedInput), epub(std::move(epub)) {} 60 51 void onEnter() override; 61 52 void onExit() override; 62 53 void loop() override; 63 - void render(Activity::RenderLock&& lock) override; 54 + void render(RenderLock&& lock) override; 55 + bool isReaderActivity() const override { return true; } 64 56 };
+13 -11
src/activities/reader/EpubReaderChapterSelectionActivity.cpp
··· 26 26 } 27 27 28 28 void EpubReaderChapterSelectionActivity::onEnter() { 29 - ActivityWithSubactivity::onEnter(); 29 + Activity::onEnter(); 30 30 31 31 if (!epub) { 32 32 return; ··· 41 41 requestUpdate(); 42 42 } 43 43 44 - void EpubReaderChapterSelectionActivity::onExit() { ActivityWithSubactivity::onExit(); } 44 + void EpubReaderChapterSelectionActivity::onExit() { Activity::onExit(); } 45 45 46 46 void EpubReaderChapterSelectionActivity::loop() { 47 - if (subActivity) { 48 - subActivity->loop(); 49 - return; 50 - } 51 - 52 47 const int pageItems = getPageItems(); 53 48 const int totalItems = getTotalItems(); 54 49 55 50 if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 56 51 const auto newSpineIndex = epub->getSpineIndexForTocIndex(selectorIndex); 57 52 if (newSpineIndex == -1) { 58 - onGoBack(); 53 + ActivityResult result; 54 + result.isCancelled = true; 55 + setResult(std::move(result)); 56 + finish(); 59 57 } else { 60 - onSelectSpineIndex(newSpineIndex); 58 + setResult(ChapterResult{newSpineIndex}); 59 + finish(); 61 60 } 62 61 } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { 63 - onGoBack(); 62 + ActivityResult result; 63 + result.isCancelled = true; 64 + setResult(std::move(result)); 65 + finish(); 64 66 } 65 67 66 68 buttonNavigator.onNextRelease([this, totalItems] { ··· 84 86 }); 85 87 } 86 88 87 - void EpubReaderChapterSelectionActivity::render(Activity::RenderLock&&) { 89 + void EpubReaderChapterSelectionActivity::render(RenderLock&&) { 88 90 renderer.clearScreen(); 89 91 90 92 const auto pageWidth = renderer.getScreenWidth();
+6 -20
src/activities/reader/EpubReaderChapterSelectionActivity.h
··· 3 3 4 4 #include <memory> 5 5 6 - #include "../ActivityWithSubactivity.h" 6 + #include "../Activity.h" 7 7 #include "util/ButtonNavigator.h" 8 8 9 - class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity { 9 + class EpubReaderChapterSelectionActivity final : public Activity { 10 10 std::shared_ptr<Epub> epub; 11 11 std::string epubPath; 12 12 ButtonNavigator buttonNavigator; 13 13 int currentSpineIndex = 0; 14 - int currentPage = 0; 15 - int totalPagesInSpine = 0; 16 14 int selectorIndex = 0; 17 15 18 - const std::function<void()> onGoBack; 19 - const std::function<void(int newSpineIndex)> onSelectSpineIndex; 20 - const std::function<void(int newSpineIndex, int newPage)> onSyncPosition; 21 - 22 16 // Number of items that fit on a page, derived from logical screen height. 23 17 // This adapts automatically when switching between portrait and landscape. 24 18 int getPageItems() const; ··· 29 23 public: 30 24 explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 31 25 const std::shared_ptr<Epub>& epub, const std::string& epubPath, 32 - const int currentSpineIndex, const int currentPage, 33 - const int totalPagesInSpine, const std::function<void()>& onGoBack, 34 - const std::function<void(int newSpineIndex)>& onSelectSpineIndex, 35 - const std::function<void(int newSpineIndex, int newPage)>& onSyncPosition) 36 - : ActivityWithSubactivity("EpubReaderChapterSelection", renderer, mappedInput), 26 + const int currentSpineIndex) 27 + : Activity("EpubReaderChapterSelection", renderer, mappedInput), 37 28 epub(epub), 38 29 epubPath(epubPath), 39 - currentSpineIndex(currentSpineIndex), 40 - currentPage(currentPage), 41 - totalPagesInSpine(totalPagesInSpine), 42 - onGoBack(onGoBack), 43 - onSelectSpineIndex(onSelectSpineIndex), 44 - onSyncPosition(onSyncPosition) {} 30 + currentSpineIndex(currentSpineIndex) {} 45 31 void onEnter() override; 46 32 void onExit() override; 47 33 void loop() override; 48 - void render(Activity::RenderLock&&) override; 34 + void render(RenderLock&&) override; 49 35 };
+9 -10
src/activities/reader/EpubReaderFootnotesActivity.cpp
··· 10 10 #include "fontIds.h" 11 11 12 12 void EpubReaderFootnotesActivity::onEnter() { 13 - ActivityWithSubactivity::onEnter(); 13 + Activity::onEnter(); 14 14 selectedIndex = 0; 15 15 requestUpdate(); 16 16 } 17 17 18 - void EpubReaderFootnotesActivity::onExit() { ActivityWithSubactivity::onExit(); } 18 + void EpubReaderFootnotesActivity::onExit() { Activity::onExit(); } 19 19 20 20 void EpubReaderFootnotesActivity::loop() { 21 - if (subActivity) { 22 - subActivity->loop(); 23 - return; 24 - } 25 - 26 21 if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { 27 - onGoBack(); 22 + ActivityResult result; 23 + result.isCancelled = true; 24 + setResult(std::move(result)); 25 + finish(); 28 26 return; 29 27 } 30 28 31 29 if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 32 30 if (selectedIndex >= 0 && selectedIndex < static_cast<int>(footnotes.size())) { 33 - onSelectFootnote(footnotes[selectedIndex].href); 31 + setResult(FootnoteResult{footnotes[selectedIndex].href}); 32 + finish(); 34 33 } 35 34 return; 36 35 } ··· 50 49 }); 51 50 } 52 51 53 - void EpubReaderFootnotesActivity::render(Activity::RenderLock&&) { 52 + void EpubReaderFootnotesActivity::render(RenderLock&&) { 54 53 renderer.clearScreen(); 55 54 56 55 renderer.drawCenteredText(UI_12_FONT_ID, 15, tr(STR_FOOTNOTES), true, EpdFontFamily::BOLD);
+5 -12
src/activities/reader/EpubReaderFootnotesActivity.h
··· 6 6 #include <functional> 7 7 #include <vector> 8 8 9 - #include "../ActivityWithSubactivity.h" 9 + #include "../Activity.h" 10 10 #include "util/ButtonNavigator.h" 11 11 12 - class EpubReaderFootnotesActivity final : public ActivityWithSubactivity { 12 + class EpubReaderFootnotesActivity final : public Activity { 13 13 public: 14 14 explicit EpubReaderFootnotesActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 15 - const std::vector<FootnoteEntry>& footnotes, 16 - const std::function<void()>& onGoBack, 17 - const std::function<void(const char*)>& onSelectFootnote) 18 - : ActivityWithSubactivity("EpubReaderFootnotes", renderer, mappedInput), 19 - footnotes(footnotes), 20 - onGoBack(onGoBack), 21 - onSelectFootnote(onSelectFootnote) {} 15 + const std::vector<FootnoteEntry>& footnotes) 16 + : Activity("EpubReaderFootnotes", renderer, mappedInput), footnotes(footnotes) {} 22 17 23 18 void onEnter() override; 24 19 void onExit() override; 25 20 void loop() override; 26 - void render(Activity::RenderLock&&) override; 21 + void render(RenderLock&&) override; 27 22 28 23 private: 29 24 const std::vector<FootnoteEntry>& footnotes; 30 - const std::function<void()> onGoBack; 31 - const std::function<void(const char*)> onSelectFootnote; 32 25 int selectedIndex = 0; 33 26 int scrollOffset = 0; 34 27 ButtonNavigator buttonNavigator;
+14 -25
src/activities/reader/EpubReaderMenuActivity.cpp
··· 10 10 EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 11 11 const std::string& title, const int currentPage, const int totalPages, 12 12 const int bookProgressPercent, const uint8_t currentOrientation, 13 - const bool hasFootnotes, const std::function<void(uint8_t)>& onBack, 14 - const std::function<void(MenuAction)>& onAction) 15 - : ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput), 13 + const bool hasFootnotes) 14 + : Activity("EpubReaderMenu", renderer, mappedInput), 16 15 menuItems(buildMenuItems(hasFootnotes)), 17 16 title(title), 18 17 pendingOrientation(currentOrientation), 19 18 currentPage(currentPage), 20 19 totalPages(totalPages), 21 - bookProgressPercent(bookProgressPercent), 22 - onBack(onBack), 23 - onAction(onAction) {} 20 + bookProgressPercent(bookProgressPercent) {} 24 21 25 22 std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) { 26 23 std::vector<MenuItem> items; ··· 40 37 } 41 38 42 39 void EpubReaderMenuActivity::onEnter() { 43 - ActivityWithSubactivity::onEnter(); 40 + Activity::onEnter(); 44 41 requestUpdate(); 45 42 } 46 43 47 - void EpubReaderMenuActivity::onExit() { ActivityWithSubactivity::onExit(); } 44 + void EpubReaderMenuActivity::onExit() { Activity::onExit(); } 48 45 49 46 void EpubReaderMenuActivity::loop() { 50 - if (subActivity) { 51 - subActivity->loop(); 52 - return; 53 - } 54 - 55 47 // Handle navigation 56 48 buttonNavigator.onNext([this] { 57 49 selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast<int>(menuItems.size())); ··· 63 55 requestUpdate(); 64 56 }); 65 57 66 - // Use local variables for items we need to check after potential deletion 67 58 if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 68 59 const auto selectedAction = menuItems[selectedIndex].action; 69 60 if (selectedAction == MenuAction::ROTATE_SCREEN) { ··· 73 64 return; 74 65 } 75 66 76 - // 1. Capture the callback and action locally 77 - auto actionCallback = onAction; 78 - 79 - // 2. Execute the callback 80 - actionCallback(selectedAction); 81 - 82 - // 3. CRITICAL: Return immediately. 'this' is likely deleted now. 67 + setResult(MenuResult{static_cast<int>(selectedAction), pendingOrientation}); 68 + finish(); 83 69 return; 84 70 } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { 85 - // Return the pending orientation to the parent so it can apply on exit. 86 - onBack(pendingOrientation); 87 - return; // Also return here just in case 71 + ActivityResult result; 72 + result.isCancelled = true; 73 + result.data = MenuResult{-1, pendingOrientation}; 74 + setResult(std::move(result)); 75 + finish(); 76 + return; 88 77 } 89 78 } 90 79 91 - void EpubReaderMenuActivity::render(Activity::RenderLock&&) { 80 + void EpubReaderMenuActivity::render(RenderLock&&) { 92 81 renderer.clearScreen(); 93 82 const auto pageWidth = renderer.getScreenWidth(); 94 83 const auto orientation = renderer.getOrientation();
+4 -10
src/activities/reader/EpubReaderMenuActivity.h
··· 2 2 #include <Epub.h> 3 3 #include <I18n.h> 4 4 5 - #include <functional> 6 5 #include <string> 7 6 #include <vector> 8 7 9 - #include "../ActivityWithSubactivity.h" 8 + #include "../Activity.h" 10 9 #include "util/ButtonNavigator.h" 11 10 12 - class EpubReaderMenuActivity final : public ActivityWithSubactivity { 11 + class EpubReaderMenuActivity final : public Activity { 13 12 public: 14 13 // Menu actions available from the reader menu. 15 14 enum class MenuAction { ··· 26 25 27 26 explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, 28 27 const int currentPage, const int totalPages, const int bookProgressPercent, 29 - const uint8_t currentOrientation, const bool hasFootnotes, 30 - const std::function<void(uint8_t)>& onBack, 31 - const std::function<void(MenuAction)>& onAction); 28 + const uint8_t currentOrientation, const bool hasFootnotes); 32 29 33 30 void onEnter() override; 34 31 void onExit() override; 35 32 void loop() override; 36 - void render(Activity::RenderLock&&) override; 33 + void render(RenderLock&&) override; 37 34 38 35 private: 39 36 struct MenuItem { ··· 56 53 int currentPage = 0; 57 54 int totalPages = 0; 58 55 int bookProgressPercent = 0; 59 - 60 - const std::function<void(uint8_t)> onBack; 61 - const std::function<void(MenuAction)> onAction; 62 56 };
+9 -10
src/activities/reader/EpubReaderPercentSelectionActivity.cpp
··· 14 14 } // namespace 15 15 16 16 void EpubReaderPercentSelectionActivity::onEnter() { 17 - ActivityWithSubactivity::onEnter(); 17 + Activity::onEnter(); 18 18 // Set up rendering task and mark first frame dirty. 19 19 requestUpdate(); 20 20 } 21 21 22 - void EpubReaderPercentSelectionActivity::onExit() { ActivityWithSubactivity::onExit(); } 22 + void EpubReaderPercentSelectionActivity::onExit() { Activity::onExit(); } 23 23 24 24 void EpubReaderPercentSelectionActivity::adjustPercent(const int delta) { 25 25 // Apply delta and clamp within 0-100. ··· 33 33 } 34 34 35 35 void EpubReaderPercentSelectionActivity::loop() { 36 - if (subActivity) { 37 - subActivity->loop(); 38 - return; 39 - } 40 - 41 36 // Back cancels, confirm selects, arrows adjust the percent. 42 37 if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { 43 - onCancel(); 38 + ActivityResult result; 39 + result.isCancelled = true; 40 + setResult(std::move(result)); 41 + finish(); 44 42 return; 45 43 } 46 44 47 45 if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 48 - onSelect(percent); 46 + setResult(PercentResult{percent}); 47 + finish(); 49 48 return; 50 49 } 51 50 ··· 56 55 buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { adjustPercent(-kLargeStep); }); 57 56 } 58 57 59 - void EpubReaderPercentSelectionActivity::render(Activity::RenderLock&&) { 58 + void EpubReaderPercentSelectionActivity::render(RenderLock&&) { 60 59 renderer.clearScreen(); 61 60 62 61 // Title and numeric percent value.
+5 -16
src/activities/reader/EpubReaderPercentSelectionActivity.h
··· 1 1 #pragma once 2 2 3 - #include <functional> 4 - 5 3 #include "MappedInputManager.h" 6 - #include "activities/ActivityWithSubactivity.h" 4 + #include "activities/Activity.h" 7 5 #include "util/ButtonNavigator.h" 8 6 9 - class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity { 7 + class EpubReaderPercentSelectionActivity final : public Activity { 10 8 public: 11 9 // Slider-style percent selector for jumping within a book. 12 10 explicit EpubReaderPercentSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 13 - const int initialPercent, const std::function<void(int)>& onSelect, 14 - const std::function<void()>& onCancel) 15 - : ActivityWithSubactivity("EpubReaderPercentSelection", renderer, mappedInput), 16 - percent(initialPercent), 17 - onSelect(onSelect), 18 - onCancel(onCancel) {} 11 + const int initialPercent) 12 + : Activity("EpubReaderPercentSelection", renderer, mappedInput), percent(initialPercent) {} 19 13 20 14 void onEnter() override; 21 15 void onExit() override; 22 16 void loop() override; 23 - void render(Activity::RenderLock&&) override; 17 + void render(RenderLock&&) override; 24 18 25 19 private: 26 20 // Current percent value (0-100) shown on the slider. 27 21 int percent = 0; 28 22 29 23 ButtonNavigator buttonNavigator; 30 - 31 - // Callback invoked when the user confirms a percent. 32 - const std::function<void(int)> onSelect; 33 - // Callback invoked when the user cancels the slider. 34 - const std::function<void()> onCancel; 35 24 36 25 // Change the current percent by a delta and clamp within bounds. 37 26 void adjustPercent(int delta);
+42 -42
src/activities/reader/KOReaderSyncActivity.cpp
··· 51 51 } // namespace 52 52 53 53 void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { 54 - exitActivity(); 55 - 56 54 if (!success) { 57 55 LOG_DBG("KOSync", "WiFi connection failed, exiting"); 58 - onCancel(); 56 + ActivityResult result; 57 + result.isCancelled = true; 58 + setResult(std::move(result)); 59 + finish(); 59 60 return; 60 61 } 61 62 ··· 66 67 state = SYNCING; 67 68 statusMessage = tr(STR_SYNCING_TIME); 68 69 } 69 - requestUpdate(); 70 + requestUpdate(true); 70 71 71 72 // Sync time with NTP before making API requests 72 73 syncTimeWithNTP(); ··· 75 76 RenderLock lock(*this); 76 77 statusMessage = tr(STR_CALC_HASH); 77 78 } 78 - requestUpdate(); 79 + requestUpdate(true); 79 80 80 81 performSync(); 81 82 } ··· 93 94 state = SYNC_FAILED; 94 95 statusMessage = tr(STR_HASH_FAILED); 95 96 } 96 - requestUpdate(); 97 + requestUpdate(true); 97 98 return; 98 99 } 99 100 ··· 115 116 state = NO_REMOTE_PROGRESS; 116 117 hasRemoteProgress = false; 117 118 } 118 - requestUpdate(); 119 + requestUpdate(true); 119 120 return; 120 121 } 121 122 ··· 125 126 state = SYNC_FAILED; 126 127 statusMessage = KOReaderSyncClient::errorString(result); 127 128 } 128 - requestUpdate(); 129 + requestUpdate(true); 129 130 return; 130 131 } 131 132 ··· 149 150 selectedOption = 0; // Apply remote progress 150 151 } 151 152 } 152 - requestUpdate(); 153 + requestUpdate(true); 153 154 } 154 155 155 156 void KOReaderSyncActivity::performUpload() { ··· 158 159 state = UPLOADING; 159 160 statusMessage = tr(STR_UPLOAD_PROGRESS); 160 161 } 161 - requestUpdate(); 162 162 requestUpdateAndWait(); 163 163 164 164 // Convert current position to KOReader format ··· 188 188 RenderLock lock(*this); 189 189 state = UPLOAD_COMPLETE; 190 190 } 191 - requestUpdate(); 191 + requestUpdate(true); 192 192 } 193 193 194 194 void KOReaderSyncActivity::onEnter() { 195 - ActivityWithSubactivity::onEnter(); 195 + Activity::onEnter(); 196 196 197 197 // Check for credentials first 198 198 if (!KOREADER_STORE.hasCredentials()) { ··· 206 206 LOG_DBG("KOSync", "Already connected to WiFi"); 207 207 state = SYNCING; 208 208 statusMessage = tr(STR_SYNCING_TIME); 209 - requestUpdate(); 209 + requestUpdate(true); 210 210 211 211 // Perform sync directly (will be handled in loop) 212 212 xTaskCreate( ··· 218 218 RenderLock lock(*self); 219 219 self->statusMessage = tr(STR_CALC_HASH); 220 220 } 221 - self->requestUpdate(); 221 + self->requestUpdate(true); 222 222 self->performSync(); 223 223 vTaskDelete(nullptr); 224 224 }, ··· 228 228 229 229 // Launch WiFi selection subactivity 230 230 LOG_DBG("KOSync", "Launching WifiSelectionActivity..."); 231 - enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, 232 - [this](const bool connected) { onWifiSelectionComplete(connected); })); 231 + startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput), 232 + [this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); }); 233 233 } 234 234 235 235 void KOReaderSyncActivity::onExit() { 236 - ActivityWithSubactivity::onExit(); 236 + Activity::onExit(); 237 237 238 238 wifiOff(); 239 239 } 240 240 241 - void KOReaderSyncActivity::render(Activity::RenderLock&&) { 242 - if (subActivity) { 243 - return; 244 - } 245 - 241 + void KOReaderSyncActivity::render(RenderLock&&) { 246 242 const auto pageWidth = renderer.getScreenWidth(); 247 243 248 244 renderer.clearScreen(); ··· 357 353 } 358 354 359 355 void KOReaderSyncActivity::loop() { 360 - if (subActivity) { 361 - subActivity->loop(); 362 - return; 363 - } 364 - 365 356 if (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE) { 366 - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 367 - onCancel(); 357 + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { 358 + ActivityResult result; 359 + result.isCancelled = true; 360 + setResult(std::move(result)); 361 + finish(); 368 362 } 369 363 return; 370 364 } 371 365 372 366 if (state == SHOWING_RESULT) { 373 367 // Navigate options 374 - if (mappedInput.wasPressed(MappedInputManager::Button::Up) || 375 - mappedInput.wasPressed(MappedInputManager::Button::Left)) { 368 + if (mappedInput.wasReleased(MappedInputManager::Button::Up) || 369 + mappedInput.wasReleased(MappedInputManager::Button::Left)) { 376 370 selectedOption = (selectedOption + 1) % 2; // Wrap around among 2 options 377 371 requestUpdate(); 378 - } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || 379 - mappedInput.wasPressed(MappedInputManager::Button::Right)) { 372 + } else if (mappedInput.wasReleased(MappedInputManager::Button::Down) || 373 + mappedInput.wasReleased(MappedInputManager::Button::Right)) { 380 374 selectedOption = (selectedOption + 1) % 2; // Wrap around among 2 options 381 375 requestUpdate(); 382 376 } 383 377 384 - if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { 378 + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 385 379 if (selectedOption == 0) { 386 - // Apply remote progress — WiFi no longer needed 387 - wifiOff(); 388 - onSyncComplete(remotePosition.spineIndex, remotePosition.pageNumber); 380 + // Wifi will be turned off in onExit() 381 + setResult(SyncResult{remotePosition.spineIndex, remotePosition.pageNumber}); 382 + finish(); 389 383 } else if (selectedOption == 1) { 390 384 // Upload local progress 391 385 performUpload(); 392 386 } 393 387 } 394 388 395 - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 396 - onCancel(); 389 + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { 390 + ActivityResult result; 391 + result.isCancelled = true; 392 + setResult(std::move(result)); 393 + finish(); 397 394 } 398 395 return; 399 396 } 400 397 401 398 if (state == NO_REMOTE_PROGRESS) { 402 - if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { 399 + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 403 400 // Calculate hash if not done yet 404 401 if (documentHash.empty()) { 405 402 if (KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME) { ··· 411 408 performUpload(); 412 409 } 413 410 414 - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 415 - onCancel(); 411 + if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { 412 + ActivityResult result; 413 + result.isCancelled = true; 414 + setResult(std::move(result)); 415 + finish(); 416 416 } 417 417 return; 418 418 }
+6 -15
src/activities/reader/KOReaderSyncActivity.h
··· 6 6 7 7 #include "KOReaderSyncClient.h" 8 8 #include "ProgressMapper.h" 9 - #include "activities/ActivityWithSubactivity.h" 9 + #include "activities/Activity.h" 10 10 11 11 /** 12 12 * Activity for syncing reading progress with KOReader sync server. ··· 18 18 * 4. Show comparison and options (Apply/Upload) 19 19 * 5. Apply or upload progress 20 20 */ 21 - class KOReaderSyncActivity final : public ActivityWithSubactivity { 21 + class KOReaderSyncActivity final : public Activity { 22 22 public: 23 - using OnCancelCallback = std::function<void()>; 24 - using OnSyncCompleteCallback = std::function<void(int newSpineIndex, int newPageNumber)>; 25 - 26 23 explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 27 24 const std::shared_ptr<Epub>& epub, const std::string& epubPath, int currentSpineIndex, 28 - int currentPage, int totalPagesInSpine, OnCancelCallback onCancel, 29 - OnSyncCompleteCallback onSyncComplete) 30 - : ActivityWithSubactivity("KOReaderSync", renderer, mappedInput), 25 + int currentPage, int totalPagesInSpine) 26 + : Activity("KOReaderSync", renderer, mappedInput), 31 27 epub(epub), 32 28 epubPath(epubPath), 33 29 currentSpineIndex(currentSpineIndex), ··· 35 31 totalPagesInSpine(totalPagesInSpine), 36 32 remoteProgress{}, 37 33 remotePosition{}, 38 - localProgress{}, 39 - onCancel(std::move(onCancel)), 40 - onSyncComplete(std::move(onSyncComplete)) {} 34 + localProgress{} {} 41 35 42 36 void onEnter() override; 43 37 void onExit() override; 44 38 void loop() override; 45 - void render(Activity::RenderLock&&) override; 39 + void render(RenderLock&&) override; 46 40 bool preventAutoSleep() override { return state == CONNECTING || state == SYNCING; } 47 41 48 42 private: ··· 78 72 79 73 // Selection in result screen (0=Apply, 1=Upload) 80 74 int selectedOption = 0; 81 - 82 - OnCancelCallback onCancel; 83 - OnSyncCompleteCallback onSyncComplete; 84 75 85 76 void onWifiSelectionComplete(bool success); 86 77 void performSync();
+2 -2
src/activities/reader/QrDisplayActivity.cpp
··· 18 18 void QrDisplayActivity::loop() { 19 19 if (mappedInput.wasReleased(MappedInputManager::Button::Back) || 20 20 mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 21 - onGoBack(); 21 + finish(); 22 22 return; 23 23 } 24 24 } 25 25 26 - void QrDisplayActivity::render(Activity::RenderLock&&) { 26 + void QrDisplayActivity::render(RenderLock&&) { 27 27 renderer.clearScreen(); 28 28 auto metrics = UITheme::getInstance().getMetrics(); 29 29 const auto pageWidth = renderer.getScreenWidth();
+3 -5
src/activities/reader/QrDisplayActivity.h
··· 7 7 8 8 class QrDisplayActivity final : public Activity { 9 9 public: 10 - explicit QrDisplayActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& textPayload, 11 - const std::function<void()>& onGoBack) 12 - : Activity("QrDisplay", renderer, mappedInput), textPayload(textPayload), onGoBack(onGoBack) {} 10 + explicit QrDisplayActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& textPayload) 11 + : Activity("QrDisplay", renderer, mappedInput), textPayload(textPayload) {} 13 12 14 13 void onEnter() override; 15 14 void onExit() override; 16 15 void loop() override; 17 - void render(Activity::RenderLock&&) override; 16 + void render(RenderLock&&) override; 18 17 19 18 private: 20 19 std::string textPayload; 21 - const std::function<void()> onGoBack; 22 20 };
+9 -14
src/activities/reader/ReaderActivity.cpp
··· 79 79 80 80 void ReaderActivity::goToLibrary(const std::string& fromBookPath) { 81 81 // If coming from a book, start in that book's folder; otherwise start from root 82 - const auto initialPath = fromBookPath.empty() ? "/" : extractFolderPath(fromBookPath); 83 - onGoToLibrary(initialPath); 82 + auto initialPath = fromBookPath.empty() ? "/" : extractFolderPath(fromBookPath); 83 + activityManager.goToMyLibrary(std::move(initialPath)); 84 84 } 85 85 86 86 void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) { 87 87 const auto epubPath = epub->getPath(); 88 88 currentBookPath = epubPath; 89 - exitActivity(); 90 - enterNewActivity(new EpubReaderActivity( 91 - renderer, mappedInput, std::move(epub), [this, epubPath] { goToLibrary(epubPath); }, [this] { onGoBack(); })); 89 + activityManager.replaceActivity(std::make_unique<EpubReaderActivity>(renderer, mappedInput, std::move(epub))); 92 90 } 93 91 94 92 void ReaderActivity::onGoToBmpViewer(const std::string& path) { 95 - exitActivity(); 96 - enterNewActivity(new BmpViewerActivity(renderer, mappedInput, path, [this, path] { goToLibrary(path); })); 93 + activityManager.replaceActivity(std::make_unique<BmpViewerActivity>(renderer, mappedInput, path)); 97 94 } 98 95 99 96 void ReaderActivity::onGoToXtcReader(std::unique_ptr<Xtc> xtc) { 100 97 const auto xtcPath = xtc->getPath(); 101 98 currentBookPath = xtcPath; 102 - exitActivity(); 103 - enterNewActivity(new XtcReaderActivity( 104 - renderer, mappedInput, std::move(xtc), [this, xtcPath] { goToLibrary(xtcPath); }, [this] { onGoBack(); })); 99 + activityManager.replaceActivity(std::make_unique<XtcReaderActivity>(renderer, mappedInput, std::move(xtc))); 105 100 } 106 101 107 102 void ReaderActivity::onGoToTxtReader(std::unique_ptr<Txt> txt) { 108 103 const auto txtPath = txt->getPath(); 109 104 currentBookPath = txtPath; 110 - exitActivity(); 111 - enterNewActivity(new TxtReaderActivity( 112 - renderer, mappedInput, std::move(txt), [this, txtPath] { goToLibrary(txtPath); }, [this] { onGoBack(); })); 105 + activityManager.replaceActivity(std::make_unique<TxtReaderActivity>(renderer, mappedInput, std::move(txt))); 113 106 } 114 107 115 108 void ReaderActivity::onEnter() { 116 - ActivityWithSubactivity::onEnter(); 109 + Activity::onEnter(); 117 110 118 111 if (initialBookPath.empty()) { 119 112 goToLibrary(); // Start from root when entering via Browse ··· 146 139 onGoToEpubReader(std::move(epub)); 147 140 } 148 141 } 142 + 143 + void ReaderActivity::onGoBack() { finish(); }
+6 -11
src/activities/reader/ReaderActivity.h
··· 1 1 #pragma once 2 2 #include <memory> 3 3 4 - #include "../ActivityWithSubactivity.h" 4 + #include "../Activity.h" 5 5 #include "activities/home/MyLibraryActivity.h" 6 6 7 7 class Epub; 8 8 class Xtc; 9 9 class Txt; 10 10 11 - class ReaderActivity final : public ActivityWithSubactivity { 11 + class ReaderActivity final : public Activity { 12 12 std::string initialBookPath; 13 13 std::string currentBookPath; // Track current book path for navigation 14 - const std::function<void()> onGoBack; 15 - const std::function<void(const std::string&)> onGoToLibrary; 16 14 static std::unique_ptr<Epub> loadEpub(const std::string& path); 17 15 static std::unique_ptr<Xtc> loadXtc(const std::string& path); 18 16 static std::unique_ptr<Txt> loadTxt(const std::string& path); ··· 27 25 void onGoToTxtReader(std::unique_ptr<Txt> txt); 28 26 void onGoToBmpViewer(const std::string& path); 29 27 28 + void onGoBack(); 29 + 30 30 public: 31 - explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath, 32 - const std::function<void()>& onGoBack, 33 - const std::function<void(const std::string&)>& onGoToLibrary) 34 - : ActivityWithSubactivity("Reader", renderer, mappedInput), 35 - initialBookPath(std::move(initialBookPath)), 36 - onGoBack(onGoBack), 37 - onGoToLibrary(onGoToLibrary) {} 31 + explicit ReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialBookPath) 32 + : Activity("Reader", renderer, mappedInput), initialBookPath(std::move(initialBookPath)) {} 38 33 void onEnter() override; 39 34 bool isReaderActivity() const override { return true; } 40 35 };
+4 -9
src/activities/reader/TxtReaderActivity.cpp
··· 23 23 } // namespace 24 24 25 25 void TxtReaderActivity::onEnter() { 26 - ActivityWithSubactivity::onEnter(); 26 + Activity::onEnter(); 27 27 28 28 if (!txt) { 29 29 return; ··· 61 61 } 62 62 63 63 void TxtReaderActivity::onExit() { 64 - ActivityWithSubactivity::onExit(); 64 + Activity::onExit(); 65 65 66 66 // Reset orientation back to portrait for the rest of the UI 67 67 renderer.setOrientation(GfxRenderer::Orientation::Portrait); ··· 74 74 } 75 75 76 76 void TxtReaderActivity::loop() { 77 - if (subActivity) { 78 - subActivity->loop(); 79 - return; 80 - } 81 - 82 77 // Long press BACK (1s+) goes to file selection 83 78 if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) { 84 - onGoBack(); 79 + activityManager.goToMyLibrary(txt ? txt->getPath() : ""); 85 80 return; 86 81 } 87 82 ··· 325 320 return !outLines.empty(); 326 321 } 327 322 328 - void TxtReaderActivity::render(Activity::RenderLock&&) { 323 + void TxtReaderActivity::render(RenderLock&&) { 329 324 if (!txt) { 330 325 return; 331 326 }
+6 -12
src/activities/reader/TxtReaderActivity.h
··· 5 5 #include <vector> 6 6 7 7 #include "CrossPointSettings.h" 8 - #include "activities/ActivityWithSubactivity.h" 8 + #include "activities/Activity.h" 9 9 10 - class TxtReaderActivity final : public ActivityWithSubactivity { 10 + class TxtReaderActivity final : public Activity { 11 11 std::unique_ptr<Txt> txt; 12 12 13 13 int currentPage = 0; 14 14 int totalPages = 1; 15 15 int pagesUntilFullRefresh = 0; 16 - 17 - const std::function<void()> onGoBack; 18 - const std::function<void()> onGoHome; 19 16 20 17 // Streaming text reader - stores file offsets for each page 21 18 std::vector<size_t> pageOffsets; // File offset for start of each page ··· 45 42 void loadProgress(); 46 43 47 44 public: 48 - explicit TxtReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Txt> txt, 49 - const std::function<void()>& onGoBack, const std::function<void()>& onGoHome) 50 - : ActivityWithSubactivity("TxtReader", renderer, mappedInput), 51 - txt(std::move(txt)), 52 - onGoBack(onGoBack), 53 - onGoHome(onGoHome) {} 45 + explicit TxtReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Txt> txt) 46 + : Activity("TxtReader", renderer, mappedInput), txt(std::move(txt)) {} 54 47 void onEnter() override; 55 48 void onExit() override; 56 49 void loop() override; 57 - void render(Activity::RenderLock&&) override; 50 + void render(RenderLock&&) override; 51 + bool isReaderActivity() const override { return true; } 58 52 };
+11 -22
src/activities/reader/XtcReaderActivity.cpp
··· 26 26 } // namespace 27 27 28 28 void XtcReaderActivity::onEnter() { 29 - ActivityWithSubactivity::onEnter(); 29 + Activity::onEnter(); 30 30 31 31 if (!xtc) { 32 32 return; ··· 47 47 } 48 48 49 49 void XtcReaderActivity::onExit() { 50 - ActivityWithSubactivity::onExit(); 50 + Activity::onExit(); 51 51 52 52 APP_STATE.readerActivityLoadCount = 0; 53 53 APP_STATE.saveToFile(); ··· 55 55 } 56 56 57 57 void XtcReaderActivity::loop() { 58 - // Pass input responsibility to sub activity if exists 59 - if (subActivity) { 60 - subActivity->loop(); 61 - return; 62 - } 63 - 64 58 // Enter chapter selection activity 65 59 if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 66 60 if (xtc && xtc->hasChapters() && !xtc->getChapters().empty()) { 67 - exitActivity(); 68 - enterNewActivity(new XtcReaderChapterSelectionActivity( 69 - this->renderer, this->mappedInput, xtc, currentPage, 70 - [this] { 71 - exitActivity(); 72 - requestUpdate(); 73 - }, 74 - [this](const uint32_t newPage) { 75 - currentPage = newPage; 76 - exitActivity(); 77 - requestUpdate(); 78 - })); 61 + startActivityForResult( 62 + std::make_unique<XtcReaderChapterSelectionActivity>(renderer, mappedInput, xtc, currentPage), 63 + [this](const ActivityResult& result) { 64 + if (!result.isCancelled) { 65 + currentPage = std::get<PageResult>(result.data).page; 66 + } 67 + }); 79 68 } 80 69 } 81 70 82 71 // Long press BACK (1s+) goes to file selection 83 72 if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= goHomeMs) { 84 - onGoBack(); 73 + activityManager.goToMyLibrary(xtc ? xtc->getPath() : ""); 85 74 return; 86 75 } 87 76 ··· 135 124 } 136 125 } 137 126 138 - void XtcReaderActivity::render(Activity::RenderLock&&) { 127 + void XtcReaderActivity::render(RenderLock&&) { 139 128 if (!xtc) { 140 129 return; 141 130 }
+6 -12
src/activities/reader/XtcReaderActivity.h
··· 9 9 10 10 #include <Xtc.h> 11 11 12 - #include "activities/ActivityWithSubactivity.h" 12 + #include "activities/Activity.h" 13 13 14 - class XtcReaderActivity final : public ActivityWithSubactivity { 14 + class XtcReaderActivity final : public Activity { 15 15 std::shared_ptr<Xtc> xtc; 16 16 17 17 uint32_t currentPage = 0; 18 18 int pagesUntilFullRefresh = 0; 19 19 20 - const std::function<void()> onGoBack; 21 - const std::function<void()> onGoHome; 22 - 23 20 void renderPage(); 24 21 void saveProgress() const; 25 22 void loadProgress(); 26 23 27 24 public: 28 - explicit XtcReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Xtc> xtc, 29 - const std::function<void()>& onGoBack, const std::function<void()>& onGoHome) 30 - : ActivityWithSubactivity("XtcReader", renderer, mappedInput), 31 - xtc(std::move(xtc)), 32 - onGoBack(onGoBack), 33 - onGoHome(onGoHome) {} 25 + explicit XtcReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr<Xtc> xtc) 26 + : Activity("XtcReader", renderer, mappedInput), xtc(std::move(xtc)) {} 34 27 void onEnter() override; 35 28 void onExit() override; 36 29 void loop() override; 37 - void render(Activity::RenderLock&&) override; 30 + void render(RenderLock&&) override; 31 + bool isReaderActivity() const override { return true; } 38 32 };
+7 -3
src/activities/reader/XtcReaderChapterSelectionActivity.cpp
··· 59 59 if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 60 60 const auto& chapters = xtc->getChapters(); 61 61 if (!chapters.empty() && selectorIndex >= 0 && selectorIndex < static_cast<int>(chapters.size())) { 62 - onSelectPage(chapters[selectorIndex].startPage); 62 + setResult(PageResult{chapters[selectorIndex].startPage}); 63 + finish(); 63 64 } 64 65 } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { 65 - onGoBack(); 66 + ActivityResult result; 67 + result.isCancelled = true; 68 + setResult(std::move(result)); 69 + finish(); 66 70 } 67 71 68 72 buttonNavigator.onNextRelease([this, totalItems] { ··· 86 90 }); 87 91 } 88 92 89 - void XtcReaderChapterSelectionActivity::render(Activity::RenderLock&&) { 93 + void XtcReaderChapterSelectionActivity::render(RenderLock&&) { 90 94 renderer.clearScreen(); 91 95 92 96 const auto pageWidth = renderer.getScreenWidth();
+4 -12
src/activities/reader/XtcReaderChapterSelectionActivity.h
··· 12 12 uint32_t currentPage = 0; 13 13 int selectorIndex = 0; 14 14 15 - const std::function<void()> onGoBack; 16 - const std::function<void(uint32_t newPage)> onSelectPage; 17 - 18 15 int getPageItems() const; 19 16 int findChapterIndexForPage(uint32_t page) const; 20 17 21 18 public: 22 19 explicit XtcReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 23 - const std::shared_ptr<Xtc>& xtc, uint32_t currentPage, 24 - const std::function<void()>& onGoBack, 25 - const std::function<void(uint32_t newPage)>& onSelectPage) 26 - : Activity("XtcReaderChapterSelection", renderer, mappedInput), 27 - xtc(xtc), 28 - currentPage(currentPage), 29 - onGoBack(onGoBack), 30 - onSelectPage(onSelectPage) {} 20 + const std::shared_ptr<Xtc>& xtc, uint32_t currentPage) 21 + : Activity("XtcReaderChapterSelection", renderer, mappedInput), xtc(xtc), currentPage(currentPage) {} 31 22 void onEnter() override; 32 23 void onExit() override; 33 24 void loop() override; 34 - void render(Activity::RenderLock&&) override; 25 + void render(RenderLock&&) override; 26 + bool isReaderActivity() const override { return true; } 35 27 };
+6 -6
src/activities/settings/ButtonRemapActivity.cpp
··· 52 52 SETTINGS.frontButtonLeft = CrossPointSettings::FRONT_HW_LEFT; 53 53 SETTINGS.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT; 54 54 SETTINGS.saveToFile(); 55 - onBack(); 55 + finish(); 56 56 return; 57 57 } 58 58 59 59 if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { 60 60 // Exit without changing settings. 61 - onBack(); 61 + finish(); 62 62 return; 63 63 } 64 64 65 65 { 66 - // Wait for the UI to refresh before accepting another assignment. 66 + // Make sure UI done rendering before accepting another assignment. 67 67 // This avoids rapid double-presses that can advance the step without a visible redraw. 68 - requestUpdateAndWait(); 68 + RenderLock lock(*this); 69 69 70 70 // Wait for a front button press to assign to the current role. 71 71 const int pressedButton = mappedInput.getPressedFrontButton(); ··· 86 86 // All roles assigned; save to settings and exit. 87 87 applyTempMapping(); 88 88 SETTINGS.saveToFile(); 89 - onBack(); 89 + finish(); 90 90 return; 91 91 } 92 92 ··· 94 94 } 95 95 } 96 96 97 - void ButtonRemapActivity::render(Activity::RenderLock&&) { 97 + void ButtonRemapActivity::render(RenderLock&&) { 98 98 const auto labelForHardware = [&](uint8_t hardwareIndex) -> const char* { 99 99 for (uint8_t i = 0; i < kRoleCount; i++) { 100 100 if (tempMapping[i] == hardwareIndex) {
+3 -6
src/activities/settings/ButtonRemapActivity.h
··· 7 7 8 8 class ButtonRemapActivity final : public Activity { 9 9 public: 10 - explicit ButtonRemapActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 11 - const std::function<void()>& onBack) 12 - : Activity("ButtonRemap", renderer, mappedInput), onBack(onBack) {} 10 + explicit ButtonRemapActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) 11 + : Activity("ButtonRemap", renderer, mappedInput) {} 13 12 14 13 void onEnter() override; 15 14 void onExit() override; 16 15 void loop() override; 17 - void render(Activity::RenderLock&&) override; 16 + void render(RenderLock&&) override; 18 17 19 18 private: 20 19 // Rendering task state. 21 20 22 - // Callback used to exit the remap flow back to the settings list. 23 - const std::function<void()> onBack; 24 21 // Index of the logical role currently awaiting input. 25 22 uint8_t currentStep = 0; 26 23 // Temporary mapping from logical role -> hardware button index.
+34 -57
src/activities/settings/CalibreSettingsActivity.cpp
··· 17 17 } // namespace 18 18 19 19 void CalibreSettingsActivity::onEnter() { 20 - ActivityWithSubactivity::onEnter(); 20 + Activity::onEnter(); 21 21 22 22 selectedIndex = 0; 23 23 requestUpdate(); 24 24 } 25 25 26 - void CalibreSettingsActivity::onExit() { ActivityWithSubactivity::onExit(); } 26 + void CalibreSettingsActivity::onExit() { Activity::onExit(); } 27 27 28 28 void CalibreSettingsActivity::loop() { 29 - if (subActivity) { 30 - subActivity->loop(); 31 - return; 32 - } 33 - 34 29 if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 35 - onBack(); 30 + activityManager.popActivity(); 36 31 return; 37 32 } 38 33 ··· 56 51 void CalibreSettingsActivity::handleSelection() { 57 52 if (selectedIndex == 0) { 58 53 // OPDS Server URL 59 - exitActivity(); 60 - enterNewActivity(new KeyboardEntryActivity( 61 - renderer, mappedInput, tr(STR_CALIBRE_WEB_URL), SETTINGS.opdsServerUrl, 62 - 127, // maxLength 63 - false, // not password 64 - [this](const std::string& url) { 65 - strncpy(SETTINGS.opdsServerUrl, url.c_str(), sizeof(SETTINGS.opdsServerUrl) - 1); 66 - SETTINGS.opdsServerUrl[sizeof(SETTINGS.opdsServerUrl) - 1] = '\0'; 67 - SETTINGS.saveToFile(); 68 - exitActivity(); 69 - requestUpdate(); 70 - }, 71 - [this]() { 72 - exitActivity(); 73 - requestUpdate(); 74 - })); 54 + startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_CALIBRE_WEB_URL), 55 + SETTINGS.opdsServerUrl, 127, false), 56 + [this](const ActivityResult& result) { 57 + if (!result.isCancelled) { 58 + const auto& kb = std::get<KeyboardResult>(result.data); 59 + strncpy(SETTINGS.opdsServerUrl, kb.text.c_str(), sizeof(SETTINGS.opdsServerUrl) - 1); 60 + SETTINGS.opdsServerUrl[sizeof(SETTINGS.opdsServerUrl) - 1] = '\0'; 61 + SETTINGS.saveToFile(); 62 + } 63 + }); 75 64 } else if (selectedIndex == 1) { 76 65 // Username 77 - exitActivity(); 78 - enterNewActivity(new KeyboardEntryActivity( 79 - renderer, mappedInput, tr(STR_USERNAME), SETTINGS.opdsUsername, 80 - 63, // maxLength 81 - false, // not password 82 - [this](const std::string& username) { 83 - strncpy(SETTINGS.opdsUsername, username.c_str(), sizeof(SETTINGS.opdsUsername) - 1); 84 - SETTINGS.opdsUsername[sizeof(SETTINGS.opdsUsername) - 1] = '\0'; 85 - SETTINGS.saveToFile(); 86 - exitActivity(); 87 - requestUpdate(); 88 - }, 89 - [this]() { 90 - exitActivity(); 91 - requestUpdate(); 92 - })); 66 + startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_USERNAME), 67 + SETTINGS.opdsUsername, 63, false), 68 + [this](const ActivityResult& result) { 69 + if (!result.isCancelled) { 70 + const auto& kb = std::get<KeyboardResult>(result.data); 71 + strncpy(SETTINGS.opdsUsername, kb.text.c_str(), sizeof(SETTINGS.opdsUsername) - 1); 72 + SETTINGS.opdsUsername[sizeof(SETTINGS.opdsUsername) - 1] = '\0'; 73 + SETTINGS.saveToFile(); 74 + } 75 + }); 93 76 } else if (selectedIndex == 2) { 94 77 // Password 95 - exitActivity(); 96 - enterNewActivity(new KeyboardEntryActivity( 97 - renderer, mappedInput, tr(STR_PASSWORD), SETTINGS.opdsPassword, 98 - 63, // maxLength 99 - false, // not password mode 100 - [this](const std::string& password) { 101 - strncpy(SETTINGS.opdsPassword, password.c_str(), sizeof(SETTINGS.opdsPassword) - 1); 102 - SETTINGS.opdsPassword[sizeof(SETTINGS.opdsPassword) - 1] = '\0'; 103 - SETTINGS.saveToFile(); 104 - exitActivity(); 105 - requestUpdate(); 106 - }, 107 - [this]() { 108 - exitActivity(); 109 - requestUpdate(); 110 - })); 78 + startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_PASSWORD), 79 + SETTINGS.opdsPassword, 63, false), 80 + [this](const ActivityResult& result) { 81 + if (!result.isCancelled) { 82 + const auto& kb = std::get<KeyboardResult>(result.data); 83 + strncpy(SETTINGS.opdsPassword, kb.text.c_str(), sizeof(SETTINGS.opdsPassword) - 1); 84 + SETTINGS.opdsPassword[sizeof(SETTINGS.opdsPassword) - 1] = '\0'; 85 + SETTINGS.saveToFile(); 86 + } 87 + }); 111 88 } 112 89 } 113 90 114 - void CalibreSettingsActivity::render(Activity::RenderLock&&) { 91 + void CalibreSettingsActivity::render(RenderLock&&) { 115 92 renderer.clearScreen(); 116 93 117 94 const auto& metrics = UITheme::getInstance().getMetrics();
+5 -9
src/activities/settings/CalibreSettingsActivity.h
··· 1 1 #pragma once 2 2 3 - #include <functional> 4 - 5 - #include "activities/ActivityWithSubactivity.h" 3 + #include "activities/Activity.h" 6 4 #include "util/ButtonNavigator.h" 7 5 8 6 /** 9 7 * Submenu for OPDS Browser settings. 10 8 * Shows OPDS Server URL and HTTP authentication options. 11 9 */ 12 - class CalibreSettingsActivity final : public ActivityWithSubactivity { 10 + class CalibreSettingsActivity final : public Activity { 13 11 public: 14 - explicit CalibreSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 15 - const std::function<void()>& onBack) 16 - : ActivityWithSubactivity("CalibreSettings", renderer, mappedInput), onBack(onBack) {} 12 + explicit CalibreSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) 13 + : Activity("CalibreSettings", renderer, mappedInput) {} 17 14 18 15 void onEnter() override; 19 16 void onExit() override; 20 17 void loop() override; 21 - void render(Activity::RenderLock&&) override; 18 + void render(RenderLock&&) override; 22 19 23 20 private: 24 21 ButtonNavigator buttonNavigator; 25 22 26 23 size_t selectedIndex = 0; 27 - const std::function<void()> onBack; 28 24 void handleSelection(); 29 25 };
+3 -3
src/activities/settings/ClearCacheActivity.cpp
··· 10 10 #include "fontIds.h" 11 11 12 12 void ClearCacheActivity::onEnter() { 13 - ActivityWithSubactivity::onEnter(); 13 + Activity::onEnter(); 14 14 15 15 state = WARNING; 16 16 requestUpdate(); 17 17 } 18 18 19 - void ClearCacheActivity::onExit() { ActivityWithSubactivity::onExit(); } 19 + void ClearCacheActivity::onExit() { Activity::onExit(); } 20 20 21 - void ClearCacheActivity::render(Activity::RenderLock&&) { 21 + void ClearCacheActivity::render(RenderLock&&) { 22 22 const auto& metrics = UITheme::getInstance().getMetrics(); 23 23 const auto pageWidth = renderer.getScreenWidth(); 24 24 const auto pageHeight = renderer.getScreenHeight();
+6 -7
src/activities/settings/ClearCacheActivity.h
··· 2 2 3 3 #include <functional> 4 4 5 - #include "activities/ActivityWithSubactivity.h" 5 + #include "activities/Activity.h" 6 6 7 - class ClearCacheActivity final : public ActivityWithSubactivity { 7 + class ClearCacheActivity final : public Activity { 8 8 public: 9 - explicit ClearCacheActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 10 - const std::function<void()>& goBack) 11 - : ActivityWithSubactivity("ClearCache", renderer, mappedInput), goBack(goBack) {} 9 + explicit ClearCacheActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) 10 + : Activity("ClearCache", renderer, mappedInput) {} 12 11 13 12 void onEnter() override; 14 13 void onExit() override; 15 14 void loop() override; 16 15 bool skipLoopDelay() override { return true; } // Prevent power-saving mode 17 - void render(Activity::RenderLock&&) override; 16 + void render(RenderLock&&) override; 18 17 19 18 private: 20 19 enum State { WARNING, CLEARING, SUCCESS, FAILED }; 21 20 22 21 State state = WARNING; 23 22 24 - const std::function<void()> goBack; 23 + void goBack() { finish(); } 25 24 26 25 int clearedCount = 0; 27 26 int failedCount = 0;
+6 -13
src/activities/settings/KOReaderAuthActivity.cpp
··· 12 12 #include "fontIds.h" 13 13 14 14 void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) { 15 - exitActivity(); 16 - 17 15 if (!success) { 18 16 { 19 17 RenderLock lock(*this); ··· 51 49 } 52 50 53 51 void KOReaderAuthActivity::onEnter() { 54 - ActivityWithSubactivity::onEnter(); 52 + Activity::onEnter(); 55 53 56 54 // Turn on WiFi 57 55 WiFi.mode(WIFI_STA); ··· 74 72 } 75 73 76 74 // Launch WiFi selection 77 - enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, 78 - [this](const bool connected) { onWifiSelectionComplete(connected); })); 75 + startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput), 76 + [this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); }); 79 77 } 80 78 81 79 void KOReaderAuthActivity::onExit() { 82 - ActivityWithSubactivity::onExit(); 80 + Activity::onExit(); 83 81 84 82 // Turn off wifi 85 83 WiFi.disconnect(false); ··· 88 86 delay(100); 89 87 } 90 88 91 - void KOReaderAuthActivity::render(Activity::RenderLock&&) { 89 + void KOReaderAuthActivity::render(RenderLock&&) { 92 90 renderer.clearScreen(); 93 91 94 92 const auto& metrics = UITheme::getInstance().getMetrics(); ··· 115 113 } 116 114 117 115 void KOReaderAuthActivity::loop() { 118 - if (subActivity) { 119 - subActivity->loop(); 120 - return; 121 - } 122 - 123 116 if (state == SUCCESS || state == FAILED) { 124 117 if (mappedInput.wasPressed(MappedInputManager::Button::Back) || 125 118 mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { 126 - onComplete(); 119 + finish(); 127 120 } 128 121 } 129 122 }
+5 -8
src/activities/settings/KOReaderAuthActivity.h
··· 2 2 3 3 #include <functional> 4 4 5 - #include "activities/ActivityWithSubactivity.h" 5 + #include "activities/Activity.h" 6 6 7 7 /** 8 8 * Activity for testing KOReader credentials. 9 9 * Connects to WiFi and authenticates with the KOReader sync server. 10 10 */ 11 - class KOReaderAuthActivity final : public ActivityWithSubactivity { 11 + class KOReaderAuthActivity final : public Activity { 12 12 public: 13 - explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 14 - const std::function<void()>& onComplete) 15 - : ActivityWithSubactivity("KOReaderAuth", renderer, mappedInput), onComplete(onComplete) {} 13 + explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) 14 + : Activity("KOReaderAuth", renderer, mappedInput) {} 16 15 17 16 void onEnter() override; 18 17 void onExit() override; 19 18 void loop() override; 20 - void render(Activity::RenderLock&&) override; 19 + void render(RenderLock&&) override; 21 20 bool preventAutoSleep() override { return state == CONNECTING || state == AUTHENTICATING; } 22 21 23 22 private: ··· 26 25 State state = WIFI_SELECTION; 27 26 std::string statusMessage; 28 27 std::string errorMessage; 29 - 30 - const std::function<void()> onComplete; 31 28 32 29 void onWifiSelectionComplete(bool success); 33 30 void performAuthentication();
+39 -61
src/activities/settings/KOReaderSettingsActivity.cpp
··· 19 19 } // namespace 20 20 21 21 void KOReaderSettingsActivity::onEnter() { 22 - ActivityWithSubactivity::onEnter(); 22 + Activity::onEnter(); 23 23 24 24 selectedIndex = 0; 25 25 requestUpdate(); 26 26 } 27 27 28 - void KOReaderSettingsActivity::onExit() { ActivityWithSubactivity::onExit(); } 28 + void KOReaderSettingsActivity::onExit() { Activity::onExit(); } 29 29 30 30 void KOReaderSettingsActivity::loop() { 31 - if (subActivity) { 32 - subActivity->loop(); 33 - return; 34 - } 35 - 36 31 if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 37 - onBack(); 32 + activityManager.popActivity(); 38 33 return; 39 34 } 40 35 ··· 58 53 void KOReaderSettingsActivity::handleSelection() { 59 54 if (selectedIndex == 0) { 60 55 // Username 61 - exitActivity(); 62 - enterNewActivity(new KeyboardEntryActivity( 63 - renderer, mappedInput, tr(STR_KOREADER_USERNAME), KOREADER_STORE.getUsername(), 64 - 64, // maxLength 65 - false, // not password 66 - [this](const std::string& username) { 67 - KOREADER_STORE.setCredentials(username, KOREADER_STORE.getPassword()); 68 - KOREADER_STORE.saveToFile(); 69 - exitActivity(); 70 - requestUpdate(); 71 - }, 72 - [this]() { 73 - exitActivity(); 74 - requestUpdate(); 75 - })); 56 + startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_KOREADER_USERNAME), 57 + KOREADER_STORE.getUsername(), 58 + 64, // maxLength 59 + false), // not password 60 + [this](const ActivityResult& result) { 61 + if (!result.isCancelled) { 62 + const auto& kb = std::get<KeyboardResult>(result.data); 63 + KOREADER_STORE.setCredentials(kb.text, KOREADER_STORE.getPassword()); 64 + KOREADER_STORE.saveToFile(); 65 + } 66 + }); 76 67 } else if (selectedIndex == 1) { 77 68 // Password 78 - exitActivity(); 79 - enterNewActivity(new KeyboardEntryActivity( 80 - renderer, mappedInput, tr(STR_KOREADER_PASSWORD), KOREADER_STORE.getPassword(), 81 - 64, // maxLength 82 - false, // show characters 83 - [this](const std::string& password) { 84 - KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), password); 85 - KOREADER_STORE.saveToFile(); 86 - exitActivity(); 87 - requestUpdate(); 88 - }, 89 - [this]() { 90 - exitActivity(); 91 - requestUpdate(); 92 - })); 69 + startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_KOREADER_PASSWORD), 70 + KOREADER_STORE.getPassword(), 71 + 64, // maxLength 72 + false), // show characters 73 + [this](const ActivityResult& result) { 74 + if (!result.isCancelled) { 75 + const auto& kb = std::get<KeyboardResult>(result.data); 76 + KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), kb.text); 77 + KOREADER_STORE.saveToFile(); 78 + } 79 + }); 93 80 } else if (selectedIndex == 2) { 94 81 // Sync Server URL - prefill with https:// if empty to save typing 95 82 const std::string currentUrl = KOREADER_STORE.getServerUrl(); 96 83 const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl; 97 - exitActivity(); 98 - enterNewActivity(new KeyboardEntryActivity( 99 - renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, 100 - 128, // maxLength - URLs can be long 101 - false, // not password 102 - [this](const std::string& url) { 103 - // Clear if user just left the prefilled https:// 104 - const std::string urlToSave = (url == "https://" || url == "http://") ? "" : url; 105 - KOREADER_STORE.setServerUrl(urlToSave); 106 - KOREADER_STORE.saveToFile(); 107 - exitActivity(); 108 - requestUpdate(); 109 - }, 110 - [this]() { 111 - exitActivity(); 112 - requestUpdate(); 113 - })); 84 + startActivityForResult( 85 + std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl, 86 + 128, // maxLength - URLs can be long 87 + false), // not password 88 + [this](const ActivityResult& result) { 89 + if (!result.isCancelled) { 90 + const auto& kb = std::get<KeyboardResult>(result.data); 91 + const std::string urlToSave = (kb.text == "https://" || kb.text == "http://") ? "" : kb.text; 92 + KOREADER_STORE.setServerUrl(urlToSave); 93 + KOREADER_STORE.saveToFile(); 94 + } 95 + }); 114 96 } else if (selectedIndex == 3) { 115 97 // Document Matching - toggle between Filename and Binary 116 98 const auto current = KOREADER_STORE.getMatchMethod(); ··· 125 107 // Can't authenticate without credentials - just show message briefly 126 108 return; 127 109 } 128 - exitActivity(); 129 - enterNewActivity(new KOReaderAuthActivity(renderer, mappedInput, [this] { 130 - exitActivity(); 131 - requestUpdate(); 132 - })); 110 + startActivityForResult(std::make_unique<KOReaderAuthActivity>(renderer, mappedInput), [](const ActivityResult&) {}); 133 111 } 134 112 } 135 113 136 - void KOReaderSettingsActivity::render(Activity::RenderLock&&) { 114 + void KOReaderSettingsActivity::render(RenderLock&&) { 137 115 renderer.clearScreen(); 138 116 139 117 const auto& metrics = UITheme::getInstance().getMetrics();
+5 -9
src/activities/settings/KOReaderSettingsActivity.h
··· 1 1 #pragma once 2 2 3 - #include <functional> 4 - 5 - #include "activities/ActivityWithSubactivity.h" 3 + #include "activities/Activity.h" 6 4 #include "util/ButtonNavigator.h" 7 5 8 6 /** 9 7 * Submenu for KOReader Sync settings. 10 8 * Shows username, password, and authenticate options. 11 9 */ 12 - class KOReaderSettingsActivity final : public ActivityWithSubactivity { 10 + class KOReaderSettingsActivity final : public Activity { 13 11 public: 14 - explicit KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 15 - const std::function<void()>& onBack) 16 - : ActivityWithSubactivity("KOReaderSettings", renderer, mappedInput), onBack(onBack) {} 12 + explicit KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) 13 + : Activity("KOReaderSettings", renderer, mappedInput) {} 17 14 18 15 void onEnter() override; 19 16 void onExit() override; 20 17 void loop() override; 21 - void render(Activity::RenderLock&&) override; 18 + void render(RenderLock&&) override; 22 19 23 20 private: 24 21 ButtonNavigator buttonNavigator; 25 22 26 23 size_t selectedIndex = 0; 27 - const std::function<void()> onBack; 28 24 29 25 void handleSelection(); 30 26 };
+1 -1
src/activities/settings/LanguageSelectActivity.cpp
··· 58 58 onBack(); 59 59 } 60 60 61 - void LanguageSelectActivity::render(Activity::RenderLock&&) { 61 + void LanguageSelectActivity::render(RenderLock&&) { 62 62 renderer.clearScreen(); 63 63 64 64 const auto pageWidth = renderer.getScreenWidth();
+5 -6
src/activities/settings/LanguageSelectActivity.h
··· 5 5 6 6 #include <functional> 7 7 8 - #include "../ActivityWithSubactivity.h" 8 + #include "../Activity.h" 9 9 #include "components/UITheme.h" 10 10 #include "util/ButtonNavigator.h" 11 11 ··· 16 16 */ 17 17 class LanguageSelectActivity final : public Activity { 18 18 public: 19 - explicit LanguageSelectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 20 - const std::function<void()>& onBack) 21 - : Activity("LanguageSelect", renderer, mappedInput), onBack(onBack) {} 19 + explicit LanguageSelectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) 20 + : Activity("LanguageSelect", renderer, mappedInput) {} 22 21 23 22 void onEnter() override; 24 23 void onExit() override; 25 24 void loop() override; 26 - void render(Activity::RenderLock&&) override; 25 + void render(RenderLock&&) override; 27 26 28 27 private: 29 28 void handleSelection(); 30 29 31 - std::function<void()> onBack; 30 + void onBack() { finish(); } 32 31 ButtonNavigator buttonNavigator; 33 32 int selectedIndex = 0; 34 33 constexpr static uint8_t totalItems = getLanguageCount();
+9 -25
src/activities/settings/OtaUpdateActivity.cpp
··· 11 11 #include "network/OtaUpdater.h" 12 12 13 13 void OtaUpdateActivity::onWifiSelectionComplete(const bool success) { 14 - exitActivity(); 15 - 16 14 if (!success) { 17 15 LOG_ERR("OTA", "WiFi connection failed, exiting"); 18 - goBack(); 16 + activityManager.popActivity(); 19 17 return; 20 18 } 21 19 ··· 34 32 RenderLock lock(*this); 35 33 state = FAILED; 36 34 } 37 - requestUpdate(); 38 35 return; 39 36 } 40 37 ··· 44 41 RenderLock lock(*this); 45 42 state = NO_UPDATE; 46 43 } 47 - requestUpdate(); 48 44 return; 49 45 } 50 46 ··· 52 48 RenderLock lock(*this); 53 49 state = WAITING_CONFIRMATION; 54 50 } 55 - requestUpdate(); 56 51 } 57 52 58 53 void OtaUpdateActivity::onEnter() { 59 - ActivityWithSubactivity::onEnter(); 54 + Activity::onEnter(); 60 55 61 56 // Turn on WiFi immediately 62 57 LOG_DBG("OTA", "Turning on WiFi..."); ··· 64 59 65 60 // Launch WiFi selection subactivity 66 61 LOG_DBG("OTA", "Launching WifiSelectionActivity..."); 67 - enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, 68 - [this](const bool connected) { onWifiSelectionComplete(connected); })); 62 + startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput), 63 + [this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); }); 69 64 } 70 65 71 66 void OtaUpdateActivity::onExit() { 72 - ActivityWithSubactivity::onExit(); 67 + Activity::onExit(); 73 68 74 69 // Turn off wifi 75 70 WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame ··· 78 73 delay(100); // Allow WiFi hardware to fully power down 79 74 } 80 75 81 - void OtaUpdateActivity::render(Activity::RenderLock&&) { 82 - if (subActivity) { 83 - // Subactivity handles its own rendering 84 - return; 85 - } 86 - 76 + void OtaUpdateActivity::render(RenderLock&&) { 87 77 const auto& metrics = UITheme::getInstance().getMetrics(); 88 78 const auto pageWidth = renderer.getScreenWidth(); 89 79 const auto pageHeight = renderer.getScreenHeight(); ··· 154 144 requestUpdate(); 155 145 } 156 146 157 - if (subActivity) { 158 - subActivity->loop(); 159 - return; 160 - } 161 - 162 147 if (state == WAITING_CONFIRMATION) { 163 148 if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { 164 149 LOG_DBG("OTA", "New update available, starting download..."); ··· 166 151 RenderLock lock(*this); 167 152 state = UPDATE_IN_PROGRESS; 168 153 } 169 - requestUpdate(); 170 154 requestUpdateAndWait(); 171 155 const auto res = updater.installUpdate(); 172 156 ··· 188 172 } 189 173 190 174 if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 191 - goBack(); 175 + activityManager.popActivity(); 192 176 } 193 177 194 178 return; ··· 196 180 197 181 if (state == FAILED) { 198 182 if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 199 - goBack(); 183 + activityManager.popActivity(); 200 184 } 201 185 return; 202 186 } 203 187 204 188 if (state == NO_UPDATE) { 205 189 if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 206 - goBack(); 190 + activityManager.popActivity(); 207 191 } 208 192 return; 209 193 }
+5 -7
src/activities/settings/OtaUpdateActivity.h
··· 1 1 #pragma once 2 2 3 - #include "activities/ActivityWithSubactivity.h" 3 + #include "activities/Activity.h" 4 4 #include "network/OtaUpdater.h" 5 5 6 - class OtaUpdateActivity : public ActivityWithSubactivity { 6 + class OtaUpdateActivity : public Activity { 7 7 enum State { 8 8 WIFI_SELECTION, 9 9 CHECKING_FOR_UPDATE, ··· 18 18 // Can't initialize this to 0 or the first render doesn't happen 19 19 static constexpr unsigned int UNINITIALIZED_PERCENTAGE = 111; 20 20 21 - const std::function<void()> goBack; 22 21 State state = WIFI_SELECTION; 23 22 unsigned int lastUpdaterPercentage = UNINITIALIZED_PERCENTAGE; 24 23 OtaUpdater updater; ··· 26 25 void onWifiSelectionComplete(bool success); 27 26 28 27 public: 29 - explicit OtaUpdateActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 30 - const std::function<void()>& goBack) 31 - : ActivityWithSubactivity("OtaUpdate", renderer, mappedInput), goBack(goBack), updater() {} 28 + explicit OtaUpdateActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) 29 + : Activity("OtaUpdate", renderer, mappedInput), updater() {} 32 30 void onEnter() override; 33 31 void onExit() override; 34 32 void loop() override; 35 - void render(Activity::RenderLock&&) override; 33 + void render(RenderLock&&) override; 36 34 bool preventAutoSleep() override { return state == CHECKING_FOR_UPDATE || state == UPDATE_IN_PROGRESS; } 37 35 bool skipLoopDelay() override { return true; } // Prevent power-saving mode 38 36 };
+12 -28
src/activities/settings/SettingsActivity.cpp
··· 67 67 } 68 68 69 69 void SettingsActivity::onExit() { 70 - ActivityWithSubactivity::onExit(); 70 + Activity::onExit(); 71 71 72 72 UITheme::getInstance().reload(); // Re-apply theme in case it was changed 73 73 } 74 74 75 75 void SettingsActivity::loop() { 76 - if (subActivity) { 77 - subActivity->loop(); 78 - return; 79 - } 80 76 bool hasChangedCategory = false; 81 77 82 78 // Handle actions with early return ··· 164 160 SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step; 165 161 } 166 162 } else if (setting.type == SettingType::ACTION) { 167 - auto enterSubActivity = [this](Activity* activity) { 168 - exitActivity(); 169 - enterNewActivity(activity); 170 - }; 171 - 172 - auto onComplete = [this] { 173 - exitActivity(); 174 - requestUpdate(); 175 - }; 176 - 177 - auto onCompleteBool = [this](bool) { 178 - exitActivity(); 179 - requestUpdate(); 180 - }; 163 + auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); }; 181 164 182 165 switch (setting.action) { 183 166 case SettingAction::RemapFrontButtons: 184 - enterSubActivity(new ButtonRemapActivity(renderer, mappedInput, onComplete)); 167 + startActivityForResult(std::make_unique<ButtonRemapActivity>(renderer, mappedInput), resultHandler); 185 168 break; 186 169 case SettingAction::CustomiseStatusBar: 187 - enterSubActivity(new StatusBarSettingsActivity(renderer, mappedInput, onComplete)); 170 + startActivityForResult(std::make_unique<StatusBarSettingsActivity>(renderer, mappedInput), resultHandler); 188 171 break; 189 172 case SettingAction::KOReaderSync: 190 - enterSubActivity(new KOReaderSettingsActivity(renderer, mappedInput, onComplete)); 173 + startActivityForResult(std::make_unique<KOReaderSettingsActivity>(renderer, mappedInput), resultHandler); 191 174 break; 192 175 case SettingAction::OPDSBrowser: 193 - enterSubActivity(new CalibreSettingsActivity(renderer, mappedInput, onComplete)); 176 + startActivityForResult(std::make_unique<CalibreSettingsActivity>(renderer, mappedInput), resultHandler); 194 177 break; 195 178 case SettingAction::Network: 196 - enterSubActivity(new WifiSelectionActivity(renderer, mappedInput, onCompleteBool, false)); 179 + startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput, false), resultHandler); 197 180 break; 198 181 case SettingAction::ClearCache: 199 - enterSubActivity(new ClearCacheActivity(renderer, mappedInput, onComplete)); 182 + startActivityForResult(std::make_unique<ClearCacheActivity>(renderer, mappedInput), resultHandler); 200 183 break; 201 184 case SettingAction::CheckForUpdates: 202 - enterSubActivity(new OtaUpdateActivity(renderer, mappedInput, onComplete)); 185 + startActivityForResult(std::make_unique<OtaUpdateActivity>(renderer, mappedInput), resultHandler); 203 186 break; 204 187 case SettingAction::Language: 205 - enterSubActivity(new LanguageSelectActivity(renderer, mappedInput, onComplete)); 188 + startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler); 206 189 break; 207 190 case SettingAction::None: 208 191 // Do nothing 209 192 break; 210 193 } 194 + return; // Results will be handled in the result handler, so we can return early here 211 195 } else { 212 196 return; 213 197 } ··· 215 199 SETTINGS.saveToFile(); 216 200 } 217 201 218 - void SettingsActivity::render(Activity::RenderLock&&) { 202 + void SettingsActivity::render(RenderLock&&) { 219 203 renderer.clearScreen(); 220 204 221 205 const auto pageWidth = renderer.getScreenWidth();
+5 -8
src/activities/settings/SettingsActivity.h
··· 5 5 #include <string> 6 6 #include <vector> 7 7 8 - #include "activities/ActivityWithSubactivity.h" 8 + #include "activities/Activity.h" 9 9 #include "util/ButtonNavigator.h" 10 10 11 11 class CrossPointSettings; ··· 134 134 } 135 135 }; 136 136 137 - class SettingsActivity final : public ActivityWithSubactivity { 137 + class SettingsActivity final : public Activity { 138 138 ButtonNavigator buttonNavigator; 139 139 140 140 int selectedCategoryIndex = 0; // Currently selected category ··· 148 148 std::vector<SettingInfo> systemSettings; 149 149 const std::vector<SettingInfo>* currentSettings = nullptr; 150 150 151 - const std::function<void()> onGoHome; 152 - 153 151 static constexpr int categoryCount = 4; 154 152 static const StrId categoryNames[categoryCount]; 155 153 ··· 157 155 void toggleCurrentSetting(); 158 156 159 157 public: 160 - explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 161 - const std::function<void()>& onGoHome) 162 - : ActivityWithSubactivity("Settings", renderer, mappedInput), onGoHome(onGoHome) {} 158 + explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) 159 + : Activity("Settings", renderer, mappedInput) {} 163 160 void onEnter() override; 164 161 void onExit() override; 165 162 void loop() override; 166 - void render(Activity::RenderLock&&) override; 163 + void render(RenderLock&&) override; 167 164 };
+2 -2
src/activities/settings/StatusBarSettingsActivity.cpp
··· 58 58 59 59 void StatusBarSettingsActivity::loop() { 60 60 if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 61 - onBack(); 61 + finish(); 62 62 return; 63 63 } 64 64 ··· 114 114 SETTINGS.saveToFile(); 115 115 } 116 116 117 - void StatusBarSettingsActivity::render(Activity::RenderLock&&) { 117 + void StatusBarSettingsActivity::render(RenderLock&&) { 118 118 renderer.clearScreen(); 119 119 120 120 auto metrics = UITheme::getInstance().getMetrics();
+3 -7
src/activities/settings/StatusBarSettingsActivity.h
··· 9 9 // Reader status bar configuration activity 10 10 class StatusBarSettingsActivity final : public Activity { 11 11 public: 12 - explicit StatusBarSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 13 - const std::function<void()>& onBack) 14 - : Activity("StatusBarSettings", renderer, mappedInput), onBack(onBack) {} 12 + explicit StatusBarSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput) 13 + : Activity("StatusBarSettings", renderer, mappedInput) {} 15 14 16 15 void onEnter() override; 17 16 void onExit() override; 18 17 void loop() override; 19 - void render(Activity::RenderLock&&) override; 18 + void render(RenderLock&&) override; 20 19 21 20 private: 22 21 ButtonNavigator buttonNavigator; 23 22 24 23 int selectedIndex = 0; 25 24 26 - const std::function<void()> onBack; 27 - 28 - static void taskTrampoline(void* param); 29 25 void handleSelection(); 30 26 };
+3 -4
src/activities/util/BmpViewerActivity.cpp
··· 8 8 #include "components/UITheme.h" 9 9 #include "fontIds.h" 10 10 11 - BmpViewerActivity::BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string path, 12 - std::function<void()> onGoBack) 13 - : Activity("BmpViewer", renderer, mappedInput), filePath(std::move(path)), onGoBack(std::move(onGoBack)) {} 11 + BmpViewerActivity::BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string path) 12 + : Activity("BmpViewer", renderer, mappedInput), filePath(std::move(path)) {} 14 13 15 14 void BmpViewerActivity::onEnter() { 16 15 Activity::onEnter(); ··· 95 94 Activity::loop(); 96 95 97 96 if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { 98 - if (onGoBack) onGoBack(); 97 + onGoHome(); 99 98 return; 100 99 } 101 100 }
+1 -3
src/activities/util/BmpViewerActivity.h
··· 8 8 9 9 class BmpViewerActivity final : public Activity { 10 10 public: 11 - BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string filePath, 12 - std::function<void()> onGoBack); 11 + BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string filePath); 13 12 14 13 void onEnter() override; 15 14 void onExit() override; ··· 17 16 18 17 private: 19 18 std::string filePath; 20 - std::function<void()> onGoBack; 21 19 };
+27 -16
src/activities/util/KeyboardEntryActivity.cpp
··· 57 57 return layout[selectedRow][selectedCol]; 58 58 } 59 59 60 - void KeyboardEntryActivity::handleKeyPress() { 60 + bool KeyboardEntryActivity::handleKeyPress() { 61 61 // Handle special row (bottom row with shift, space, backspace, done) 62 62 if (selectedRow == SPECIAL_ROW) { 63 63 if (selectedCol >= SHIFT_COL && selectedCol < SPACE_COL) { 64 64 // Shift toggle (0 = lower case, 1 = upper case, 2 = shift lock) 65 65 shiftState = (shiftState + 1) % 3; 66 - return; 66 + return true; 67 67 } 68 68 69 69 if (selectedCol >= SPACE_COL && selectedCol < BACKSPACE_COL) { ··· 71 71 if (maxLength == 0 || text.length() < maxLength) { 72 72 text += ' '; 73 73 } 74 - return; 74 + return true; 75 75 } 76 76 77 77 if (selectedCol >= BACKSPACE_COL && selectedCol < DONE_COL) { ··· 79 79 if (!text.empty()) { 80 80 text.pop_back(); 81 81 } 82 - return; 82 + return true; 83 83 } 84 84 85 85 if (selectedCol >= DONE_COL) { 86 86 // Done button 87 - if (onComplete) { 88 - onComplete(text); 89 - } 90 - return; 87 + onComplete(text); 88 + return false; 91 89 } 92 90 } 93 91 94 92 // Regular character 95 93 const char c = getSelectedChar(); 96 94 if (c == '\0') { 97 - return; 95 + return true; 98 96 } 99 97 100 98 if (maxLength == 0 || text.length() < maxLength) { ··· 104 102 shiftState = 0; 105 103 } 106 104 } 105 + 106 + return true; 107 107 } 108 108 109 109 void KeyboardEntryActivity::loop() { ··· 177 177 178 178 // Selection 179 179 if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { 180 - handleKeyPress(); 181 - requestUpdate(); 180 + if (handleKeyPress()) { 181 + requestUpdate(); 182 + } 183 + // If handleKeyPress returns false, it means onComplete was triggered, no update needed 182 184 } 183 185 184 186 // Cancel 185 187 if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { 186 - if (onCancel) { 187 - onCancel(); 188 - } 189 - requestUpdate(); 188 + onCancel(); 190 189 } 191 190 } 192 191 193 - void KeyboardEntryActivity::render(Activity::RenderLock&&) { 192 + void KeyboardEntryActivity::render(RenderLock&&) { 194 193 renderer.clearScreen(); 195 194 196 195 const auto pageWidth = renderer.getScreenWidth(); ··· 321 320 322 321 renderer.displayBuffer(); 323 322 } 323 + 324 + void KeyboardEntryActivity::onComplete(std::string text) { 325 + setResult(KeyboardResult{std::move(text)}); 326 + finish(); 327 + } 328 + 329 + void KeyboardEntryActivity::onCancel() { 330 + ActivityResult result; 331 + result.isCancelled = true; 332 + setResult(std::move(result)); 333 + finish(); 334 + }
+8 -24
src/activities/util/KeyboardEntryActivity.h
··· 10 10 11 11 /** 12 12 * Reusable keyboard entry activity for text input. 13 - * Can be started from any activity that needs text entry. 14 - * 15 - * Usage: 16 - * 1. Create a KeyboardEntryActivity instance 17 - * 2. Set callbacks with setOnComplete() and setOnCancel() 18 - * 3. Call onEnter() to start the activity 19 - * 4. Call loop() in your main loop 20 - * 5. When complete or cancelled, callbacks will be invoked 13 + * Can be started from any activity that needs text entry via startActivityForResult() 21 14 */ 22 15 class KeyboardEntryActivity : public Activity { 23 16 public: 24 - // Callback types 25 - using OnCompleteCallback = std::function<void(const std::string&)>; 26 - using OnCancelCallback = std::function<void()>; 27 - 28 17 /** 29 18 * Constructor 30 19 * @param renderer Reference to the GfxRenderer for drawing ··· 33 22 * @param initialText Initial text to show in the input field 34 23 * @param maxLength Maximum length of input text (0 for unlimited) 35 24 * @param isPassword If true, display asterisks instead of actual characters 36 - * @param onComplete Callback invoked when input is complete 37 - * @param onCancel Callback invoked when input is cancelled 38 25 */ 39 26 explicit KeyboardEntryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, 40 27 std::string title = "Enter Text", std::string initialText = "", 41 - const size_t maxLength = 0, const bool isPassword = false, 42 - OnCompleteCallback onComplete = nullptr, OnCancelCallback onCancel = nullptr) 28 + const size_t maxLength = 0, const bool isPassword = false) 43 29 : Activity("KeyboardEntry", renderer, mappedInput), 44 30 title(std::move(title)), 45 31 text(std::move(initialText)), 46 32 maxLength(maxLength), 47 - isPassword(isPassword), 48 - onComplete(std::move(onComplete)), 49 - onCancel(std::move(onCancel)) {} 33 + isPassword(isPassword) {} 50 34 51 35 // Activity overrides 52 36 void onEnter() override; 53 37 void onExit() override; 54 38 void loop() override; 55 - void render(Activity::RenderLock&&) override; 39 + void render(RenderLock&&) override; 56 40 57 41 private: 58 42 std::string title; ··· 67 51 int selectedCol = 0; 68 52 int shiftState = 0; // 0 = lower case, 1 = upper case, 2 = shift lock) 69 53 70 - // Callbacks 71 - OnCompleteCallback onComplete; 72 - OnCancelCallback onCancel; 54 + // Handlers 55 + void onComplete(std::string text); 56 + void onCancel(); 73 57 74 58 // Keyboard layout 75 59 static constexpr int NUM_ROWS = 5; ··· 86 70 static constexpr int DONE_COL = 9; 87 71 88 72 char getSelectedChar() const; 89 - void handleKeyPress(); 73 + bool handleKeyPress(); // false if onComplete was triggered 90 74 int getRowLength(int row) const; 91 75 };
+16 -85
src/main.cpp
··· 18 18 #include "KOReaderCredentialStore.h" 19 19 #include "MappedInputManager.h" 20 20 #include "RecentBooksStore.h" 21 - #include "activities/boot_sleep/BootActivity.h" 22 - #include "activities/boot_sleep/SleepActivity.h" 23 - #include "activities/browser/OpdsBookBrowserActivity.h" 24 - #include "activities/home/HomeActivity.h" 25 - #include "activities/home/MyLibraryActivity.h" 26 - #include "activities/home/RecentBooksActivity.h" 27 - #include "activities/network/CrossPointWebServerActivity.h" 28 - #include "activities/reader/ReaderActivity.h" 29 - #include "activities/settings/SettingsActivity.h" 30 - #include "activities/util/FullScreenMessageActivity.h" 21 + #include "activities/Activity.h" 22 + #include "activities/ActivityManager.h" 31 23 #include "components/UITheme.h" 32 24 #include "fontIds.h" 33 25 #include "util/ButtonNavigator.h" ··· 37 29 HalGPIO gpio; 38 30 MappedInputManager mappedInputManager(gpio); 39 31 GfxRenderer renderer(display); 32 + ActivityManager activityManager(renderer, mappedInputManager); 40 33 FontDecompressor fontDecompressor; 41 - Activity* currentActivity; 42 34 43 35 // Fonts 44 36 EpdFont bookerly14RegularFont(&bookerly_14_regular); ··· 133 125 unsigned long t1 = 0; 134 126 unsigned long t2 = 0; 135 127 136 - void exitActivity() { 137 - if (currentActivity) { 138 - currentActivity->onExit(); 139 - delete currentActivity; 140 - currentActivity = nullptr; 141 - } 142 - } 143 - 144 - void enterNewActivity(Activity* activity) { 145 - currentActivity = activity; 146 - currentActivity->onEnter(); 147 - } 148 - 149 128 // Verify power button press duration on wake-up from deep sleep 150 129 // Pre-condition: isWakeupByPowerButton() == true 151 130 void verifyPowerButtonDuration() { ··· 201 180 // Enter deep sleep mode 202 181 void enterDeepSleep() { 203 182 HalPowerManager::Lock powerLock; // Ensure we are at normal CPU frequency for sleep preparation 204 - APP_STATE.lastSleepFromReader = currentActivity && currentActivity->isReaderActivity(); 183 + APP_STATE.lastSleepFromReader = activityManager.isReaderActivity(); 205 184 APP_STATE.saveToFile(); 206 - exitActivity(); 207 - enterNewActivity(new SleepActivity(renderer, mappedInputManager)); 185 + 186 + activityManager.goToSleep(); 208 187 209 188 display.deepSleep(); 210 189 LOG_DBG("MAIN", "Power button press calibration value: %lu ms", t2 - t1); ··· 213 192 powerManager.startDeepSleep(gpio); 214 193 } 215 194 216 - void onGoHome(); 217 - void onGoToMyLibraryWithPath(const std::string& path); 218 - void onGoToRecentBooks(); 219 - void onGoToReader(const std::string& initialEpubPath) { 220 - const std::string bookPath = initialEpubPath; // Copy before exitActivity() invalidates the reference 221 - exitActivity(); 222 - enterNewActivity(new ReaderActivity(renderer, mappedInputManager, bookPath, onGoHome, onGoToMyLibraryWithPath)); 223 - } 224 - 225 - void onGoToFileTransfer() { 226 - exitActivity(); 227 - enterNewActivity(new CrossPointWebServerActivity(renderer, mappedInputManager, onGoHome)); 228 - } 229 - 230 - void onGoToSettings() { 231 - exitActivity(); 232 - enterNewActivity(new SettingsActivity(renderer, mappedInputManager, onGoHome)); 233 - } 234 - 235 - void onGoToMyLibrary() { 236 - exitActivity(); 237 - enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader)); 238 - } 239 - 240 - void onGoToRecentBooks() { 241 - exitActivity(); 242 - enterNewActivity(new RecentBooksActivity(renderer, mappedInputManager, onGoHome, onGoToReader)); 243 - } 244 - 245 - void onGoToMyLibraryWithPath(const std::string& path) { 246 - exitActivity(); 247 - enterNewActivity(new MyLibraryActivity(renderer, mappedInputManager, onGoHome, onGoToReader, path)); 248 - } 249 - 250 - void onGoToBrowser() { 251 - exitActivity(); 252 - enterNewActivity(new OpdsBookBrowserActivity(renderer, mappedInputManager, onGoHome)); 253 - } 254 - 255 - void onGoHome() { 256 - exitActivity(); 257 - enterNewActivity(new HomeActivity(renderer, mappedInputManager, onGoToReader, onGoToMyLibrary, onGoToRecentBooks, 258 - onGoToSettings, onGoToFileTransfer, onGoToBrowser)); 259 - } 260 - 261 195 void setupDisplayAndFonts() { 262 196 display.begin(); 263 197 renderer.begin(); 198 + activityManager.begin(); 264 199 LOG_DBG("MAIN", "Display initialized"); 265 200 266 201 // Initialize font decompressor for compressed reader fonts ··· 310 245 if (!Storage.begin()) { 311 246 LOG_ERR("MAIN", "SD card initialization failed"); 312 247 setupDisplayAndFonts(); 313 - exitActivity(); 314 - enterNewActivity(new FullScreenMessageActivity(renderer, mappedInputManager, "SD card error", EpdFontFamily::BOLD)); 248 + activityManager.goToFullScreenMessage("SD card error", EpdFontFamily::BOLD); 315 249 return; 316 250 } 317 251 ··· 344 278 345 279 setupDisplayAndFonts(); 346 280 347 - exitActivity(); 348 - enterNewActivity(new BootActivity(renderer, mappedInputManager)); 281 + activityManager.goToBoot(); 349 282 350 283 APP_STATE.loadFromFile(); 351 284 RECENT_BOOKS.loadFromFile(); ··· 354 287 // crashed (indicated by readerActivityLoadCount > 0) 355 288 if (APP_STATE.openEpubPath.empty() || !APP_STATE.lastSleepFromReader || 356 289 mappedInputManager.isPressed(MappedInputManager::Button::Back) || APP_STATE.readerActivityLoadCount > 0) { 357 - onGoHome(); 290 + activityManager.goHome(); 358 291 } else { 359 292 // Clear app state to avoid getting into a boot loop if the epub doesn't load 360 293 const auto path = APP_STATE.openEpubPath; 361 294 APP_STATE.openEpubPath = ""; 362 295 APP_STATE.readerActivityLoadCount++; 363 296 APP_STATE.saveToFile(); 364 - onGoToReader(path); 297 + activityManager.goToReader(path); 365 298 } 366 299 367 300 // Ensure we're not still holding the power button before leaving setup ··· 401 334 402 335 // Check for any user activity (button press or release) or active background work 403 336 static unsigned long lastActivityTime = millis(); 404 - if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || (currentActivity && currentActivity->preventAutoSleep())) { 337 + if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || activityManager.preventAutoSleep()) { 405 338 lastActivityTime = millis(); // Reset inactivity timer 406 339 powerManager.setPowerSaving(false); // Restore normal CPU frequency on user activity 407 340 } ··· 410 343 if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.isPressed(HalGPIO::BTN_DOWN)) { 411 344 if (screenshotButtonsReleased) { 412 345 screenshotButtonsReleased = false; 413 - if (currentActivity) { 414 - Activity::RenderLock lock(*currentActivity); 346 + { 347 + RenderLock lock; 415 348 ScreenshotUtil::takeScreenshot(renderer); 416 349 } 417 350 } ··· 439 372 } 440 373 441 374 const unsigned long activityStartTime = millis(); 442 - if (currentActivity) { 443 - currentActivity->loop(); 444 - } 375 + activityManager.loop(); 445 376 const unsigned long activityDuration = millis() - activityStartTime; 446 377 447 378 const unsigned long loopDuration = millis() - loopStartTime; ··· 455 386 // Add delay at the end of the loop to prevent tight spinning 456 387 // When an activity requests skip loop delay (e.g., webserver running), use yield() for faster response 457 388 // Otherwise, use longer delay to save power 458 - if (currentActivity && currentActivity->skipLoopDelay()) { 389 + if (activityManager.skipLoopDelay()) { 459 390 powerManager.setPowerSaving(false); // Make sure we're at full performance when skipLoopDelay is requested 460 391 yield(); // Give FreeRTOS a chance to run tasks, but return immediately 461 392 } else {