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
305 lines
10 KiB
C++
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; };
|