A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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>

authored by

danoob
danoooob
Dave Allie
and committed by
GitHub
fb0af32e cb4d86fe

+88 -91
+70 -8
src/activities/reader/EpubReaderActivity.cpp
··· 9 9 #include "CrossPointState.h" 10 10 #include "EpubReaderChapterSelectionActivity.h" 11 11 #include "EpubReaderPercentSelectionActivity.h" 12 + #include "KOReaderCredentialStore.h" 13 + #include "KOReaderSyncActivity.h" 12 14 #include "MappedInputManager.h" 13 15 #include "RecentBooksStore.h" 14 16 #include "components/UITheme.h" ··· 140 142 // Pass input responsibility to sub activity if exists 141 143 if (subActivity) { 142 144 subActivity->loop(); 145 + // Deferred exit: process after subActivity->loop() returns to avoid use-after-free 146 + if (pendingSubactivityExit) { 147 + pendingSubactivityExit = false; 148 + exitActivity(); 149 + updateRequired = true; 150 + skipNextButtonCheck = true; // Skip button processing to ignore stale events 151 + } 152 + // Deferred go home: process after subActivity->loop() returns to avoid race condition 153 + if (pendingGoHome) { 154 + pendingGoHome = false; 155 + exitActivity(); 156 + if (onGoHome) { 157 + onGoHome(); 158 + } 159 + return; // Don't access 'this' after callback 160 + } 161 + return; 162 + } 163 + 164 + // Handle pending go home when no subactivity (e.g., from long press back) 165 + if (pendingGoHome) { 166 + pendingGoHome = false; 167 + if (onGoHome) { 168 + onGoHome(); 169 + } 170 + return; // Don't access 'this' after callback 171 + } 172 + 173 + // Skip button processing after returning from subactivity 174 + // This prevents stale button release events from triggering actions 175 + // We wait until: (1) all relevant buttons are released, AND (2) wasReleased events have been cleared 176 + if (skipNextButtonCheck) { 177 + const bool confirmCleared = !mappedInput.isPressed(MappedInputManager::Button::Confirm) && 178 + !mappedInput.wasReleased(MappedInputManager::Button::Confirm); 179 + const bool backCleared = !mappedInput.isPressed(MappedInputManager::Button::Back) && 180 + !mappedInput.wasReleased(MappedInputManager::Button::Back); 181 + if (confirmCleared && backCleared) { 182 + skipNextButtonCheck = false; 183 + } 143 184 return; 144 185 } 145 186 ··· 387 428 break; 388 429 } 389 430 case EpubReaderMenuActivity::MenuAction::GO_HOME: { 390 - // 2. Trigger the reader's "Go Home" callback 391 - if (onGoHome) { 392 - onGoHome(); 393 - } 394 - 431 + // Defer go home to avoid race condition with display task 432 + pendingGoHome = true; 395 433 break; 396 434 } 397 435 case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: { ··· 412 450 413 451 saveProgress(backupSpine, backupPage, backupPageCount); 414 452 } 415 - exitActivity(); 416 - updateRequired = true; 417 453 xSemaphoreGive(renderingMutex); 418 - if (onGoHome) onGoHome(); 454 + // Defer go home to avoid race condition with display task 455 + pendingGoHome = true; 456 + break; 457 + } 458 + case EpubReaderMenuActivity::MenuAction::SYNC: { 459 + if (KOREADER_STORE.hasCredentials()) { 460 + xSemaphoreTake(renderingMutex, portMAX_DELAY); 461 + const int currentPage = section ? section->currentPage : 0; 462 + const int totalPages = section ? section->pageCount : 0; 463 + exitActivity(); 464 + enterNewActivity(new KOReaderSyncActivity( 465 + renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, currentPage, totalPages, 466 + [this]() { 467 + // On cancel - defer exit to avoid use-after-free 468 + pendingSubactivityExit = true; 469 + }, 470 + [this](int newSpineIndex, int newPage) { 471 + // On sync complete - update position and defer exit 472 + if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) { 473 + currentSpineIndex = newSpineIndex; 474 + nextPageNumber = newPage; 475 + section.reset(); 476 + } 477 + pendingSubactivityExit = true; 478 + })); 479 + xSemaphoreGive(renderingMutex); 480 + } 419 481 break; 420 482 } 421 483 }
+3
src/activities/reader/EpubReaderActivity.h
··· 24 24 // Normalized 0.0-1.0 progress within the target spine item, computed from book percentage. 25 25 float pendingSpineProgress = 0.0f; 26 26 bool updateRequired = false; 27 + bool pendingSubactivityExit = false; // Defer subactivity exit to avoid use-after-free 28 + bool pendingGoHome = false; // Defer go home to avoid race condition with display task 29 + bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit 27 30 const std::function<void()> onGoBack; 28 31 const std::function<void()> onGoHome; 29 32
+8 -65
src/activities/reader/EpubReaderChapterSelectionActivity.cpp
··· 2 2 3 3 #include <GfxRenderer.h> 4 4 5 - #include <algorithm> 6 - 7 - #include "KOReaderCredentialStore.h" 8 - #include "KOReaderSyncActivity.h" 9 5 #include "MappedInputManager.h" 10 6 #include "components/UITheme.h" 11 7 #include "fontIds.h" ··· 15 11 constexpr int SKIP_PAGE_MS = 700; 16 12 } // namespace 17 13 18 - bool EpubReaderChapterSelectionActivity::hasSyncOption() const { return KOREADER_STORE.hasCredentials(); } 19 - 20 - int EpubReaderChapterSelectionActivity::getTotalItems() const { 21 - // Add 2 for sync options (top and bottom) if credentials are configured 22 - const int syncCount = hasSyncOption() ? 2 : 0; 23 - return epub->getTocItemsCount() + syncCount; 24 - } 25 - 26 - bool EpubReaderChapterSelectionActivity::isSyncItem(int index) const { 27 - if (!hasSyncOption()) return false; 28 - // First item and last item are sync options 29 - return index == 0 || index == getTotalItems() - 1; 30 - } 31 - 32 - int EpubReaderChapterSelectionActivity::tocIndexFromItemIndex(int itemIndex) const { 33 - // Account for the sync option at the top 34 - const int offset = hasSyncOption() ? 1 : 0; 35 - return itemIndex - offset; 36 - } 14 + int EpubReaderChapterSelectionActivity::getTotalItems() const { return epub->getTocItemsCount(); } 37 15 38 16 int EpubReaderChapterSelectionActivity::getPageItems() const { 39 17 // Layout constants used in renderScreen ··· 65 43 66 44 renderingMutex = xSemaphoreCreateMutex(); 67 45 68 - // Account for sync option offset when finding current TOC index 69 - const int syncOffset = hasSyncOption() ? 1 : 0; 70 46 selectorIndex = epub->getTocIndexForSpineIndex(currentSpineIndex); 71 47 if (selectorIndex == -1) { 72 48 selectorIndex = 0; 73 49 } 74 - selectorIndex += syncOffset; // Offset for top sync option 75 50 76 51 // Trigger first update 77 52 updateRequired = true; ··· 96 71 renderingMutex = nullptr; 97 72 } 98 73 99 - void EpubReaderChapterSelectionActivity::launchSyncActivity() { 100 - xSemaphoreTake(renderingMutex, portMAX_DELAY); 101 - exitActivity(); 102 - enterNewActivity(new KOReaderSyncActivity( 103 - renderer, mappedInput, epub, epubPath, currentSpineIndex, currentPage, totalPagesInSpine, 104 - [this]() { 105 - // On cancel 106 - exitActivity(); 107 - updateRequired = true; 108 - }, 109 - [this](int newSpineIndex, int newPage) { 110 - // On sync complete 111 - exitActivity(); 112 - onSyncPosition(newSpineIndex, newPage); 113 - })); 114 - xSemaphoreGive(renderingMutex); 115 - } 116 - 117 74 void EpubReaderChapterSelectionActivity::loop() { 118 75 if (subActivity) { 119 76 subActivity->loop(); ··· 130 87 const int totalItems = getTotalItems(); 131 88 132 89 if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { 133 - // Check if sync option is selected (first or last item) 134 - if (isSyncItem(selectorIndex)) { 135 - launchSyncActivity(); 136 - return; 137 - } 138 - 139 - // Get TOC index (account for top sync offset) 140 - const int tocIndex = tocIndexFromItemIndex(selectorIndex); 141 - const auto newSpineIndex = epub->getSpineIndexForTocIndex(tocIndex); 90 + const auto newSpineIndex = epub->getSpineIndexForTocIndex(selectorIndex); 142 91 if (newSpineIndex == -1) { 143 92 onGoBack(); 144 93 } else { ··· 209 158 const int displayY = 60 + contentY + i * 30; 210 159 const bool isSelected = (itemIndex == selectorIndex); 211 160 212 - if (isSyncItem(itemIndex)) { 213 - // Sync option uses a fixed label and stays aligned to the content margin. 214 - renderer.drawText(UI_10_FONT_ID, contentX + 20, displayY, ">> Sync Progress", !isSelected); 215 - } else { 216 - const int tocIndex = tocIndexFromItemIndex(itemIndex); 217 - auto item = epub->getTocItem(tocIndex); 161 + auto item = epub->getTocItem(itemIndex); 218 162 219 - // Indent per TOC level while keeping content within the gutter-safe region. 220 - const int indentSize = contentX + 20 + (item.level - 1) * 15; 221 - const std::string chapterName = 222 - renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), contentWidth - 40 - indentSize); 163 + // Indent per TOC level while keeping content within the gutter-safe region. 164 + const int indentSize = contentX + 20 + (item.level - 1) * 15; 165 + const std::string chapterName = 166 + renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), contentWidth - 40 - indentSize); 223 167 224 - renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected); 225 - } 168 + renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected); 226 169 } 227 170 228 171 const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down");
+1 -11
src/activities/reader/EpubReaderChapterSelectionActivity.h
··· 26 26 // This adapts automatically when switching between portrait and landscape. 27 27 int getPageItems() const; 28 28 29 - // Total items including sync options (top and bottom) 29 + // Total TOC items count 30 30 int getTotalItems() const; 31 31 32 - // Check if sync option is available (credentials configured) 33 - bool hasSyncOption() const; 34 - 35 - // Check if given item index is a sync option (first or last) 36 - bool isSyncItem(int index) const; 37 - 38 - // Convert item index to TOC index (accounting for top sync option offset) 39 - int tocIndexFromItemIndex(int itemIndex) const; 40 - 41 32 static void taskTrampoline(void* param); 42 33 [[noreturn]] void displayTaskLoop(); 43 34 void renderScreen(); 44 - void launchSyncActivity(); 45 35 46 36 public: 47 37 explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
+1
src/activities/reader/EpubReaderMenuActivity.cpp
··· 2 2 3 3 #include <GfxRenderer.h> 4 4 5 + #include "MappedInputManager.h" 5 6 #include "components/UITheme.h" 6 7 #include "fontIds.h" 7 8
+5 -7
src/activities/reader/EpubReaderMenuActivity.h
··· 9 9 #include <vector> 10 10 11 11 #include "../ActivityWithSubactivity.h" 12 - #include "MappedInputManager.h" 13 12 14 13 class EpubReaderMenuActivity final : public ActivityWithSubactivity { 15 14 public: 16 15 // Menu actions available from the reader menu. 17 - enum class MenuAction { SELECT_CHAPTER, GO_TO_PERCENT, ROTATE_SCREEN, GO_HOME, DELETE_CACHE }; 16 + enum class MenuAction { SELECT_CHAPTER, GO_TO_PERCENT, ROTATE_SCREEN, GO_HOME, SYNC, DELETE_CACHE }; 18 17 19 18 explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, 20 19 const int currentPage, const int totalPages, const int bookProgressPercent, ··· 40 39 }; 41 40 42 41 // Fixed menu layout (order matters for up/down navigation). 43 - const std::vector<MenuItem> menuItems = {{MenuAction::SELECT_CHAPTER, "Go to Chapter"}, 44 - {MenuAction::ROTATE_SCREEN, "Reading Orientation"}, 45 - {MenuAction::GO_TO_PERCENT, "Go to %"}, 46 - {MenuAction::GO_HOME, "Go Home"}, 47 - {MenuAction::DELETE_CACHE, "Delete Book Cache"}}; 42 + const std::vector<MenuItem> menuItems = { 43 + {MenuAction::SELECT_CHAPTER, "Go to Chapter"}, {MenuAction::ROTATE_SCREEN, "Reading Orientation"}, 44 + {MenuAction::GO_TO_PERCENT, "Go to %"}, {MenuAction::GO_HOME, "Go Home"}, 45 + {MenuAction::SYNC, "Sync Progress"}, {MenuAction::DELETE_CACHE, "Delete Book Cache"}}; 48 46 49 47 int selectedIndex = 0; 50 48 bool updateRequired = false;