Files
crosspoint-reader-mod/src/activities/ActivityManager.cpp
cottongin 60a3e21c0e mod: Phase 3 — Re-port unmerged upstream PRs
Re-applied upstream PRs not yet merged to upstream/master:

- #1055: Byte-level framebuffer writes (fillPhysicalHSpan*,
  optimized fillRect/drawLine/fillRectDither/fillPolygon)
- #1027: Word-width cache (FNV-1a, 128-entry) and hyphenation
  early exit in ParsedText for 7-9% layout speedup
- #1068: Already present in upstream — URL hyphenation fix
- #1019: Already present in upstream — file extensions in browser
- #1090/#1185/#1217: KOReader sync improvements — binary credential
  store, document hash caching, ChapterXPathIndexer integration
- #1209: OPDS multi-server — OpdsBookBrowserActivity accepts
  OpdsServer, directory picker for downloads, download-complete
  prompt with open/back options
- #857: Dictionary activities already ported in Phase 1/2
- #1003: Placeholder cover already integrated in Phase 2

Also fixed: STR_OFF i18n string, include paths, replaced
Epub::isValidThumbnailBmp with Storage.exists, replaced
StringUtils::checkFileExtension with FsHelpers equivalents.

Made-with: Cursor
2026-03-07 16:15:42 -05:00

305 lines
10 KiB
C++

#include "ActivityManager.h"
#include <HalPowerManager.h>
#include "boot_sleep/BootActivity.h"
#include "boot_sleep/SleepActivity.h"
#include "browser/OpdsBookBrowserActivity.h"
#include "home/FileBrowserActivity.h"
#include "home/HomeActivity.h"
#include "home/RecentBooksActivity.h"
#include "network/CrossPointWebServerActivity.h"
#include "reader/ReaderActivity.h"
#include "settings/SettingsActivity.h"
#include "util/FullScreenMessageActivity.h"
void ActivityManager::begin() {
xTaskCreate(&renderTaskTrampoline, "ActivityManagerRender",
8192, // Stack size
this, // Parameters
1, // Priority
&renderTaskHandle // Task handle
);
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
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() {
if (currentActivity) {
// 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));
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) {
requestedUpdate = 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();
}
}
void ActivityManager::replaceActivity(std::unique_ptr<Activity>&& 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
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::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::goToRecentBooks() {
replaceActivity(std::make_unique<RecentBooksActivity>(renderer, mappedInput));
}
void ActivityManager::goToBrowser() {
replaceActivity(std::make_unique<OpdsBookBrowserActivity>(renderer, mappedInput));
}
void ActivityManager::goToBrowser(const OpdsServer& server) {
replaceActivity(std::make_unique<OpdsBookBrowserActivity>(renderer, mappedInput, &server));
}
void ActivityManager::goToReader(std::string path) {
replaceActivity(std::make_unique<ReaderActivity>(renderer, mappedInput, std::move(path)));
}
void ActivityManager::goToSleep() {
replaceActivity(std::make_unique<SleepActivity>(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<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() { replaceActivity(std::make_unique<HomeActivity>(renderer, mappedInput)); }
void ActivityManager::pushActivity(std::unique_ptr<Activity>&& activity) {
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() {
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::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(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; };