Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
17 kB · 457 lines
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458#include "ActivityManager.h"
#include <BoardConfig.h>#include <FontCacheManager.h>#include <FsHelpers.h>#include <HalDisplay.h>#include <HalPowerManager.h>#include <Memory.h>#include <VectorFontSupport.h>
#include <algorithm>
#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<ActivityManager*>(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<FrontlightPanelActivity>(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<Activity>&& 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<CrossPointWebServerActivity>(renderer, mappedInput));}
void ActivityManager::goToUsbDrive() {#if FREEINK_CAP_USB_MSC auto activity = makeUniqueNoThrow<UsbDriveActivity>(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<SettingsActivity>(renderer, mappedInput)); }
void ActivityManager::goToFileBrowser(std::string path) { replaceActivity(std::make_unique<FileBrowserActivity>(renderer, mappedInput, std::move(path)));}
void ActivityManager::goToLibrary() { auto activity = makeUniqueNoThrow<LibraryListActivity>(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<OpdsBookBrowserActivity>(renderer, mappedInput, servers[0])); } else { replaceActivity(std::make_unique<OpdsServerListActivity>(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<BmpViewerActivity>(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<SleepActivity>(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<BootActivity>(renderer, mappedInput)); }
void ActivityManager::goToFullScreenMessage(std::string message, EpdFontFamily::Style style) { replaceActivity(std::make_unique<FullScreenMessageActivity>(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<HomeActivity>(renderer, mappedInput, initialMenuItem, cleanInitialRefresh));}void ActivityManager::goToCrashReport() { replaceActivity(std::make_unique<CrashActivity>(renderer, mappedInput)); }
void ActivityManager::pushActivity(std::unique_ptr<Activity>&& 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; };