diff --git a/lib/KOReaderSync/KOReaderSyncClient.cpp b/lib/KOReaderSync/KOReaderSyncClient.cpp index b65d98ab..a32e0f40 100644 --- a/lib/KOReaderSync/KOReaderSyncClient.cpp +++ b/lib/KOReaderSync/KOReaderSyncClient.cpp @@ -32,6 +32,30 @@ esp_http_client_handle_t g_sessionClient = nullptr; // numbers, and HTTP status. Single-threaded sync flow makes static safe. char g_failureDetailBuf[160] = {0}; +std::string previewBody(const char* body, const size_t maxLen = 120) { + if (!body || !*body) { + return ""; + } + + std::string preview; + preview.reserve(maxLen); + for (const char* p = body; *p && preview.size() < maxLen; ++p) { + const unsigned char c = static_cast(*p); + if (c == '\r' || c == '\n' || c == '\t') { + preview.push_back(' '); + } else if (std::isprint(c)) { + preview.push_back(static_cast(c)); + } else { + preview.push_back('?'); + } + } + + if (strlen(body) > preview.size()) { + preview += "..."; + } + return preview; +} + // Reset the static diagnostic state at the start of each request and capture pre-flight // heap so failure reporting always reflects what was available when the request started. void beginRequest(const char* operation) { @@ -447,7 +471,12 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc esp_http_client_cleanup(client); } - LOG_DBG("KOSync", "Get progress response: %d (err: %s) [attempt %d]", httpCode, esp_err_to_name(err), attempt); + const size_t bodyLen = activeBuf->data ? strlen(activeBuf->data) : 0; + LOG_DBG("KOSync", "GET %s -> %d (err: %s) [attempt %d body_len=%u]", url.c_str(), httpCode, esp_err_to_name(err), + attempt, static_cast(bodyLen)); + if (err == ESP_OK && (httpCode < 200 || httpCode >= 300)) { + LOG_ERR("KOSync", "GET failure body preview: %s", previewBody(activeBuf->data).c_str()); + } // Retry exactly once for connect-level failures only. // Why: this recovers short AP/roaming hiccups without masking persistent @@ -497,7 +526,10 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc } if (httpCode == 401) return AUTH_FAILED; - if (httpCode == 404) return NOT_FOUND; + if (httpCode == 404) { + LOG_DBG("KOSync", "GET progress returned 404 for %s - treating as NOT_FOUND", url.c_str()); + return NOT_FOUND; + } return SERVER_ERROR; } @@ -561,7 +593,14 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr esp_http_client_cleanup(client); } - LOG_DBG("KOSync", "Update progress response: %d (err: %s) [attempt %d]", httpCode, esp_err_to_name(err), attempt); + const size_t bodyLen = activeBuf->data ? strlen(activeBuf->data) : 0; + LOG_DBG("KOSync", "PUT %s -> %d (err: %s) [attempt %d body_len=%u]", url.c_str(), httpCode, esp_err_to_name(err), + attempt, static_cast(bodyLen)); + if (err == ESP_OK && (httpCode < 200 || httpCode >= 300)) { + LOG_ERR("KOSync", "PUT failure body preview: %s", previewBody(activeBuf->data).c_str()); + LOG_ERR("KOSync", "PUT failure request summary: document=%s percentage=%.4f progress=%s", progress.document.c_str(), + progress.percentage, progress.progress.c_str()); + } // Retry exactly once for connect-level failures only. // Why: same policy as GET keeps behavior predictable across both endpoints. diff --git a/src/CrossPointState.cpp b/src/CrossPointState.cpp index 3d6c2b70..35bcd827 100644 --- a/src/CrossPointState.cpp +++ b/src/CrossPointState.cpp @@ -76,6 +76,8 @@ bool CrossPointState::loadFromBinaryFile() { lastSleepFromReader = false; } + koReaderSyncSession.clear(); + inputFile.close(); return true; } diff --git a/src/CrossPointState.h b/src/CrossPointState.h index 9e7795b9..bcf74e8a 100644 --- a/src/CrossPointState.h +++ b/src/CrossPointState.h @@ -3,6 +3,53 @@ #include #include +enum class KOReaderSyncIntentState : uint8_t { + COMPARE = 0, + PULL_REMOTE = 1, + PUSH_LOCAL = 2, +}; + +enum class KOReaderSyncOutcomeState : uint8_t { + NONE = 0, + PENDING = 1, + CANCELLED = 2, + FAILED = 3, + UPLOAD_COMPLETE = 4, + APPLIED_REMOTE = 5, +}; + +struct KOReaderSyncSessionState { + bool active = false; + std::string epubPath; + int spineIndex = 0; + int page = 0; + int totalPagesInSpine = 0; + uint16_t paragraphIndex = 0; + bool hasParagraphIndex = false; + KOReaderSyncIntentState intent = KOReaderSyncIntentState::COMPARE; + KOReaderSyncOutcomeState outcome = KOReaderSyncOutcomeState::NONE; + int resultSpineIndex = 0; + int resultPage = 0; + uint16_t resultParagraphIndex = 0; + bool resultHasParagraphIndex = false; + + void clear() { + active = false; + epubPath.clear(); + spineIndex = 0; + page = 0; + totalPagesInSpine = 0; + paragraphIndex = 0; + hasParagraphIndex = false; + intent = KOReaderSyncIntentState::COMPARE; + outcome = KOReaderSyncOutcomeState::NONE; + resultSpineIndex = 0; + resultPage = 0; + resultParagraphIndex = 0; + resultHasParagraphIndex = false; + } +}; + class CrossPointState { // Static instance static CrossPointState instance; @@ -12,6 +59,7 @@ class CrossPointState { size_t lastSleepImage = SIZE_MAX; // SIZE_MAX = unset sentinel uint8_t readerActivityLoadCount = 0; bool lastSleepFromReader = false; + KOReaderSyncSessionState koReaderSyncSession; ~CrossPointState() = default; // Get singleton instance diff --git a/src/JsonSettingsIO.cpp b/src/JsonSettingsIO.cpp index bfa0e011..ef1e8d49 100644 --- a/src/JsonSettingsIO.cpp +++ b/src/JsonSettingsIO.cpp @@ -73,6 +73,20 @@ bool JsonSettingsIO::saveState(const CrossPointState& s, const char* path) { doc["lastSleepImage"] = s.lastSleepImage; doc["readerActivityLoadCount"] = s.readerActivityLoadCount; doc["lastSleepFromReader"] = s.lastSleepFromReader; + JsonObject sync = doc["koReaderSyncSession"].to(); + sync["active"] = s.koReaderSyncSession.active; + sync["epubPath"] = s.koReaderSyncSession.epubPath; + sync["spineIndex"] = s.koReaderSyncSession.spineIndex; + sync["page"] = s.koReaderSyncSession.page; + sync["totalPagesInSpine"] = s.koReaderSyncSession.totalPagesInSpine; + sync["paragraphIndex"] = s.koReaderSyncSession.paragraphIndex; + sync["hasParagraphIndex"] = s.koReaderSyncSession.hasParagraphIndex; + sync["intent"] = static_cast(s.koReaderSyncSession.intent); + sync["outcome"] = static_cast(s.koReaderSyncSession.outcome); + sync["resultSpineIndex"] = s.koReaderSyncSession.resultSpineIndex; + sync["resultPage"] = s.koReaderSyncSession.resultPage; + sync["resultParagraphIndex"] = s.koReaderSyncSession.resultParagraphIndex; + sync["resultHasParagraphIndex"] = s.koReaderSyncSession.resultHasParagraphIndex; String json; serializeJson(doc, json); @@ -91,6 +105,22 @@ bool JsonSettingsIO::loadState(CrossPointState& s, const char* json) { s.lastSleepImage = doc["lastSleepImage"] | SIZE_MAX; s.readerActivityLoadCount = doc["readerActivityLoadCount"] | (uint8_t)0; s.lastSleepFromReader = doc["lastSleepFromReader"] | false; + JsonObject sync = doc["koReaderSyncSession"].as(); + s.koReaderSyncSession.active = sync["active"] | false; + s.koReaderSyncSession.epubPath = sync["epubPath"] | std::string(""); + s.koReaderSyncSession.spineIndex = sync["spineIndex"] | 0; + s.koReaderSyncSession.page = sync["page"] | 0; + s.koReaderSyncSession.totalPagesInSpine = sync["totalPagesInSpine"] | 0; + s.koReaderSyncSession.paragraphIndex = sync["paragraphIndex"] | (uint16_t)0; + s.koReaderSyncSession.hasParagraphIndex = sync["hasParagraphIndex"] | false; + s.koReaderSyncSession.intent = + static_cast(sync["intent"] | static_cast(KOReaderSyncIntentState::COMPARE)); + s.koReaderSyncSession.outcome = + static_cast(sync["outcome"] | static_cast(KOReaderSyncOutcomeState::NONE)); + s.koReaderSyncSession.resultSpineIndex = sync["resultSpineIndex"] | 0; + s.koReaderSyncSession.resultPage = sync["resultPage"] | 0; + s.koReaderSyncSession.resultParagraphIndex = sync["resultParagraphIndex"] | (uint16_t)0; + s.koReaderSyncSession.resultHasParagraphIndex = sync["resultHasParagraphIndex"] | false; return true; } diff --git a/src/activities/ActivityManager.cpp b/src/activities/ActivityManager.cpp index b17d4542..47b2d76f 100644 --- a/src/activities/ActivityManager.cpp +++ b/src/activities/ActivityManager.cpp @@ -4,6 +4,7 @@ #include #include +#include "CrossPointState.h" #include "boot_sleep/BootActivity.h" #include "boot_sleep/SleepActivity.h" #include "browser/OpdsBookBrowserActivity.h" @@ -11,6 +12,7 @@ #include "home/HomeActivity.h" #include "home/RecentBooksActivity.h" #include "network/CrossPointWebServerActivity.h" +#include "reader/KOReaderSyncActivity.h" #include "reader/ReaderActivity.h" #include "settings/SettingsActivity.h" #include "util/FullScreenMessageActivity.h" @@ -227,6 +229,19 @@ void ActivityManager::goToReader(std::string path) { replaceActivity(std::make_unique(renderer, mappedInput, std::move(path))); } +void ActivityManager::goToKOReaderSync() { + const auto& sync = APP_STATE.koReaderSyncSession; + if (!sync.active || sync.epubPath.empty()) { + LOG_ERR("ACT", "Cannot launch KOReader sync without an active EPUB handoff"); + goHome(); + return; + } + + replaceActivity(std::make_unique(renderer, mappedInput, sync.epubPath, sync.spineIndex, + sync.page, sync.totalPagesInSpine, sync.paragraphIndex, + sync.hasParagraphIndex, sync.intent)); +} + void ActivityManager::pushReader(std::string path) { pushActivity(std::make_unique(renderer, mappedInput, std::move(path))); } diff --git a/src/activities/ActivityManager.h b/src/activities/ActivityManager.h index 33de24b2..a907b8a9 100644 --- a/src/activities/ActivityManager.h +++ b/src/activities/ActivityManager.h @@ -89,6 +89,7 @@ class ActivityManager { void goToRecentBooks(); void goToBrowser(); void goToReader(std::string path); + void goToKOReaderSync(); void pushReader(std::string path); void goToSleep(); void goToBoot(); diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 0826f3ab..baf492f8 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -18,7 +19,6 @@ #include "EpubReaderFootnotesActivity.h" #include "EpubReaderPercentSelectionActivity.h" #include "KOReaderCredentialStore.h" -#include "KOReaderSyncActivity.h" #include "MappedInputManager.h" #include "QrDisplayActivity.h" #include "ReaderUtils.h" @@ -33,6 +33,31 @@ constexpr unsigned long skipChapterMs = 700; // pages per minute, first item is 1 to prevent division by zero if accessed const std::vector PAGE_TURN_LABELS = {1, 1, 3, 6, 12}; +void logReaderMemSnapshot(const char* stage) { + const uint32_t freeHeap = esp_get_free_heap_size(); + const uint32_t contigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT); + LOG_DBG("ERS", "Reader mem[%s]: free=%lu contig=%lu", stage, freeHeap, contigHeap); +} + +bool writeReaderProgressCache(const std::string& cachePath, const int spineIndex, const int currentPage, const int pageCount) { + FsFile f; + if (!Storage.openFileForWrite("ERS", cachePath + "/progress.bin", f)) { + LOG_ERR("ERS", "Failed to open progress cache for sync restore: %s", cachePath.c_str()); + return false; + } + + uint8_t data[6]; + data[0] = spineIndex & 0xFF; + data[1] = (spineIndex >> 8) & 0xFF; + data[2] = currentPage & 0xFF; + data[3] = (currentPage >> 8) & 0xFF; + data[4] = pageCount & 0xFF; + data[5] = (pageCount >> 8) & 0xFF; + f.write(data, 6); + f.close(); + return true; +} + int clampPercent(int percent) { if (percent < 0) { return 0; @@ -47,6 +72,7 @@ int clampPercent(int percent) { void EpubReaderActivity::onEnter() { Activity::onEnter(); + logReaderMemSnapshot("onEnter_begin"); // Drop any input events that arrived from the activity that launched us (e.g. a wake-up power // button hold) before they reach detectPageTurn() — see ReaderUtils::InputDrainGuard. @@ -64,6 +90,7 @@ void EpubReaderActivity::onEnter() { } epub->setupCacheDir(); + applyPendingSyncSession(); FsFile f; if (Storage.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) { @@ -104,10 +131,12 @@ void EpubReaderActivity::onEnter() { // Trigger first update requestUpdate(); + logReaderMemSnapshot("onEnter_ready"); } void EpubReaderActivity::onExit() { Activity::onExit(); + logReaderMemSnapshot("onExit_before_release"); // Reset orientation back to portrait for the rest of the UI renderer.setOrientation(GfxRenderer::Orientation::Portrait); @@ -116,6 +145,9 @@ void EpubReaderActivity::onExit() { APP_STATE.saveToFile(); section.reset(); epub.reset(); + currentPageFootnotes.clear(); + currentPageFootnotes.shrink_to_fit(); + logReaderMemSnapshot("onExit_after_release"); } void EpubReaderActivity::loop() { @@ -498,70 +530,77 @@ void EpubReaderActivity::launchKOReaderSync(const SyncLaunchMode mode) { return; } - const std::string syncEpubPath = epub->getPath(); const int currentPage = section ? section->currentPage : 0; const int totalPages = section ? section->pageCount : 0; - - { - // Drop large reader state before TLS-heavy sync to improve contiguous heap - // and reduce long-run fragmentation across repeated sync attempts. - RenderLock lock(*this); - nextPageNumber = currentPage; - cachedSpineIndex = currentSpineIndex; - cachedChapterTotalPageCount = totalPages; - section.reset(); - epub.reset(); - currentPageFootnotes.clear(); - currentPageFootnotes.shrink_to_fit(); - } - deferredSyncEpubPath = syncEpubPath; - - renderer.cleanupGrayscaleWithFrameBuffer(); - if (auto* cacheManager = renderer.getFontCacheManager()) { - cacheManager->clearCache(); - cacheManager->resetStats(); - } - - LOG_DBG("ERS", "Pre-sync trim: spine=%d page=%d/%d heap=%lu", currentSpineIndex, currentPage, totalPages, - static_cast(esp_get_free_heap_size())); - - // Map reader-level launch mode to activity-level intent once, then pass a - // stable intent into KOReaderSyncActivity so it can own the sync state machine. - KOReaderSyncActivity::SyncIntent syncIntent = KOReaderSyncActivity::SyncIntent::COMPARE; + KOReaderSyncIntentState syncIntent = KOReaderSyncIntentState::COMPARE; if (mode == SyncLaunchMode::PULL_REMOTE) { - syncIntent = KOReaderSyncActivity::SyncIntent::PULL_REMOTE; + syncIntent = KOReaderSyncIntentState::PULL_REMOTE; } else if (mode == SyncLaunchMode::PUSH_LOCAL) { - syncIntent = KOReaderSyncActivity::SyncIntent::PUSH_LOCAL; + syncIntent = KOReaderSyncIntentState::PUSH_LOCAL; } - startActivityForResult( - std::make_unique(renderer, mappedInput, std::shared_ptr{}, syncEpubPath, - currentSpineIndex, currentPage, totalPages, 0, false, syncIntent), - [this](const ActivityResult& result) { handleSyncResult(result); }); + auto& sync = APP_STATE.koReaderSyncSession; + sync.active = true; + sync.epubPath = epub->getPath(); + sync.spineIndex = currentSpineIndex; + sync.page = currentPage; + sync.totalPagesInSpine = totalPages; + sync.paragraphIndex = 0; + sync.hasParagraphIndex = false; + sync.intent = syncIntent; + sync.outcome = KOReaderSyncOutcomeState::PENDING; + sync.resultSpineIndex = 0; + sync.resultPage = 0; + sync.resultParagraphIndex = 0; + sync.resultHasParagraphIndex = false; + APP_STATE.saveToFile(); + + LOG_DBG("ERS", "Standalone sync handoff: spine=%d page=%d/%d", currentSpineIndex, currentPage, totalPages); + logReaderMemSnapshot("before_replace_with_sync"); + activityManager.goToKOReaderSync(); } -void EpubReaderActivity::handleSyncResult(const ActivityResult& result) { - if (!epub && !deferredSyncEpubPath.empty()) { - epub = std::make_shared(deferredSyncEpubPath, "/.crosspoint"); - if (!epub->load(true, true)) { - LOG_ERR("ERS", "Failed to reload EPUB after sync: %s", deferredSyncEpubPath.c_str()); - finish(); - return; - } - epub->setupCacheDir(); - LOG_DBG("ERS", "Reloaded EPUB after sync: %s", deferredSyncEpubPath.c_str()); - deferredSyncEpubPath.clear(); +void EpubReaderActivity::applyPendingSyncSession() { + auto& sync = APP_STATE.koReaderSyncSession; + if (!sync.active || !epub || sync.epubPath != epub->getPath()) { + return; } - if (!result.isCancelled) { - const auto& sync = std::get(result.data); - if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) { - RenderLock lock(*this); - currentSpineIndex = sync.spineIndex; - nextPageNumber = sync.page; - section.reset(); - } + LOG_DBG("ERS", "Applying pending sync session outcome=%d path=%s", static_cast(sync.outcome), sync.epubPath.c_str()); + + int restoreSpineIndex = sync.spineIndex; + int restorePage = sync.page; + pendingParagraphLookup = sync.hasParagraphIndex; + pendingParagraphIndex = sync.paragraphIndex; + + if (sync.outcome == KOReaderSyncOutcomeState::APPLIED_REMOTE) { + restoreSpineIndex = sync.resultSpineIndex; + restorePage = sync.resultPage; + pendingParagraphLookup = sync.resultHasParagraphIndex; + pendingParagraphIndex = sync.resultParagraphIndex; + LOG_DBG("ERS", "Applied synced remote position: spine=%d page=%d paragraph=%u hasParagraph=%s", + restoreSpineIndex, restorePage, pendingParagraphIndex, pendingParagraphLookup ? "yes" : "no"); + } else { + LOG_DBG("ERS", "Restored local pre-sync position: spine=%d page=%d paragraph=%u hasParagraph=%s", restoreSpineIndex, + restorePage, pendingParagraphIndex, pendingParagraphLookup ? "yes" : "no"); } + + if (writeReaderProgressCache(epub->getCachePath(), restoreSpineIndex, restorePage, sync.totalPagesInSpine)) { + cachedSpineIndex = restoreSpineIndex; + cachedChapterTotalPageCount = sync.totalPagesInSpine; + LOG_DBG("ERS", "Prepared progress.bin for sync restore: spine=%d page=%d/%d", restoreSpineIndex, restorePage, + sync.totalPagesInSpine); + } else { + // Fall back to directly seeding live state if cache write fails. + currentSpineIndex = restoreSpineIndex; + nextPageNumber = restorePage; + cachedSpineIndex = restoreSpineIndex; + cachedChapterTotalPageCount = sync.totalPagesInSpine; + } + + sync.clear(); + APP_STATE.saveToFile(); + logReaderMemSnapshot("after_apply_pending_sync_session"); } void EpubReaderActivity::applyOrientation(const uint8_t orientation) { @@ -914,8 +953,8 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC FsFile f; if (Storage.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) { uint8_t data[6]; - data[0] = currentSpineIndex & 0xFF; - data[1] = (currentSpineIndex >> 8) & 0xFF; + data[0] = spineIndex & 0xFF; + data[1] = (spineIndex >> 8) & 0xFF; data[2] = currentPage & 0xFF; data[3] = (currentPage >> 8) & 0xFF; data[4] = pageCount & 0xFF; diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 5cd77a37..07cb710a 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -48,7 +48,6 @@ class EpubReaderActivity final : public Activity { bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit ReaderUtils::InputDrainGuard inputDrainGuard; bool automaticPageTurnActive = false; - std::string deferredSyncEpubPath; // -1 means use global SETTINGS value. int8_t bookEmbeddedStyleOverride = -1; int8_t bookImageRenderingOverride = -1; @@ -72,7 +71,7 @@ class EpubReaderActivity final : public Activity { void jumpToPercent(int percent); void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action); void launchKOReaderSync(SyncLaunchMode mode = SyncLaunchMode::COMPARE); - void handleSyncResult(const ActivityResult& result); + void applyPendingSyncSession(); void applyOrientation(uint8_t orientation); void applyTextDarkness(uint8_t textDarkness); void toggleAutoPageTurn(uint8_t selectedPageTurnOption); diff --git a/src/activities/reader/KOReaderSyncActivity.cpp b/src/activities/reader/KOReaderSyncActivity.cpp index 522080c9..496c5423 100644 --- a/src/activities/reader/KOReaderSyncActivity.cpp +++ b/src/activities/reader/KOReaderSyncActivity.cpp @@ -57,11 +57,8 @@ bool shouldSyncNtpNow() { void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) { if (!success) { - LOG_DBG("KOSync", "WiFi connection failed, exiting"); - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); + LOG_DBG("KOSync", "WiFi connection failed, resuming reader"); + resumeReader(KOReaderSyncOutcomeState::CANCELLED); return; } @@ -118,7 +115,7 @@ void KOReaderSyncActivity::performSync() { // Local mapping is only needed for compare/upload paths. // Pull-only mode can skip this expensive step and go straight to remote fetch. - if (syncIntent != SyncIntent::PULL_REMOTE) { + if (syncIntent != KOReaderSyncIntentState::PULL_REMOTE) { // Precompute local mapping before first network request so the expensive // inflate/index work happens before TLS. This avoids a second local mapping // pass later and keeps the upload path lightweight. @@ -143,7 +140,7 @@ void KOReaderSyncActivity::performSync() { // Push intent skips comparison UI but still warms an HTTP/TLS session first // so PUT can reuse the connection instead of forcing a fresh handshake. - if (syncIntent == SyncIntent::PUSH_LOCAL) { + if (syncIntent == KOReaderSyncIntentState::PUSH_LOCAL) { // Direct push previously started with no reusable HTTP/TLS session, forcing // a fresh handshake in updateProgress. Compare flow often succeeds because // upload reuses the GET session. Warm the session here so push can take the @@ -184,7 +181,7 @@ void KOReaderSyncActivity::performSync() { const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress); if (result == KOReaderSyncClient::NOT_FOUND) { - if (syncIntent == SyncIntent::PULL_REMOTE) { + if (syncIntent == KOReaderSyncIntentState::PULL_REMOTE) { // Pull intent must not silently fall back to upload when server has no // remote progress. Failing explicitly keeps action semantics predictable. KOReaderSyncClient::endPersistentSession(); @@ -236,7 +233,7 @@ void KOReaderSyncActivity::performSync() { remotePosition.hasParagraphIndex = false; remoteChapterLabel.clear(); - if (syncIntent == SyncIntent::PULL_REMOTE) { + if (syncIntent == KOReaderSyncIntentState::PULL_REMOTE) { // Pull intent applies immediately and exits. We bypass chooser UI to keep // reader menu actions deterministic ("pull" always means apply remote). if (!ensureRemotePositionMapped()) { @@ -251,8 +248,13 @@ void KOReaderSyncActivity::performSync() { // Preserve the apply result and show explicit confirmation before returning // to the reader so users can tell pull succeeded. - setResult(SyncResult{remotePosition.spineIndex, remotePosition.pageNumber, remotePosition.paragraphIndex, - remotePosition.hasParagraphIndex}); + auto& sync = APP_STATE.koReaderSyncSession; + sync.outcome = KOReaderSyncOutcomeState::APPLIED_REMOTE; + sync.resultSpineIndex = remotePosition.spineIndex; + sync.resultPage = remotePosition.pageNumber; + sync.resultParagraphIndex = remotePosition.paragraphIndex; + sync.resultHasParagraphIndex = remotePosition.hasParagraphIndex; + APP_STATE.saveToFile(); { RenderLock lock(*this); state = APPLY_COMPLETE; @@ -370,6 +372,8 @@ void KOReaderSyncActivity::performUpload() { } HalClock::wifiOff(true); + APP_STATE.koReaderSyncSession.outcome = KOReaderSyncOutcomeState::UPLOAD_COMPLETE; + APP_STATE.saveToFile(); { RenderLock lock(*this); state = UPLOAD_COMPLETE; @@ -380,6 +384,9 @@ void KOReaderSyncActivity::performUpload() { void KOReaderSyncActivity::onEnter() { Activity::onEnter(); + logSyncMemSnapshot("onEnter_begin"); + LOG_DBG("KOSync", "Standalone sync start: path=%s spine=%d page=%d/%d intent=%d", epubPath.c_str(), currentSpineIndex, + currentPage, totalPagesInSpine, static_cast(syncIntent)); // Check for credentials first if (!KOREADER_STORE.hasCredentials()) { @@ -404,8 +411,11 @@ void KOReaderSyncActivity::onEnter() { void KOReaderSyncActivity::onExit() { Activity::onExit(); + logSyncMemSnapshot("onExit_before_cleanup"); KOReaderSyncClient::endPersistentSession(); HalClock::wifiOff(true); + releaseEpubForMapping(); + logSyncMemSnapshot("onExit_after_cleanup"); } void KOReaderSyncActivity::closeCancelled() { @@ -413,11 +423,31 @@ void KOReaderSyncActivity::closeCancelled() { return; } + resumeReader(KOReaderSyncOutcomeState::CANCELLED); +} + +void KOReaderSyncActivity::resumeReader(const KOReaderSyncOutcomeState outcome, const SyncResult* appliedResult) { + if (closeRequested) { + return; + } + closeRequested = true; - ActivityResult result; - result.isCancelled = true; - setResult(std::move(result)); - finish(); + auto& sync = APP_STATE.koReaderSyncSession; + sync.outcome = outcome; + if (appliedResult) { + sync.resultSpineIndex = appliedResult->spineIndex; + sync.resultPage = appliedResult->page; + sync.resultParagraphIndex = appliedResult->paragraphIndex; + sync.resultHasParagraphIndex = appliedResult->hasParagraphIndex; + } else { + sync.resultSpineIndex = 0; + sync.resultPage = 0; + sync.resultParagraphIndex = 0; + sync.resultHasParagraphIndex = false; + } + APP_STATE.saveToFile(); + logSyncMemSnapshot("before_resume_reader"); + activityManager.goToReader(epubPath); } void KOReaderSyncActivity::render(RenderLock&&) { @@ -626,22 +656,23 @@ void KOReaderSyncActivity::computeRemoteChapter() { void KOReaderSyncActivity::loop() { if (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE || state == APPLY_COMPLETE) { if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { - // APPLY_COMPLETE already has a valid SyncResult, so exit normally. - // Other terminal states are treated as cancelled when backing out. if (state == APPLY_COMPLETE) { - finish(); + resumeReader(KOReaderSyncOutcomeState::APPLIED_REMOTE); + } else if (state == UPLOAD_COMPLETE) { + resumeReader(KOReaderSyncOutcomeState::UPLOAD_COMPLETE); + } else if (state == SYNC_FAILED || state == NO_CREDENTIALS) { + resumeReader(KOReaderSyncOutcomeState::FAILED); } else { - closeCancelled(); + resumeReader(KOReaderSyncOutcomeState::CANCELLED); } return; } if ((state == UPLOAD_COMPLETE || state == APPLY_COMPLETE) && millis() - uploadCompleteTime >= 3000) { - // Keep pull/apply result on auto-close; upload-complete remains cancel-style. if (state == APPLY_COMPLETE) { - finish(); + resumeReader(KOReaderSyncOutcomeState::APPLIED_REMOTE); } else { - closeCancelled(); + resumeReader(KOReaderSyncOutcomeState::UPLOAD_COMPLETE); } } return; @@ -671,9 +702,9 @@ void KOReaderSyncActivity::loop() { return; } // Wifi will be turned off in onExit() - setResult(SyncResult{remotePosition.spineIndex, remotePosition.pageNumber, remotePosition.paragraphIndex, - remotePosition.hasParagraphIndex}); - finish(); + const SyncResult result = {remotePosition.spineIndex, remotePosition.pageNumber, remotePosition.paragraphIndex, + remotePosition.hasParagraphIndex}; + resumeReader(KOReaderSyncOutcomeState::APPLIED_REMOTE, &result); } else if (selectedOption == 1) { // Upload local progress performUpload(); diff --git a/src/activities/reader/KOReaderSyncActivity.h b/src/activities/reader/KOReaderSyncActivity.h index da7eb124..06ac7bb5 100644 --- a/src/activities/reader/KOReaderSyncActivity.h +++ b/src/activities/reader/KOReaderSyncActivity.h @@ -1,12 +1,12 @@ #pragma once #include -#include #include #include "ChapterXPathIndexer.h" #include "KOReaderSyncClient.h" #include "ProgressMapper.h" +#include "CrossPointState.h" #include "activities/Activity.h" /** @@ -27,24 +27,11 @@ */ class KOReaderSyncActivity final : public Activity { public: - // Intent controls UI/behavior split for the same sync pipeline. - // - COMPARE: fetch then let user choose apply/upload. - // - PULL_REMOTE: fetch and apply immediately. - // - PUSH_LOCAL: upload immediately. - // This keeps WiFi/NTP/hash/memory handling centralized while enabling a - // simpler KOReader-like reader menu UX. - enum class SyncIntent { - COMPARE, - PULL_REMOTE, - PUSH_LOCAL, - }; - - explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::shared_ptr& epub, const std::string& epubPath, int currentSpineIndex, - int currentPage, int totalPagesInSpine, uint16_t paragraphIndex = 0, - bool hasParagraphIndex = false, SyncIntent syncIntent = SyncIntent::COMPARE) + explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& epubPath, + int currentSpineIndex, int currentPage, int totalPagesInSpine, + uint16_t paragraphIndex = 0, bool hasParagraphIndex = false, + KOReaderSyncIntentState syncIntent = KOReaderSyncIntentState::COMPARE) : Activity("KOReaderSync", renderer, mappedInput), - epub(epub), epubPath(epubPath), currentSpineIndex(currentSpineIndex), currentPage(currentPage), @@ -83,7 +70,7 @@ class KOReaderSyncActivity final : public Activity { int totalPagesInSpine; uint16_t localParagraphIndex; bool hasLocalParagraphIndex; - SyncIntent syncIntent = SyncIntent::COMPARE; + KOReaderSyncIntentState syncIntent = KOReaderSyncIntentState::COMPARE; State state = WIFI_SELECTION; std::string statusMessage; @@ -111,6 +98,7 @@ class KOReaderSyncActivity final : public Activity { void performSync(); void performUpload(); void closeCancelled(); + void resumeReader(KOReaderSyncOutcomeState outcome, const SyncResult* appliedResult = nullptr); bool ensureEpubLoadedForMapping(); void releaseEpubForMapping(); bool computeLocalProgressAndChapter(); diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index 3f702ab8..ee7eb060 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -3,8 +3,12 @@ #include #include #include +#include +#include +#include #include "CrossPointSettings.h" +#include "CrossPointState.h" #include "Epub.h" #include "EpubReaderActivity.h" #include "Txt.h" @@ -16,6 +20,14 @@ #include "components/UITheme.h" #include "fontIds.h" +namespace { +void logReaderLaunchMemSnapshot(const char* stage) { + const uint32_t freeHeap = esp_get_free_heap_size(); + const uint32_t contigHeap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT); + LOG_DBG("READER", "Reader mem[%s]: free=%lu contig=%lu", stage, freeHeap, contigHeap); +} +} // namespace + std::string ReaderActivity::extractFolderPath(const std::string& filePath) { const auto lastSlash = filePath.find_last_of('/'); if (lastSlash == std::string::npos || lastSlash == 0) { @@ -90,6 +102,7 @@ void ReaderActivity::goToLibrary(const std::string& fromBookPath) { void ReaderActivity::onGoToEpubReader(std::unique_ptr epub) { const auto epubPath = epub->getPath(); currentBookPath = epubPath; + logReaderLaunchMemSnapshot("before_push_epub_reader"); startActivityForResult(std::make_unique(renderer, mappedInput, std::move(epub)), [this](const ActivityResult&) { finish(); }); } @@ -115,12 +128,17 @@ void ReaderActivity::onGoToTxtReader(std::unique_ptr txt) { void ReaderActivity::onEnter() { Activity::onEnter(); + logReaderLaunchMemSnapshot("onEnter_begin"); if (initialBookPath.empty()) { goToLibrary(); // Start from root when entering via Browse return; } + if (APP_STATE.koReaderSyncSession.active && APP_STATE.koReaderSyncSession.epubPath == initialBookPath) { + LOG_DBG("READER", "Opening EPUB with pending KOReader sync outcome=%d", static_cast(APP_STATE.koReaderSyncSession.outcome)); + } + currentBookPath = initialBookPath; if (isImageFile(initialBookPath)) { onGoToBmpViewer(initialBookPath);