#include "ActivityManager.h" #include #include #include #include #include #include #include #include #include "CrossPointSettings.h" #include "OpdsServerStore.h" #include "boot_sleep/BootActivity.h" #include "boot_sleep/SleepActivity.h" #include "browser/OpdsBookBrowserActivity.h" #include "components/HeaderBackTapTarget.h" #include "home/CrashActivity.h" #include "home/FileBrowserActivity.h" #include "home/HomeActivity.h" #include "library/LibraryListActivity.h" #include "network/CrossPointWebServerActivity.h" #include "network/UsbDriveActivity.h" #include "reader/ReaderActivity.h" #include "settings/OpdsServerListActivity.h" #include "settings/SettingsActivity.h" #include "util/BmpViewerActivity.h" #include "util/FrontlightPanelActivity.h" #include "util/FullScreenMessageActivity.h" static portMUX_TYPE activityManagerSpinlock = portMUX_INITIALIZER_UNLOCKED; void ActivityManager::begin() { #if defined(configNUM_CORES) && configNUM_CORES > 1 constexpr BaseType_t renderTaskCore = 1; #else constexpr BaseType_t renderTaskCore = 0; #endif #if CROSSPOINT_VECTOR_FONTS // FreeType rasterization runs on this task, and the deepest observed chain // is a glyph fault DURING LAYOUT: expat + parser + line-layout frames // (~3.5KB on Xtensa) with the scan converter's FT_RENDER_POOL_SIZE (4KB) // stack-resident band pool on top — a measured ~8KB peak that trips the // canary on an 8KB stack. Vector-font boards all have PSRAM-class RAM. constexpr uint32_t renderTaskStackBytes = 16384; #else constexpr uint32_t renderTaskStackBytes = 8192; #endif xTaskCreatePinnedToCore(&renderTaskTrampoline, "ActivityManagerRender", renderTaskStackBytes, // Stack size this, // Parameters 1, // Priority &renderTaskHandle, // Task handle renderTaskCore // Keep long renders/cover decodes off CPU 0's idle watchdog when available ); assert(renderTaskHandle != nullptr && "Failed to create render task"); } 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 // Night mode is a global output polarity applied to every activity. // The sleep screen forces normal polarity itself (SleepActivity). display.setInverted(SETTINGS.screenInverted != 0); currentActivity->render(std::move(lock)); } // Notify any task blocked in requestUpdateAndWait() that the render is done. TaskHandle_t waiter = nullptr; taskENTER_CRITICAL(&activityManagerSpinlock); waiter = waitingTaskHandle; waitingTaskHandle = nullptr; taskEXIT_CRITICAL(&activityManagerSpinlock); if (waiter) { xTaskNotify(waiter, 1, eIncrement); } } } void ActivityManager::loop() { if (mappedInput.consumeSuppressedRelease()) return; if (currentActivity && currentActivity->requiresExclusiveStorageLoop()) { currentActivity->loop(); // An exclusive-storage activity must restart rather than navigate away: // processing a pending action here could re-enable filesystem users while // the USB host still owns the raw SD card. if (requestedUpdate.exchange(false) && renderTaskHandle) { xTaskNotify(renderTaskHandle, 1, eIncrement); } return; } if (currentActivity) { if (!currentActivity->isHomeActivity() && mappedInput.wasHomeGesture()) { if (currentActivity->handleHomeGesture()) { return; } goHome(); return; } // Tap-first control-center entry: a tap on the status-bar band of the // top-level tab screens opens it, mirroring the top-edge swipe (which some // panels' etched glass makes unreliable). The reader keeps its clean page // (no status bar there to tap). Touch boards only, like the swipe itself. bool statusBarTap = false; if (mappedInput.hasTouch() && (currentActivity->name == "Home" || currentActivity->name == "FileBrowser" || currentActivity->name == "Settings" || currentActivity->name == "NetworkModeSelection")) { int tx = 0; int ty = 0; // The header back button shares this band; its taps stay Back. statusBarTap = mappedInput.wasScreenTapped(tx, ty) && ty < 44 && !HeaderBackTapTarget::contains(tx, ty); } if (currentActivity->name != "FrontlightPanel" && (statusBarTap || mappedInput.wasLightPanelGesture())) { pushActivity(std::make_unique(renderer, mappedInput)); return; } // Note: do not hold a lock here, the loop() method must be responsible for acquire one if needed currentActivity->loop(); } 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, going home"); lock.unlock(); // goHome may acquire its own lock goHome(); continue; // Will launch goHome 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); } // 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) { // Destroy the current activity exitActivity(lock); // Clear the stack while (!stackActivities.empty()) { stackActivities.back()->onExit(); stackActivities.pop_back(); } } else if (pendingAction == PendingAction::Push) { // Move current activity to stack stackActivities.push_back(std::move(currentActivity)); // The parent's header back rect must not route taps on the pushed // screen (which may draw no header of its own). HeaderBackTapTarget::clear(); LOG_DBG("ACT", "Pushed to activity stack, new size = %zu", stackActivities.size()); } pendingAction = PendingAction::None; currentActivity = std::move(pendingActivity); lock.unlock(); // onEnter may acquire its own lock currentActivity->onEnter(); // onEnter may request another pending action, we will handle it in the next loop iteration continue; } } if (requestedUpdate.exchange(false)) { // 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(); } // The outgoing screen's header back button must not eat taps on the next // screen; the next header draw re-records it. HeaderBackTapTarget::clear(); } void ActivityManager::replaceActivity(std::unique_ptr&& newActivity) { mappedInput.resetHomeButtonInput(); // 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 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::goToUsbDrive() { #if FREEINK_CAP_USB_MSC auto activity = makeUniqueNoThrow(renderer, mappedInput); if (!activity) { LOG_ERR("ACT", "OOM: USB Drive activity"); return; } replaceActivity(std::move(activity)); #else LOG_ERR("ACT", "USB Drive requested in a build without USB Drive capability"); #endif } void ActivityManager::goToSettings() { replaceActivity(std::make_unique(renderer, mappedInput)); } void ActivityManager::goToFileBrowser(std::string path) { replaceActivity(std::make_unique(renderer, mappedInput, std::move(path))); } void ActivityManager::goToLibrary() { auto activity = makeUniqueNoThrow(renderer, mappedInput); if (!activity) { LOG_ERR("ACT", "OOM: library activity"); return; } replaceActivity(std::move(activity)); } 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, const bool allowFastInitialRefresh) { if (path.empty()) { goToFileBrowser("/"); return; } if (FsHelpers::hasBmpExtension(path) || FsHelpers::hasPngExtension(path)) { auto activity = makeUniqueNoThrow(renderer, mappedInput, std::move(path)); if (!activity) { LOG_ERR("ACT", "OOM: bitmap viewer activity"); return; } replaceActivity(std::move(activity)); return; } auto activity = ReaderActivity::create(renderer, mappedInput, std::move(path), allowFastInitialRefresh); if (activity) { replaceActivity(std::move(activity)); } } void ActivityManager::goToSleep(bool fromTimeout) { replaceActivity(std::make_unique(renderer, mappedInput, fromTimeout)); 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::goHome(HomeMenuItem initialMenuItem, bool cleanInitialRefresh) { if (initialMenuItem == HomeMenuItem::NONE && currentActivity) { const auto& activityName = currentActivity->name; if (activityName == "FileBrowser") { initialMenuItem = HomeMenuItem::FILE_BROWSER; } else if (activityName == "Library") { initialMenuItem = HomeMenuItem::LIBRARY; } else if (activityName == "OpdsBookBrowser") { initialMenuItem = HomeMenuItem::OPDS_BROWSER; } else if (activityName == "CrossPointWebServer") { initialMenuItem = HomeMenuItem::FILE_TRANSFER; } else if (activityName == "Settings") { initialMenuItem = HomeMenuItem::SETTINGS_MENU; } } replaceActivity(std::make_unique(renderer, mappedInput, initialMenuItem, cleanInitialRefresh)); } void ActivityManager::goToCrashReport() { replaceActivity(std::make_unique(renderer, mappedInput)); } void ActivityManager::pushActivity(std::unique_ptr&& activity) { mappedInput.resetHomeButtonInput(); if (pendingActivity) { // Should never happen in practice LOG_ERR("ACT", "pendingActivity while pushActivity is not expected"); pendingActivity.reset(); } pendingActivity = std::move(activity); pendingAction = PendingAction::Push; } void ActivityManager::popActivity() { mappedInput.resetHomeButtonInput(); 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::requiresExclusiveStorageLoop() const { return currentActivity && currentActivity->requiresExclusiveStorageLoop(); } bool ActivityManager::isReaderActivity() const { return std::any_of(stackActivities.begin(), stackActivities.end(), [](const auto& activity) { return activity->isReaderActivity(); }) || (currentActivity && currentActivity->isReaderActivity()); } bool ActivityManager::handleForcedRefresh() { return currentActivity && currentActivity->handleForcedRefresh(); } bool ActivityManager::skipLoopDelay() const { return currentActivity && currentActivity->skipLoopDelay(); } ScreenshotInfo ActivityManager::getScreenshotInfo() const { if (currentActivity) { return currentActivity->getScreenshotInfo(); } return {}; } 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 requestedUpdate = true; } } void ActivityManager::requestUpdateAndWait() { if (!renderTaskHandle) { return; } // Atomic section to perform checks taskENTER_CRITICAL(&activityManagerSpinlock); 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(&activityManagerSpinlock); // 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(Mode mode) { isLocked = xSemaphoreTake(activityManager.renderingMutex, mode == Mode::Try ? 0 : portMAX_DELAY) == pdTRUE; assert((mode == Mode::Try || isLocked) && "Blocking render lock acquisition failed"); } RenderLock::RenderLock(Activity&) : RenderLock(Mode::Blocking) {} 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; };