fix: free Epub RAM and simplify KOSync navigation via ActivityManager (#1860)

## Summary

* **What is the goal of this PR?** Fix KOSync failing with "Network
error" on large/complex EPUBs, and simplify the sync navigation flow by
removing the callback/result pattern.

* **What changes are included?**

This PR combines the approaches from #1855 and #1760 into a single,
cleaner solution:

  **Memory fix (from #1855):**
- `EpubReaderActivity` pre-computes the local KOReader position and
chapter name, then explicitly releases `epub` and `section` before
launching `KOReaderSyncActivity`. This frees ~65KB measured on device,
giving the TLS handshake sufficient heap. The root cause was
`MBEDTLS_ERR_X509_ALLOC_FAILED` (-0x2880) when a 3-cert chain consumed
~48KB during the handshake with only ~50KB available.
- `KOReaderSyncActivity` no longer receives a `shared_ptr<Epub>` at
construction — it lazy-loads the Epub after TLS only if remote progress
is found (`ensureEpubLoaded()`).
- Added `MIN_HEAP_FOR_TLS = 55000` guard in `KOReaderSyncClient` —
returns `LOW_MEMORY` early if aggregate free heap is too low before
attempting a TLS connection.

  **Navigation simplification (from #1760):**
- Replaced `startActivityForResult` + callback with
`activityManager.replaceActivity` / `activityManager.goToReader`.
Progress is saved to `progress.bin` before the epub is released
(cancel/upload paths) and in `saveProgressAndReturn` (apply remote
path). The reader re-launches from the saved position naturally via
`goToReader`, eliminating the need to reload the epub in a callback.
- Extracted `ReaderUtils::saveProgress()` as a shared helper used by
both `EpubReaderActivity` and `KOReaderSyncActivity`.
- Added `STR_SAVE_PROGRESS_FAILED` to all 22 language files for the case
where writing the synced position to SD fails.

  **Orientation fix (found during device testing):**
- `EpubReaderActivity::onExit()` resets the renderer to portrait before
destruction. With `replaceActivity` the reader is fully torn down before
KOSync starts, so KOSync was always rendering in portrait even when
reading in landscape. Fixed by calling `ReaderUtils::applyOrientation`
in `KOReaderSyncActivity::onEnter()`.

## Additional Context

Heap measurements on device (large EPUB with complex CSS):

| Metric | Before | After |
|---|---|---|
| Heap before Epub release | 88,156 bytes | — |
| Heap after Epub release | — | 153,892 bytes (+65,736) |
| Heap at TLS handshake | ~50,000 bytes (fails) | ~116,384 bytes
(passes) |
| Min-free-ever during sync session | 2,600 bytes | 33,052 bytes |
| TLS result | `MBEDTLS_ERR_X509_ALLOC_FAILED` | HTTP 200 |

Tested on device: sync from inside a large EPUB in both portrait and
landscape, cancel, apply remote progress, upload local progress.

---

### AI Usage

Did you use AI tools to help write this code? _**YES**_

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Wylan Swets
2026-05-08 22:01:31 -05:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent 7993b2bb97
commit e64155ed63
29 changed files with 221 additions and 91 deletions
+56 -33
View File
@@ -18,9 +18,11 @@
#include "EpubReaderChapterSelectionActivity.h"
#include "EpubReaderFootnotesActivity.h"
#include "EpubReaderPercentSelectionActivity.h"
#include "EpubReaderUtils.h"
#include "KOReaderCredentialStore.h"
#include "KOReaderSyncActivity.h"
#include "MappedInputManager.h"
#include "ProgressMapper.h"
#include "QrDisplayActivity.h"
#include "ReaderUtils.h"
#include "RecentBooksStore.h"
@@ -392,7 +394,9 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
section.reset();
epub->clearCache();
epub->setupCacheDir();
saveProgress(backupSpine, backupPage, backupPageCount);
if (!saveProgress(backupSpine, backupPage, backupPageCount)) {
LOG_ERR("ERS", "Failed to save progress before cache clear");
}
}
}
onGoHome();
@@ -418,23 +422,42 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
paragraphIndex = *pIdx;
}
}
startActivityForResult(
std::make_unique<KOReaderSyncActivity>(renderer, mappedInput, epub, epub->getPath(), currentSpineIndex,
currentPage, totalPages, paragraphIndex),
[this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& sync = std::get<SyncResult>(result.data);
if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) {
RenderLock lock(*this);
currentSpineIndex = sync.spineIndex;
nextPageNumber = sync.page;
cachedChapterTotalPageCount = 0; // Prevent rescaling sync page
pendingPageJump.reset();
saveProgress(currentSpineIndex, nextPageNumber, 0);
section.reset();
}
}
});
// Pre-compute local KO position and chapter name while Epub is still in RAM.
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPages};
if (paragraphIndex.has_value()) {
localPos.paragraphIndex = *paragraphIndex;
localPos.hasParagraphIndex = true;
}
KOReaderPosition localKoPos = ProgressMapper::toKOReader(epub, localPos);
const int tocIdx = epub->getTocIndexForSpineIndex(currentSpineIndex);
std::string localChapterName = (tocIdx >= 0) ? epub->getTocItem(tocIdx).title : "";
const std::string savedEpubPath = epub->getPath();
// Persist current position so the reader resumes at the right page on return.
// goToReader() depends on this file, so abort the sync if the write fails.
if (!saveProgress(currentSpineIndex, currentPage, totalPages)) {
LOG_ERR("KOSync", "Aborting sync because current progress could not be saved");
pendingSyncSaveError = true;
requestUpdate();
return;
}
// Release Epub and Section to free ~65KB RAM for the TLS handshake.
LOG_DBG("KOSync", "Releasing epub for sync (heap before: %u)", (unsigned)ESP.getFreeHeap());
{
RenderLock lock(*this);
if (section) {
nextPageNumber = section->currentPage;
}
section.reset();
epub.reset();
}
LOG_DBG("KOSync", "Epub released (heap after: %u)", (unsigned)ESP.getFreeHeap());
activityManager.replaceActivity(std::make_unique<KOReaderSyncActivity>(
renderer, mappedInput, savedEpubPath, currentSpineIndex, currentPage, totalPages, std::move(localKoPos),
std::move(localChapterName), paragraphIndex));
}
break;
}
@@ -530,6 +553,12 @@ void EpubReaderActivity::render(RenderLock&& lock) {
return;
}
const auto showPendingSyncSaveError = [this]() {
if (!pendingSyncSaveError) return;
pendingSyncSaveError = false;
GUI.drawPopup(renderer, tr(STR_SAVE_PROGRESS_FAILED));
};
// edge case handling for sub-zero spine index
if (currentSpineIndex < 0) {
currentSpineIndex = 0;
@@ -545,6 +574,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD);
renderer.displayBuffer();
automaticPageTurnActive = false;
showPendingSyncSaveError();
return;
}
@@ -592,6 +622,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
SETTINGS.imageRendering, popupFn)) {
LOG_ERR("ERS", "Failed to persist page data to SD");
section.reset();
showPendingSyncSaveError();
return;
}
} else {
@@ -655,6 +686,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
renderStatusBar();
renderer.displayBuffer();
automaticPageTurnActive = false;
showPendingSyncSaveError();
return;
}
@@ -664,6 +696,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
renderStatusBar();
renderer.displayBuffer();
automaticPageTurnActive = false;
showPendingSyncSaveError();
return;
}
@@ -676,6 +709,7 @@ void EpubReaderActivity::render(RenderLock&& lock) {
requestUpdate(); // Try again after clearing cache
// TODO: prevent infinite loop if the page keeps failing to load for some reason
automaticPageTurnActive = false;
showPendingSyncSaveError();
return;
}
@@ -689,6 +723,8 @@ void EpubReaderActivity::render(RenderLock&& lock) {
silentIndexNextChapterIfNeeded(viewportWidth, viewportHeight);
saveProgress(currentSpineIndex, section->currentPage, section->pageCount);
showPendingSyncSaveError();
if (pendingScreenshot) {
pendingScreenshot = false;
ScreenshotUtil::takeScreenshot(renderer);
@@ -727,21 +763,8 @@ void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportW
}
}
void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
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[2] = currentPage & 0xFF;
data[3] = (currentPage >> 8) & 0xFF;
data[4] = pageCount & 0xFF;
data[5] = (pageCount >> 8) & 0xFF;
f.write(data, 6);
LOG_DBG("ERS", "Progress saved: Chapter %d, Page %d", spineIndex, currentPage);
} else {
LOG_ERR("ERS", "Could not save progress!");
}
bool EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) {
return EpubReaderUtils::saveProgress(*epub, spineIndex, currentPage, pageCount);
}
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
const int orientedMarginRight, const int orientedMarginBottom,