#include "ActivityManager.h" #include #include #include #include #include #include #include "CrossPointState.h" #include "OpdsServerStore.h" #include "SdCardFontGlobals.h" #include "boot_sleep/BootActivity.h" #include "boot_sleep/SleepActivity.h" #include "browser/OpdsBookBrowserActivity.h" #include "home/FileBrowserActivity.h" #include "home/GlobalBookmarksActivity.h" #include "home/HomeActivity.h" #include "home/RecentBooksActivity.h" #include "network/CrossPointWebServerActivity.h" #include "reader/KOReaderSyncActivity.h" #include "reader/ReaderActivity.h" #include "settings/OpdsServerListActivity.h" #include "settings/SettingsActivity.h" #include "util/FullScreenMessageActivity.h" #include "weather/WeatherActivity.h" #ifndef DEBUG_MEMORY_CONSUMPTION #define DEBUG_MEMORY_CONSUMPTION 0 #endif void ActivityManager::begin() { xTaskCreate(&renderTaskTrampoline, "ActivityManagerRender", 8192, // Stack size this, // Parameters 1, // Priority &renderTaskHandle // Task handle ); assert(renderTaskHandle != nullptr && "Failed to create render task"); } #if DEBUG_MEMORY_CONSUMPTION static void logActivityStackState(const char* stage, Activity* currentActivity, size_t stackSize) { const uint32_t freeHeap = esp_get_free_heap_size(); const uint32_t contigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT); LOG_DBG("ACT", "%s: current=%s stackSize=%zu free=%lu contig=%lu", stage, currentActivity ? currentActivity->getName().c_str() : "", stackSize, freeHeap, contigHeap); } #else static inline void logActivityStackState(const char*, Activity*, size_t) {} #endif void ActivityManager::renderTaskTrampoline(void* param) { auto* self = static_cast(param); self->renderTaskLoop(); } void ActivityManager::renderTaskLoop() { while (true) { ulTaskNotifyTake(pdTRUE, portMAX_DELAY); // Acquire the lock before reading currentActivity to avoid a TOCTOU race // where the main task deletes the activity between the null-check and render(). RenderLock lock; if (currentActivity) { HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering currentActivity->render(std::move(lock)); } // Notify any task blocked in requestUpdateAndWait() that the render is done. TaskHandle_t waiter = nullptr; taskENTER_CRITICAL(nullptr); waiter = waitingTaskHandle; waitingTaskHandle = nullptr; taskEXIT_CRITICAL(nullptr); if (waiter) { xTaskNotify(waiter, 1, eIncrement); } } } void ActivityManager::loop() { // Drain leftover input after an activity transition so that the button press/release // used to leave one activity cannot bleed into the next. We consume events until // every button is released and no press/release edges remain. if (drainInput) { if (mappedInput.wasAnyPressed() || mappedInput.wasAnyReleased() || mappedInput.isPressed(MappedInputManager::Button::Back) || mappedInput.isPressed(MappedInputManager::Button::Confirm) || mappedInput.isPressed(MappedInputManager::Button::Left) || mappedInput.isPressed(MappedInputManager::Button::Right) || mappedInput.isPressed(MappedInputManager::Button::Up) || mappedInput.isPressed(MappedInputManager::Button::Down)) { // Still have pending input — skip the activity loop but continue with // the rest (pending-action processing, render flushing) so that // transitions and screen updates are not delayed. } else { drainInput = false; } } if (!drainInput && currentActivity) { // Note: do not hold a lock here, the loop() method must be responsible for acquire one if needed currentActivity->loop(); } if (SETTINGS.useClock && HalClock::isSynced()) { time_t now = HalClock::now(); if (now > 0) { static time_t lastMinute = -1; time_t minute = now / 60; if (minute != lastMinute) { lastMinute = minute; requestUpdate(); } } } while (pendingAction != PendingAction::None) { if (pendingAction == PendingAction::Pop) { RenderLock lock; if (!currentActivity) { // Should never happen in practice LOG_ERR("ACT", "Pop set but currentActivity is null; ignoring pop request"); pendingAction = PendingAction::None; continue; } ActivityResult pendingResult = std::move(currentActivity->result); // Destroy the current activity exitActivity(lock); pendingAction = PendingAction::None; if (stackActivities.empty()) { LOG_DBG("ACT", "No more activities on stack, returning from child"); lock.unlock(); // returnFromChild may acquire its own lock via replaceActivity returnFromChild(); continue; // Will launch the target activity immediately } else { currentActivity = std::move(stackActivities.back()); stackActivities.pop_back(); LOG_DBG("ACT", "Popped from activity stack, new size = %zu", stackActivities.size()); // Handle result if necessary if (currentActivity->resultHandler) { LOG_DBG("ACT", "Handling result for popped activity"); // Move it here to avoid the case where handler calling another startActivityForResult() auto handler = std::move(currentActivity->resultHandler); currentActivity->resultHandler = nullptr; lock.unlock(); // Handler may acquire its own lock handler(pendingResult); } // Arm input drain so the button that triggered the pop doesn't bleed into the // restored activity (or into a new activity the handler just pushed). drainInput = true; buttonEvents.drain(); // Request an update to ensure the popped activity gets re-rendered if (pendingAction == PendingAction::None) { requestUpdate(); } // Handler may request another pending action, we will handle it in the next loop iteration continue; } } else if (pendingActivity) { // Current activity has requested a new activity to be launched RenderLock lock; if (pendingAction == PendingAction::Replace) { #if DEBUG_MEMORY_CONSUMPTION logActivityStackState("replace_before", currentActivity.get(), stackActivities.size()); #endif // Destroy the current activity exitActivity(lock); // Clear the stack while (!stackActivities.empty()) { stackActivities.back()->onExit(); stackActivities.pop_back(); } #if DEBUG_MEMORY_CONSUMPTION logActivityStackState("replace_after_clear", nullptr, stackActivities.size()); #endif } else if (pendingAction == PendingAction::Push) { #if DEBUG_MEMORY_CONSUMPTION logActivityStackState("push_before", currentActivity.get(), stackActivities.size()); #endif // Move current activity to stack stackActivities.push_back(std::move(currentActivity)); #if DEBUG_MEMORY_CONSUMPTION LOG_DBG("ACT", "Pushed to activity stack, new size = %zu", stackActivities.size()); logActivityStackState("push_after", currentActivity.get(), stackActivities.size()); #else LOG_DBG("ACT", "Pushed to activity stack, new size = %zu", stackActivities.size()); #endif } pendingAction = PendingAction::None; currentActivity = std::move(pendingActivity); lock.unlock(); // onEnter may acquire its own lock currentActivity->onEnter(); // Arm input drain so the button that triggered the transition doesn't bleed // into the new activity. drainInput = true; buttonEvents.drain(); // onEnter may request another pending action, we will handle it in the next loop iteration continue; } } if (requestedUpdate) { taskENTER_CRITICAL(nullptr); requestedUpdate = false; taskEXIT_CRITICAL(nullptr); // Using direct notification to signal the render task to update // Increment counter so multiple rapid calls won't be lost if (renderTaskHandle) { xTaskNotify(renderTaskHandle, 1, eIncrement); } } } void ActivityManager::exitActivity(const RenderLock& lock) { // Note: lock must be held by the caller if (currentActivity) { currentActivity->onExit(); currentActivity.reset(); } } void ActivityManager::replaceActivity(std::unique_ptr&& newActivity) { // Note: no lock here, this is usually called by loop() and we may run into deadlock if (currentActivity) { // Defer launch if we're currently in an activity, to avoid deleting the current activity // leading to the "delete this" problem #if DEBUG_MEMORY_CONSUMPTION LOG_DBG("ACT", "replaceActivity requested: current=%s stackSize=%zu", currentActivity->getName().c_str(), stackActivities.size()); #endif pendingActivity = std::move(newActivity); pendingAction = PendingAction::Replace; } else { // No current activity, safe to launch immediately currentActivity = std::move(newActivity); currentActivity->onEnter(); } } void ActivityManager::goToFileTransfer() { replaceActivity(std::make_unique(renderer, mappedInput)); } void ActivityManager::goToSettings() { replaceActivity(std::make_unique(renderer, mappedInput)); } void ActivityManager::goToFileBrowser(std::string path, std::string focusName) { replaceActivity(std::make_unique(renderer, mappedInput, std::move(path), std::move(focusName))); } void ActivityManager::goToRecentBooks(int focusIndex) { replaceActivity(std::make_unique(renderer, mappedInput, focusIndex)); } void ActivityManager::goToGlobalBookmarks() { goToGlobalBookmarks({}); } void ActivityManager::goToGlobalBookmarks(ReturnHint hint) { hasReturnHint = false; replaceActivity(std::make_unique(renderer, mappedInput, std::move(hint))); } void ActivityManager::goToBrowser() { const auto& servers = OPDS_STORE.getServers(); // Skip the server picker when there's only one server configured if (servers.size() == 1) { replaceActivity(std::make_unique(renderer, mappedInput, servers[0])); } else { replaceActivity(std::make_unique(renderer, mappedInput, true)); } } void ActivityManager::goToReader(std::string path) { RenderLock lock; ensureSdFontLoaded(); replaceActivity(std::make_unique(renderer, mappedInput, std::move(path))); } void ActivityManager::goToKOReaderSync() { const auto& sync = APP_STATE.koReaderSyncSession; if (!sync.active || sync.epubPath.empty()) { LOG_ERR("ACT", "Cannot launch KOReader sync without an active EPUB handoff"); goHome(); return; } replaceActivity(std::make_unique(renderer, mappedInput, sync.epubPath, sync.spineIndex, sync.page, sync.totalPagesInSpine, sync.paragraphIndex, sync.hasParagraphIndex, sync.xhtmlSeekHint, sync.intent)); } void ActivityManager::replaceWithReader(std::string path, ReturnHint hint) { returnHint = std::move(hint); hasReturnHint = true; RenderLock lock; ensureSdFontLoaded(); replaceActivity(std::make_unique(renderer, mappedInput, std::move(path))); } void ActivityManager::replaceWithFileBrowser(std::string path, ReturnHint hint, std::string focusName) { returnHint = std::move(hint); hasReturnHint = true; replaceActivity(std::make_unique(renderer, mappedInput, std::move(path), std::move(focusName))); } void ActivityManager::replaceWithRecentBooks(ReturnHint hint) { returnHint = std::move(hint); hasReturnHint = true; replaceActivity(std::make_unique(renderer, mappedInput, -1)); } void ActivityManager::returnFromChild() { if (!hasReturnHint) { goHome(); return; } ReturnHint hint = std::move(returnHint); returnHint = {}; hasReturnHint = false; switch (hint.target) { case ReturnTo::FileBrowser: goToFileBrowser(std::move(hint.path), std::move(hint.selectName)); break; case ReturnTo::RecentBooks: goToRecentBooks(hint.selectIndex); break; case ReturnTo::GlobalBookmarks: goToGlobalBookmarks(std::move(hint)); break; case ReturnTo::Home: default: goHome(std::move(hint.selectName), hint.selectIndex); break; } } void ActivityManager::goToSleep() { replaceActivity(std::make_unique(renderer, mappedInput)); loop(); // Important: sleep screen must be rendered immediately, the caller will go to sleep right after this returns } void ActivityManager::goToBoot() { replaceActivity(std::make_unique(renderer, mappedInput)); } void ActivityManager::goToFullScreenMessage(std::string message, EpdFontFamily::Style style) { replaceActivity(std::make_unique(renderer, mappedInput, std::move(message), style)); } void ActivityManager::goToWeather() { replaceActivity(std::make_unique(renderer, mappedInput)); } void ActivityManager::goHome(std::string focusBookPath, int focusSelectorIndex) { hasReturnHint = false; replaceActivity(std::make_unique(renderer, mappedInput, std::move(focusBookPath), focusSelectorIndex)); } void ActivityManager::pushActivity(std::unique_ptr&& activity) { if (pendingActivity) { // Should never happen in practice LOG_ERR("ACT", "pendingActivity while pushActivity is not expected"); pendingActivity.reset(); } #if DEBUG_MEMORY_CONSUMPTION LOG_DBG("ACT", "pushActivity requested: current=%s stackSize=%zu", currentActivity ? currentActivity->getName().c_str() : "", stackActivities.size()); #endif pendingActivity = std::move(activity); pendingAction = PendingAction::Push; } void ActivityManager::popActivity() { if (pendingActivity) { // Should never happen in practice LOG_ERR("ACT", "pendingActivity while popActivity is not expected"); pendingActivity.reset(); } pendingAction = PendingAction::Pop; } bool ActivityManager::preventAutoSleep() const { return currentActivity && currentActivity->preventAutoSleep(); } bool ActivityManager::isReaderActivity() const { return currentActivity && currentActivity->isReaderActivity(); } bool ActivityManager::skipLoopDelay() const { return currentActivity && currentActivity->skipLoopDelay(); } void ActivityManager::dispatchButtonAction(const CrossPointSettings::BUTTON_ACTION action) { if (currentActivity && currentActivity->isReaderActivity()) { currentActivity->onButtonAction(action); } } void ActivityManager::requestUpdate(bool immediate) { if (immediate) { if (renderTaskHandle) { xTaskNotify(renderTaskHandle, 1, eIncrement); } } else { // Deferring the update until current loop is finished // This is to avoid multiple updates being requested in the same loop taskENTER_CRITICAL(nullptr); requestedUpdate = true; taskEXIT_CRITICAL(nullptr); } } void ActivityManager::requestUpdateAndWait() { if (!renderTaskHandle) { return; } // Atomic section to perform checks taskENTER_CRITICAL(nullptr); auto currTaskHandler = xTaskGetCurrentTaskHandle(); auto mutexHolder = xSemaphoreGetMutexHolder(renderingMutex); bool isRenderTask = (currTaskHandler == renderTaskHandle); bool alreadyWaiting = (waitingTaskHandle != nullptr); bool holdingRenderLock = (mutexHolder == currTaskHandler); if (!alreadyWaiting && !isRenderTask && !holdingRenderLock) { waitingTaskHandle = currTaskHandler; } taskEXIT_CRITICAL(nullptr); // Render task cannot call requestUpdateAndWait() or it will cause a deadlock assert(!isRenderTask && "Render task cannot call requestUpdateAndWait()"); // There should never be the case where 2 tasks are waiting for a render at the same time assert(!alreadyWaiting && "Already waiting for a render to complete"); // Cannot call while holding RenderLock or it will cause a deadlock assert(!holdingRenderLock && "Cannot call requestUpdateAndWait() while holding RenderLock"); xTaskNotify(renderTaskHandle, 1, eIncrement); ulTaskNotifyTake(pdTRUE, portMAX_DELAY); } // RenderLock RenderLock::RenderLock() { xSemaphoreTake(activityManager.renderingMutex, portMAX_DELAY); isLocked = true; } RenderLock::RenderLock([[maybe_unused]] Activity&) { xSemaphoreTake(activityManager.renderingMutex, portMAX_DELAY); isLocked = true; } RenderLock::~RenderLock() { if (isLocked) { xSemaphoreGive(activityManager.renderingMutex); isLocked = false; } } void RenderLock::unlock() { if (isLocked) { xSemaphoreGive(activityManager.renderingMutex); isLocked = false; } } /** * * Checks if renderingMutex is busy. * * @return true if renderingMutex is busy, otherwise false. * */ bool RenderLock::peek() { return xQueuePeek(activityManager.renderingMutex, NULL, 0) != pdTRUE; };