feat: Move Sync feature to menu (#680)
## Summary * **What is the goal of this PR?** Move the "Sync Progress" option from TOC (Chapter Selection) screen to the Reader Menu, and fix use-after-free crashes related to callback handling in activity lifecycle. * **What changes are included?** - Added "Sync Progress" as a menu item in `EpubReaderMenuActivity` (now 4 items: Go to Chapter, Sync Progress, Go Home, Delete Book Cache) - Removed sync-related logic from `EpubReaderChapterSelectionActivity` - TOC now only displays chapters - Implemented `pendingGoHome` and `pendingSubactivityExit` flags in `EpubReaderActivity` to safely handle activity destruction - Fixed GO_HOME, DELETE_CACHE, and SYNC menu actions to use deferred callbacks avoiding use-after-free ## Additional Context * Root cause of crashes: callbacks like `onGoHome()` or `onCancel()` invoked from activity handlers could destroy the current activity while code was still executing, causing use-after-free and race conditions with FreeRTOS display task. * Solution: Deferred execution pattern - set flags and process them in `loop()` after all nested activity loops have safely returned. * Files changed: `EpubReaderMenuActivity.h`, `EpubReaderActivity.h/.cpp`, `EpubReaderChapterSelectionActivity.h/.cpp` --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_ Co-authored-by: danoooob <danoooob@example.com> Co-authored-by: Dave Allie <dave@daveallie.com>
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
#include "CrossPointState.h"
|
||||
#include "EpubReaderChapterSelectionActivity.h"
|
||||
#include "EpubReaderPercentSelectionActivity.h"
|
||||
#include "KOReaderCredentialStore.h"
|
||||
#include "KOReaderSyncActivity.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/UITheme.h"
|
||||
@@ -140,6 +142,45 @@ void EpubReaderActivity::loop() {
|
||||
// Pass input responsibility to sub activity if exists
|
||||
if (subActivity) {
|
||||
subActivity->loop();
|
||||
// Deferred exit: process after subActivity->loop() returns to avoid use-after-free
|
||||
if (pendingSubactivityExit) {
|
||||
pendingSubactivityExit = false;
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
skipNextButtonCheck = true; // Skip button processing to ignore stale events
|
||||
}
|
||||
// Deferred go home: process after subActivity->loop() returns to avoid race condition
|
||||
if (pendingGoHome) {
|
||||
pendingGoHome = false;
|
||||
exitActivity();
|
||||
if (onGoHome) {
|
||||
onGoHome();
|
||||
}
|
||||
return; // Don't access 'this' after callback
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle pending go home when no subactivity (e.g., from long press back)
|
||||
if (pendingGoHome) {
|
||||
pendingGoHome = false;
|
||||
if (onGoHome) {
|
||||
onGoHome();
|
||||
}
|
||||
return; // Don't access 'this' after callback
|
||||
}
|
||||
|
||||
// Skip button processing after returning from subactivity
|
||||
// This prevents stale button release events from triggering actions
|
||||
// We wait until: (1) all relevant buttons are released, AND (2) wasReleased events have been cleared
|
||||
if (skipNextButtonCheck) {
|
||||
const bool confirmCleared = !mappedInput.isPressed(MappedInputManager::Button::Confirm) &&
|
||||
!mappedInput.wasReleased(MappedInputManager::Button::Confirm);
|
||||
const bool backCleared = !mappedInput.isPressed(MappedInputManager::Button::Back) &&
|
||||
!mappedInput.wasReleased(MappedInputManager::Button::Back);
|
||||
if (confirmCleared && backCleared) {
|
||||
skipNextButtonCheck = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -387,11 +428,8 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
|
||||
// 2. Trigger the reader's "Go Home" callback
|
||||
if (onGoHome) {
|
||||
onGoHome();
|
||||
}
|
||||
|
||||
// Defer go home to avoid race condition with display task
|
||||
pendingGoHome = true;
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: {
|
||||
@@ -412,10 +450,34 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
||||
|
||||
saveProgress(backupSpine, backupPage, backupPageCount);
|
||||
}
|
||||
exitActivity();
|
||||
updateRequired = true;
|
||||
xSemaphoreGive(renderingMutex);
|
||||
if (onGoHome) onGoHome();
|
||||
// Defer go home to avoid race condition with display task
|
||||
pendingGoHome = true;
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::SYNC: {
|
||||
if (KOREADER_STORE.hasCredentials()) {
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
const int currentPage = section ? section->currentPage : 0;
|
||||
const int totalPages = section ? section->pageCount : 0;
|
||||
exitActivity();
|
||||
enterNewActivity(new KOReaderSyncActivity(
|
||||
renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, currentPage, totalPages,
|
||||
[this]() {
|
||||
// On cancel - defer exit to avoid use-after-free
|
||||
pendingSubactivityExit = true;
|
||||
},
|
||||
[this](int newSpineIndex, int newPage) {
|
||||
// On sync complete - update position and defer exit
|
||||
if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) {
|
||||
currentSpineIndex = newSpineIndex;
|
||||
nextPageNumber = newPage;
|
||||
section.reset();
|
||||
}
|
||||
pendingSubactivityExit = true;
|
||||
}));
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user