mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-06 23:57:39 +03:00
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:
parent
cb4d86fec6
commit
fb0af32ec0
@ -9,6 +9,8 @@
|
|||||||
#include "CrossPointState.h"
|
#include "CrossPointState.h"
|
||||||
#include "EpubReaderChapterSelectionActivity.h"
|
#include "EpubReaderChapterSelectionActivity.h"
|
||||||
#include "EpubReaderPercentSelectionActivity.h"
|
#include "EpubReaderPercentSelectionActivity.h"
|
||||||
|
#include "KOReaderCredentialStore.h"
|
||||||
|
#include "KOReaderSyncActivity.h"
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "RecentBooksStore.h"
|
#include "RecentBooksStore.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
@ -140,6 +142,45 @@ void EpubReaderActivity::loop() {
|
|||||||
// Pass input responsibility to sub activity if exists
|
// Pass input responsibility to sub activity if exists
|
||||||
if (subActivity) {
|
if (subActivity) {
|
||||||
subActivity->loop();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -387,11 +428,8 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
|
case EpubReaderMenuActivity::MenuAction::GO_HOME: {
|
||||||
// 2. Trigger the reader's "Go Home" callback
|
// Defer go home to avoid race condition with display task
|
||||||
if (onGoHome) {
|
pendingGoHome = true;
|
||||||
onGoHome();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: {
|
case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: {
|
||||||
@ -412,10 +450,34 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
|||||||
|
|
||||||
saveProgress(backupSpine, backupPage, backupPageCount);
|
saveProgress(backupSpine, backupPage, backupPageCount);
|
||||||
}
|
}
|
||||||
exitActivity();
|
|
||||||
updateRequired = true;
|
|
||||||
xSemaphoreGive(renderingMutex);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,9 @@ class EpubReaderActivity final : public ActivityWithSubactivity {
|
|||||||
// Normalized 0.0-1.0 progress within the target spine item, computed from book percentage.
|
// Normalized 0.0-1.0 progress within the target spine item, computed from book percentage.
|
||||||
float pendingSpineProgress = 0.0f;
|
float pendingSpineProgress = 0.0f;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
bool pendingSubactivityExit = false; // Defer subactivity exit to avoid use-after-free
|
||||||
|
bool pendingGoHome = false; // Defer go home to avoid race condition with display task
|
||||||
|
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
|
||||||
const std::function<void()> onGoBack;
|
const std::function<void()> onGoBack;
|
||||||
const std::function<void()> onGoHome;
|
const std::function<void()> onGoHome;
|
||||||
|
|
||||||
|
|||||||
@ -2,10 +2,6 @@
|
|||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
|
|
||||||
#include "KOReaderCredentialStore.h"
|
|
||||||
#include "KOReaderSyncActivity.h"
|
|
||||||
#include "MappedInputManager.h"
|
#include "MappedInputManager.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
@ -15,25 +11,7 @@ namespace {
|
|||||||
constexpr int SKIP_PAGE_MS = 700;
|
constexpr int SKIP_PAGE_MS = 700;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
bool EpubReaderChapterSelectionActivity::hasSyncOption() const { return KOREADER_STORE.hasCredentials(); }
|
int EpubReaderChapterSelectionActivity::getTotalItems() const { return epub->getTocItemsCount(); }
|
||||||
|
|
||||||
int EpubReaderChapterSelectionActivity::getTotalItems() const {
|
|
||||||
// Add 2 for sync options (top and bottom) if credentials are configured
|
|
||||||
const int syncCount = hasSyncOption() ? 2 : 0;
|
|
||||||
return epub->getTocItemsCount() + syncCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool EpubReaderChapterSelectionActivity::isSyncItem(int index) const {
|
|
||||||
if (!hasSyncOption()) return false;
|
|
||||||
// First item and last item are sync options
|
|
||||||
return index == 0 || index == getTotalItems() - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int EpubReaderChapterSelectionActivity::tocIndexFromItemIndex(int itemIndex) const {
|
|
||||||
// Account for the sync option at the top
|
|
||||||
const int offset = hasSyncOption() ? 1 : 0;
|
|
||||||
return itemIndex - offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
int EpubReaderChapterSelectionActivity::getPageItems() const {
|
int EpubReaderChapterSelectionActivity::getPageItems() const {
|
||||||
// Layout constants used in renderScreen
|
// Layout constants used in renderScreen
|
||||||
@ -65,13 +43,10 @@ void EpubReaderChapterSelectionActivity::onEnter() {
|
|||||||
|
|
||||||
renderingMutex = xSemaphoreCreateMutex();
|
renderingMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
// Account for sync option offset when finding current TOC index
|
|
||||||
const int syncOffset = hasSyncOption() ? 1 : 0;
|
|
||||||
selectorIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
|
selectorIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
|
||||||
if (selectorIndex == -1) {
|
if (selectorIndex == -1) {
|
||||||
selectorIndex = 0;
|
selectorIndex = 0;
|
||||||
}
|
}
|
||||||
selectorIndex += syncOffset; // Offset for top sync option
|
|
||||||
|
|
||||||
// Trigger first update
|
// Trigger first update
|
||||||
updateRequired = true;
|
updateRequired = true;
|
||||||
@ -96,24 +71,6 @@ void EpubReaderChapterSelectionActivity::onExit() {
|
|||||||
renderingMutex = nullptr;
|
renderingMutex = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderChapterSelectionActivity::launchSyncActivity() {
|
|
||||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
|
||||||
exitActivity();
|
|
||||||
enterNewActivity(new KOReaderSyncActivity(
|
|
||||||
renderer, mappedInput, epub, epubPath, currentSpineIndex, currentPage, totalPagesInSpine,
|
|
||||||
[this]() {
|
|
||||||
// On cancel
|
|
||||||
exitActivity();
|
|
||||||
updateRequired = true;
|
|
||||||
},
|
|
||||||
[this](int newSpineIndex, int newPage) {
|
|
||||||
// On sync complete
|
|
||||||
exitActivity();
|
|
||||||
onSyncPosition(newSpineIndex, newPage);
|
|
||||||
}));
|
|
||||||
xSemaphoreGive(renderingMutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
void EpubReaderChapterSelectionActivity::loop() {
|
void EpubReaderChapterSelectionActivity::loop() {
|
||||||
if (subActivity) {
|
if (subActivity) {
|
||||||
subActivity->loop();
|
subActivity->loop();
|
||||||
@ -130,15 +87,7 @@ void EpubReaderChapterSelectionActivity::loop() {
|
|||||||
const int totalItems = getTotalItems();
|
const int totalItems = getTotalItems();
|
||||||
|
|
||||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||||
// Check if sync option is selected (first or last item)
|
const auto newSpineIndex = epub->getSpineIndexForTocIndex(selectorIndex);
|
||||||
if (isSyncItem(selectorIndex)) {
|
|
||||||
launchSyncActivity();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get TOC index (account for top sync offset)
|
|
||||||
const int tocIndex = tocIndexFromItemIndex(selectorIndex);
|
|
||||||
const auto newSpineIndex = epub->getSpineIndexForTocIndex(tocIndex);
|
|
||||||
if (newSpineIndex == -1) {
|
if (newSpineIndex == -1) {
|
||||||
onGoBack();
|
onGoBack();
|
||||||
} else {
|
} else {
|
||||||
@ -209,20 +158,14 @@ void EpubReaderChapterSelectionActivity::renderScreen() {
|
|||||||
const int displayY = 60 + contentY + i * 30;
|
const int displayY = 60 + contentY + i * 30;
|
||||||
const bool isSelected = (itemIndex == selectorIndex);
|
const bool isSelected = (itemIndex == selectorIndex);
|
||||||
|
|
||||||
if (isSyncItem(itemIndex)) {
|
auto item = epub->getTocItem(itemIndex);
|
||||||
// Sync option uses a fixed label and stays aligned to the content margin.
|
|
||||||
renderer.drawText(UI_10_FONT_ID, contentX + 20, displayY, ">> Sync Progress", !isSelected);
|
|
||||||
} else {
|
|
||||||
const int tocIndex = tocIndexFromItemIndex(itemIndex);
|
|
||||||
auto item = epub->getTocItem(tocIndex);
|
|
||||||
|
|
||||||
// Indent per TOC level while keeping content within the gutter-safe region.
|
// Indent per TOC level while keeping content within the gutter-safe region.
|
||||||
const int indentSize = contentX + 20 + (item.level - 1) * 15;
|
const int indentSize = contentX + 20 + (item.level - 1) * 15;
|
||||||
const std::string chapterName =
|
const std::string chapterName =
|
||||||
renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), contentWidth - 40 - indentSize);
|
renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), contentWidth - 40 - indentSize);
|
||||||
|
|
||||||
renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected);
|
renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down");
|
const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down");
|
||||||
|
|||||||
@ -26,22 +26,12 @@ class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity
|
|||||||
// This adapts automatically when switching between portrait and landscape.
|
// This adapts automatically when switching between portrait and landscape.
|
||||||
int getPageItems() const;
|
int getPageItems() const;
|
||||||
|
|
||||||
// Total items including sync options (top and bottom)
|
// Total TOC items count
|
||||||
int getTotalItems() const;
|
int getTotalItems() const;
|
||||||
|
|
||||||
// Check if sync option is available (credentials configured)
|
|
||||||
bool hasSyncOption() const;
|
|
||||||
|
|
||||||
// Check if given item index is a sync option (first or last)
|
|
||||||
bool isSyncItem(int index) const;
|
|
||||||
|
|
||||||
// Convert item index to TOC index (accounting for top sync option offset)
|
|
||||||
int tocIndexFromItemIndex(int itemIndex) const;
|
|
||||||
|
|
||||||
static void taskTrampoline(void* param);
|
static void taskTrampoline(void* param);
|
||||||
[[noreturn]] void displayTaskLoop();
|
[[noreturn]] void displayTaskLoop();
|
||||||
void renderScreen();
|
void renderScreen();
|
||||||
void launchSyncActivity();
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
explicit EpubReaderChapterSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
|
||||||
|
#include "MappedInputManager.h"
|
||||||
#include "components/UITheme.h"
|
#include "components/UITheme.h"
|
||||||
#include "fontIds.h"
|
#include "fontIds.h"
|
||||||
|
|
||||||
|
|||||||
@ -9,12 +9,11 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "../ActivityWithSubactivity.h"
|
#include "../ActivityWithSubactivity.h"
|
||||||
#include "MappedInputManager.h"
|
|
||||||
|
|
||||||
class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
||||||
public:
|
public:
|
||||||
// Menu actions available from the reader menu.
|
// Menu actions available from the reader menu.
|
||||||
enum class MenuAction { SELECT_CHAPTER, GO_TO_PERCENT, ROTATE_SCREEN, GO_HOME, DELETE_CACHE };
|
enum class MenuAction { SELECT_CHAPTER, GO_TO_PERCENT, ROTATE_SCREEN, GO_HOME, SYNC, DELETE_CACHE };
|
||||||
|
|
||||||
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title,
|
||||||
const int currentPage, const int totalPages, const int bookProgressPercent,
|
const int currentPage, const int totalPages, const int bookProgressPercent,
|
||||||
@ -40,11 +39,10 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Fixed menu layout (order matters for up/down navigation).
|
// Fixed menu layout (order matters for up/down navigation).
|
||||||
const std::vector<MenuItem> menuItems = {{MenuAction::SELECT_CHAPTER, "Go to Chapter"},
|
const std::vector<MenuItem> menuItems = {
|
||||||
{MenuAction::ROTATE_SCREEN, "Reading Orientation"},
|
{MenuAction::SELECT_CHAPTER, "Go to Chapter"}, {MenuAction::ROTATE_SCREEN, "Reading Orientation"},
|
||||||
{MenuAction::GO_TO_PERCENT, "Go to %"},
|
{MenuAction::GO_TO_PERCENT, "Go to %"}, {MenuAction::GO_HOME, "Go Home"},
|
||||||
{MenuAction::GO_HOME, "Go Home"},
|
{MenuAction::SYNC, "Sync Progress"}, {MenuAction::DELETE_CACHE, "Delete Book Cache"}};
|
||||||
{MenuAction::DELETE_CACHE, "Delete Book Cache"}};
|
|
||||||
|
|
||||||
int selectedIndex = 0;
|
int selectedIndex = 0;
|
||||||
bool updateRequired = false;
|
bool updateRequired = false;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user