Merge pull request #37 from jpirnay/refactor-koreader
refactor: refactor koreader to avoid OOM regressions
This commit is contained in:
@@ -145,14 +145,12 @@ void EpubReaderActivity::loop() {
|
||||
}
|
||||
|
||||
// Long press CONFIRM (1s+) goes directly to KOReader sync when credentials are configured.
|
||||
// We intentionally keep long-press on the richer compare flow so advanced
|
||||
// conflict-resolution behavior stays available even after simplifying menu UX.
|
||||
// Without credentials, fall through to the regular menu on release.
|
||||
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) &&
|
||||
mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS && KOREADER_STORE.hasCredentials()) {
|
||||
const int currentPage = section ? section->currentPage : 0;
|
||||
const int totalPages = section ? section->pageCount : 0;
|
||||
startActivityForResult(std::make_unique<KOReaderSyncActivity>(renderer, mappedInput, epub, epub->getPath(),
|
||||
currentSpineIndex, currentPage, totalPages),
|
||||
[this](const ActivityResult& result) { handleSyncResult(result); });
|
||||
launchKOReaderSync(SyncLaunchMode::COMPARE);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -464,20 +462,85 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
||||
requestUpdate();
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::SYNC: {
|
||||
case EpubReaderMenuActivity::MenuAction::PULL_REMOTE: {
|
||||
// One-tap pull path: run network preconditions and apply remote progress
|
||||
// directly instead of showing an intermediate chooser screen.
|
||||
if (KOREADER_STORE.hasCredentials()) {
|
||||
const int currentPage = section ? section->currentPage : 0;
|
||||
const int totalPages = section ? section->pageCount : 0;
|
||||
startActivityForResult(std::make_unique<KOReaderSyncActivity>(renderer, mappedInput, epub, epub->getPath(),
|
||||
currentSpineIndex, currentPage, totalPages),
|
||||
[this](const ActivityResult& result) { handleSyncResult(result); });
|
||||
launchKOReaderSync(SyncLaunchMode::PULL_REMOTE);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case EpubReaderMenuActivity::MenuAction::PUSH_LOCAL: {
|
||||
// One-tap push path: run network preconditions and upload local progress
|
||||
// directly for KOReader-like "sync now" behavior.
|
||||
if (KOREADER_STORE.hasCredentials()) {
|
||||
launchKOReaderSync(SyncLaunchMode::PUSH_LOCAL);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EpubReaderActivity::launchKOReaderSync(const SyncLaunchMode mode) {
|
||||
if (!epub) {
|
||||
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<unsigned long>(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;
|
||||
if (mode == SyncLaunchMode::PULL_REMOTE) {
|
||||
syncIntent = KOReaderSyncActivity::SyncIntent::PULL_REMOTE;
|
||||
} else if (mode == SyncLaunchMode::PUSH_LOCAL) {
|
||||
syncIntent = KOReaderSyncActivity::SyncIntent::PUSH_LOCAL;
|
||||
}
|
||||
|
||||
startActivityForResult(
|
||||
std::make_unique<KOReaderSyncActivity>(renderer, mappedInput, std::shared_ptr<Epub>{}, syncEpubPath,
|
||||
currentSpineIndex, currentPage, totalPages, 0, false, syncIntent),
|
||||
[this](const ActivityResult& result) { handleSyncResult(result); });
|
||||
}
|
||||
|
||||
void EpubReaderActivity::handleSyncResult(const ActivityResult& result) {
|
||||
if (!epub && !deferredSyncEpubPath.empty()) {
|
||||
epub = std::make_shared<Epub>(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();
|
||||
}
|
||||
|
||||
if (!result.isCancelled) {
|
||||
const auto& sync = std::get<SyncResult>(result.data);
|
||||
if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) {
|
||||
|
||||
@@ -9,6 +9,17 @@
|
||||
#include "activities/Activity.h"
|
||||
|
||||
class EpubReaderActivity final : public Activity {
|
||||
// Reader can launch sync in three UX modes:
|
||||
// - COMPARE: legacy chooser (apply/upload) for power users.
|
||||
// - PULL_REMOTE / PUSH_LOCAL: direct one-step actions from menu entries.
|
||||
// Keeping this split in the caller avoids branching on menu semantics deep
|
||||
// inside generic reader state handling.
|
||||
enum class SyncLaunchMode {
|
||||
COMPARE,
|
||||
PULL_REMOTE,
|
||||
PUSH_LOCAL,
|
||||
};
|
||||
|
||||
std::shared_ptr<Epub> epub;
|
||||
std::unique_ptr<Section> section = nullptr;
|
||||
int currentSpineIndex = 0;
|
||||
@@ -35,6 +46,7 @@ class EpubReaderActivity final : public Activity {
|
||||
bool pendingScreenshot = false;
|
||||
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
|
||||
bool automaticPageTurnActive = false;
|
||||
std::string deferredSyncEpubPath;
|
||||
// -1 means use global SETTINGS value.
|
||||
int8_t bookEmbeddedStyleOverride = -1;
|
||||
int8_t bookImageRenderingOverride = -1;
|
||||
@@ -57,6 +69,7 @@ class EpubReaderActivity final : public Activity {
|
||||
// Jump to a percentage of the book (0-100), mapping it to spine and page.
|
||||
void jumpToPercent(int percent);
|
||||
void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action);
|
||||
void launchKOReaderSync(SyncLaunchMode mode = SyncLaunchMode::COMPARE);
|
||||
void handleSyncResult(const ActivityResult& result);
|
||||
void applyOrientation(uint8_t orientation);
|
||||
void toggleAutoPageTurn(uint8_t selectedPageTurnOption);
|
||||
|
||||
@@ -25,7 +25,7 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu
|
||||
|
||||
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) {
|
||||
std::vector<MenuItem> items;
|
||||
items.reserve(10);
|
||||
items.reserve(12);
|
||||
items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER});
|
||||
if (hasFootnotes) {
|
||||
items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES});
|
||||
@@ -39,7 +39,8 @@ std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuI
|
||||
items.push_back({MenuAction::DISPLAY_QR, StrId::STR_DISPLAY_QR});
|
||||
items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON});
|
||||
if (KOREADER_STORE.hasCredentials()) {
|
||||
items.push_back({MenuAction::SYNC, StrId::STR_SYNC_PROGRESS});
|
||||
items.push_back({MenuAction::PULL_REMOTE, StrId::STR_PULL_PROGRESS_FROM_OTHER_DEVICES});
|
||||
items.push_back({MenuAction::PUSH_LOCAL, StrId::STR_PUSH_PROGRESS_FROM_THIS_DEVICE});
|
||||
}
|
||||
items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE});
|
||||
return items;
|
||||
|
||||
@@ -22,7 +22,8 @@ class EpubReaderMenuActivity final : public Activity {
|
||||
SCREENSHOT,
|
||||
DISPLAY_QR,
|
||||
GO_HOME,
|
||||
SYNC,
|
||||
PULL_REMOTE,
|
||||
PUSH_LOCAL,
|
||||
DELETE_CACHE
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#include "KOReaderSyncActivity.h"
|
||||
|
||||
#include <FontCacheManager.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalClock.h>
|
||||
#include <I18n.h>
|
||||
#include <Logging.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_system.h>
|
||||
|
||||
#include "KOReaderCredentialStore.h"
|
||||
#include "KOReaderDocumentId.h"
|
||||
@@ -13,6 +16,45 @@
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
|
||||
namespace {
|
||||
constexpr time_t NTP_RESYNC_MIN_INTERVAL_SEC = 15 * 60;
|
||||
|
||||
// Emits heap snapshots around sync stages so we can correlate TLS failures with
|
||||
// fragmentation and not just total free heap.
|
||||
void logSyncMemSnapshot(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);
|
||||
const bool integrityOk = heap_caps_check_integrity_all(true);
|
||||
LOG_DBG("KOSync", "Sync mem[%s]: free=%lu contig=%lu integrity=%s", stage, freeHeap, contigHeap,
|
||||
integrityOk ? "ok" : "fail");
|
||||
}
|
||||
|
||||
// Frees renderer-owned caches right before network work.
|
||||
// Why: TLS handshake needs a large contiguous block, and font cache memory can
|
||||
// increase fragmentation even when total free heap looks acceptable.
|
||||
void trimMemoryBeforeTls(const GfxRenderer& renderer) {
|
||||
if (auto* cacheManager = renderer.getFontCacheManager()) {
|
||||
cacheManager->clearCache();
|
||||
cacheManager->resetStats();
|
||||
LOG_DBG("KOSync", "Cleared font cache before TLS");
|
||||
}
|
||||
}
|
||||
|
||||
bool shouldSyncNtpNow() {
|
||||
const time_t lastSync = HalClock::lastSyncTime();
|
||||
const time_t now = HalClock::now();
|
||||
if (lastSync <= 0 || now <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const time_t age = now - lastSync;
|
||||
if (age < 0) {
|
||||
return true;
|
||||
}
|
||||
return age >= NTP_RESYNC_MIN_INTERVAL_SEC;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
|
||||
if (!success) {
|
||||
LOG_DBG("KOSync", "WiFi connection failed, exiting");
|
||||
@@ -30,18 +72,29 @@ void KOReaderSyncActivity::onWifiSelectionComplete(const bool success) {
|
||||
state = SYNCING;
|
||||
statusMessage = tr(STR_SYNCING_TIME);
|
||||
}
|
||||
requestUpdate(true);
|
||||
requestUpdate();
|
||||
|
||||
// Sync time with NTP before making API requests
|
||||
HalClock::syncNtp();
|
||||
// Avoid repeated NTP churn during rapid sync retries; it can fragment heap
|
||||
// right before TLS. Re-sync only when clock is stale.
|
||||
if (shouldSyncNtpNow()) {
|
||||
HalClock::syncNtp();
|
||||
} else {
|
||||
LOG_DBG("KOSync", "Skipping NTP sync (recently synced)");
|
||||
}
|
||||
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
statusMessage = tr(STR_CALC_HASH);
|
||||
}
|
||||
requestUpdate(true);
|
||||
requestUpdate();
|
||||
|
||||
logSyncMemSnapshot("before_performSync");
|
||||
trimMemoryBeforeTls(renderer);
|
||||
logSyncMemSnapshot("after_trim_before_performSync");
|
||||
|
||||
performSync();
|
||||
|
||||
logSyncMemSnapshot("after_performSync");
|
||||
}
|
||||
|
||||
void KOReaderSyncActivity::performSync() {
|
||||
@@ -63,16 +116,88 @@ void KOReaderSyncActivity::performSync() {
|
||||
|
||||
LOG_DBG("KOSync", "Document hash: %s", documentHash.c_str());
|
||||
|
||||
// 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) {
|
||||
// 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.
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
statusMessage = tr(STR_MAPPING_LOCAL);
|
||||
}
|
||||
requestUpdateAndWait();
|
||||
if (!computeLocalProgressAndChapter()) {
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = SYNC_FAILED;
|
||||
statusMessage = tr(STR_SYNC_FAILED_MSG);
|
||||
}
|
||||
requestUpdate(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Drop EPUB state before HTTPS to maximize contiguous heap for TLS.
|
||||
releaseEpubForMapping();
|
||||
|
||||
// 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) {
|
||||
// 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
|
||||
// same reuse path without showing comparison UI.
|
||||
KOReaderSyncClient::beginPersistentSession();
|
||||
KOReaderProgress warmupProgress;
|
||||
const auto warmupResult = KOReaderSyncClient::getProgress(documentHash, warmupProgress);
|
||||
if (warmupResult != KOReaderSyncClient::OK && warmupResult != KOReaderSyncClient::NOT_FOUND) {
|
||||
KOReaderSyncClient::endPersistentSession();
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = SYNC_FAILED;
|
||||
statusMessage = KOReaderSyncClient::errorString(warmupResult);
|
||||
const char* detail = KOReaderSyncClient::lastFailureDetail();
|
||||
if (detail && detail[0]) {
|
||||
statusMessage += " — ";
|
||||
statusMessage += detail;
|
||||
}
|
||||
}
|
||||
requestUpdate(true);
|
||||
return;
|
||||
}
|
||||
performUpload();
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
statusMessage = tr(STR_FETCH_PROGRESS);
|
||||
}
|
||||
requestUpdateAndWait();
|
||||
requestUpdate();
|
||||
|
||||
// Keep the GET connection alive so Upload can reuse the same session and
|
||||
// avoid a second TLS handshake under fragmented heap.
|
||||
KOReaderSyncClient::beginPersistentSession();
|
||||
|
||||
// Fetch remote progress
|
||||
const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress);
|
||||
|
||||
if (result == KOReaderSyncClient::NOT_FOUND) {
|
||||
if (syncIntent == SyncIntent::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();
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = SYNC_FAILED;
|
||||
statusMessage = tr(STR_NO_REMOTE_MSG);
|
||||
}
|
||||
requestUpdate(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep session open so an immediate upload can reuse the same connection.
|
||||
// No remote progress - offer to upload
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
@@ -84,6 +209,7 @@ void KOReaderSyncActivity::performSync() {
|
||||
}
|
||||
|
||||
if (result != KOReaderSyncClient::OK) {
|
||||
KOReaderSyncClient::endPersistentSession();
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = SYNC_FAILED;
|
||||
@@ -100,27 +226,48 @@ void KOReaderSyncActivity::performSync() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert remote progress to CrossPoint position
|
||||
hasRemoteProgress = true;
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
statusMessage = tr(STR_MAPPING_REMOTE);
|
||||
// Defer remote EPUB mapping until user chooses Apply. Upload only needs the
|
||||
// precomputed local XPath, so this avoids post-fetch inflate churn and keeps
|
||||
// the GET session reusable for PUT.
|
||||
hasRemoteProgress = false;
|
||||
remotePositionMapped = false;
|
||||
remotePosition.spineIndex = -1;
|
||||
remotePosition.pageNumber = -1;
|
||||
remotePosition.totalPages = 0;
|
||||
remotePosition.paragraphIndex = 0;
|
||||
remotePosition.hasParagraphIndex = false;
|
||||
remoteChapterLabel.clear();
|
||||
|
||||
if (syncIntent == SyncIntent::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()) {
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = SYNC_FAILED;
|
||||
statusMessage = tr(STR_SYNC_FAILED_MSG);
|
||||
}
|
||||
requestUpdate(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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});
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = APPLY_COMPLETE;
|
||||
uploadCompleteTime = millis();
|
||||
}
|
||||
requestUpdate(true);
|
||||
return;
|
||||
}
|
||||
requestUpdateAndWait();
|
||||
|
||||
KOReaderPosition koPos = {remoteProgress.progress, remoteProgress.percentage};
|
||||
remotePosition = ProgressMapper::toCrossPoint(epub, koPos, currentSpineIndex, totalPagesInSpine);
|
||||
|
||||
// Calculate local progress in KOReader format (for display)
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
statusMessage = tr(STR_MAPPING_LOCAL);
|
||||
}
|
||||
requestUpdateAndWait();
|
||||
|
||||
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine, localParagraphIndex,
|
||||
hasLocalParagraphIndex};
|
||||
localProgress = ProgressMapper::toKOReader(epub, localPos);
|
||||
// Compare intent keeps the legacy chooser flow (apply vs upload), which is
|
||||
// still useful for manual conflict decisions.
|
||||
// Local progress was precomputed before network; keep using the cached value.
|
||||
releaseEpubForMapping();
|
||||
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
@@ -144,17 +291,55 @@ void KOReaderSyncActivity::performUpload() {
|
||||
}
|
||||
requestUpdateAndWait();
|
||||
|
||||
// Convert current position to KOReader format
|
||||
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine, localParagraphIndex,
|
||||
hasLocalParagraphIndex};
|
||||
KOReaderPosition koPos = ProgressMapper::toKOReader(epub, localPos);
|
||||
// If sync reached this screen without cached local progress, compute it now.
|
||||
// This keeps upload robust when UI flow changes or retries happen.
|
||||
if (localProgress.xpath.empty()) {
|
||||
if (!computeLocalProgressAndChapter()) {
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = SYNC_FAILED;
|
||||
statusMessage = tr(STR_SYNC_FAILED_MSG);
|
||||
}
|
||||
requestUpdate(true);
|
||||
return;
|
||||
}
|
||||
releaseEpubForMapping();
|
||||
}
|
||||
|
||||
// Hard-stop if we still have no xpath: sending an empty progress payload would
|
||||
// be ambiguous server-side and hides the real local mapping failure.
|
||||
if (localProgress.xpath.empty()) {
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = SYNC_FAILED;
|
||||
statusMessage = tr(STR_SYNC_FAILED_MSG);
|
||||
}
|
||||
requestUpdate(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Result screen rendering repopulates glyph caches; trim again right before
|
||||
// the upload handshake to maximize contiguous heap for TLS.
|
||||
trimMemoryBeforeTls(renderer);
|
||||
logSyncMemSnapshot("after_trim_before_updateProgress");
|
||||
|
||||
// Capture upload-phase memory separately from fetch phase to diagnose failures
|
||||
// that only appear on PUT due to allocator state changes.
|
||||
logSyncMemSnapshot("before_updateProgress");
|
||||
|
||||
// Ensure a session exists for upload. In compare flow this comes from the
|
||||
// earlier GET; in direct-push flow it comes from the warmup GET above.
|
||||
// In both cases, reuse avoids a second full TLS handshake.
|
||||
KOReaderSyncClient::beginPersistentSession();
|
||||
|
||||
KOReaderProgress progress;
|
||||
progress.document = documentHash;
|
||||
progress.progress = koPos.xpath;
|
||||
progress.percentage = koPos.percentage;
|
||||
progress.progress = localProgress.xpath;
|
||||
progress.percentage = localProgress.percentage;
|
||||
|
||||
const auto result = KOReaderSyncClient::updateProgress(progress);
|
||||
KOReaderSyncClient::endPersistentSession();
|
||||
logSyncMemSnapshot("after_updateProgress");
|
||||
|
||||
if (result != KOReaderSyncClient::OK) {
|
||||
HalClock::wifiOff(true);
|
||||
@@ -209,6 +394,7 @@ void KOReaderSyncActivity::onEnter() {
|
||||
void KOReaderSyncActivity::onExit() {
|
||||
Activity::onExit();
|
||||
|
||||
KOReaderSyncClient::endPersistentSession();
|
||||
HalClock::wifiOff(true);
|
||||
}
|
||||
|
||||
@@ -241,7 +427,7 @@ void KOReaderSyncActivity::render(RenderLock&&) {
|
||||
}
|
||||
|
||||
if (state == SYNCING || state == UPLOADING) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD);
|
||||
GUI.drawPopup(renderer, statusMessage.c_str());
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
@@ -251,24 +437,20 @@ void KOReaderSyncActivity::render(RenderLock&&) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 120, tr(STR_PROGRESS_FOUND), true, EpdFontFamily::BOLD);
|
||||
|
||||
// Get chapter names from TOC
|
||||
const int remoteTocIndex = epub->getTocIndexForSpineIndex(remotePosition.spineIndex);
|
||||
const int localTocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
|
||||
const std::string remoteChapter =
|
||||
(remoteTocIndex >= 0) ? epub->getTocItem(remoteTocIndex).title
|
||||
: (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(remotePosition.spineIndex + 1));
|
||||
const std::string localChapter =
|
||||
(localTocIndex >= 0) ? epub->getTocItem(localTocIndex).title
|
||||
: (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(currentSpineIndex + 1));
|
||||
const std::string& remoteChapter = remoteChapterLabel;
|
||||
const std::string& localChapter = localChapterLabel;
|
||||
|
||||
// Remote progress - chapter and page
|
||||
renderer.drawText(UI_10_FONT_ID, contentRect.x + 20, 160, tr(STR_REMOTE_LABEL), true);
|
||||
char remoteChapterStr[128];
|
||||
snprintf(remoteChapterStr, sizeof(remoteChapterStr), " %s", remoteChapter.c_str());
|
||||
renderer.drawText(UI_10_FONT_ID, contentRect.x + 20, 185, remoteChapterStr);
|
||||
char remotePageStr[64];
|
||||
snprintf(remotePageStr, sizeof(remotePageStr), tr(STR_PAGE_OVERALL_FORMAT), remotePosition.pageNumber + 1,
|
||||
remoteProgress.percentage * 100);
|
||||
renderer.drawText(UI_10_FONT_ID, contentRect.x + 20, 210, remotePageStr);
|
||||
if (hasRemoteProgress) {
|
||||
char remoteChapterStr[128];
|
||||
snprintf(remoteChapterStr, sizeof(remoteChapterStr), " %s", remoteChapter.c_str());
|
||||
renderer.drawText(UI_10_FONT_ID, contentRect.x + 20, 185, remoteChapterStr);
|
||||
char remotePageStr[64];
|
||||
snprintf(remotePageStr, sizeof(remotePageStr), tr(STR_PAGE_OVERALL_FORMAT), remotePosition.pageNumber + 1,
|
||||
remoteProgress.percentage * 100);
|
||||
renderer.drawText(UI_10_FONT_ID, contentRect.x + 20, 210, remotePageStr);
|
||||
}
|
||||
|
||||
if (!remoteProgress.device.empty()) {
|
||||
char deviceStr[64];
|
||||
@@ -328,6 +510,15 @@ void KOReaderSyncActivity::render(RenderLock&&) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == APPLY_COMPLETE) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 300, tr(STR_PULL_SUCCESS), true, EpdFontFamily::BOLD);
|
||||
|
||||
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
|
||||
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
|
||||
renderer.displayBuffer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == SYNC_FAILED) {
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 280, tr(STR_SYNC_FAILED_MSG), true, EpdFontFamily::BOLD);
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, 320, statusMessage.c_str());
|
||||
@@ -339,15 +530,100 @@ void KOReaderSyncActivity::render(RenderLock&&) {
|
||||
}
|
||||
}
|
||||
|
||||
bool KOReaderSyncActivity::ensureEpubLoadedForMapping() {
|
||||
if (epub) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reload on demand to keep steady-state sync memory low. Mapping and chapter
|
||||
// lookup need EPUB metadata; TLS steps do not.
|
||||
epub = std::make_shared<Epub>(epubPath, "/.crosspoint");
|
||||
if (!epub->load(true, true)) {
|
||||
LOG_ERR("KOSync", "Failed to reload EPUB for mapping: %s", epubPath.c_str());
|
||||
epub.reset();
|
||||
return false;
|
||||
}
|
||||
epub->setupCacheDir();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool KOReaderSyncActivity::ensureRemotePositionMapped() {
|
||||
if (remotePositionMapped) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Apply needs remote->local mapping, which triggers EPUB inflate work.
|
||||
// Release HTTP/TLS session first so mapping has maximum heap headroom.
|
||||
KOReaderSyncClient::endPersistentSession();
|
||||
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
statusMessage = tr(STR_MAPPING_REMOTE);
|
||||
}
|
||||
requestUpdateAndWait();
|
||||
|
||||
KOReaderPosition koPos = {remoteProgress.progress, remoteProgress.percentage};
|
||||
if (!ensureEpubLoadedForMapping()) {
|
||||
return false;
|
||||
}
|
||||
remotePosition = ProgressMapper::toCrossPoint(epub, koPos, currentSpineIndex, totalPagesInSpine);
|
||||
computeRemoteChapter();
|
||||
releaseEpubForMapping();
|
||||
hasRemoteProgress = true;
|
||||
remotePositionMapped = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void KOReaderSyncActivity::releaseEpubForMapping() { epub.reset(); }
|
||||
|
||||
bool KOReaderSyncActivity::computeLocalProgressAndChapter() {
|
||||
if (!ensureEpubLoadedForMapping()) {
|
||||
localProgress = KOReaderPosition{};
|
||||
localChapterLabel.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine, localParagraphIndex,
|
||||
hasLocalParagraphIndex};
|
||||
localProgress = ProgressMapper::toKOReader(epub, localPos);
|
||||
|
||||
const int localTocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
|
||||
localChapterLabel = (localTocIndex >= 0)
|
||||
? epub->getTocItem(localTocIndex).title
|
||||
: (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(currentSpineIndex + 1));
|
||||
return true;
|
||||
}
|
||||
|
||||
void KOReaderSyncActivity::computeRemoteChapter() {
|
||||
if (!epub) {
|
||||
return;
|
||||
}
|
||||
const int remoteTocIndex = epub->getTocIndexForSpineIndex(remotePosition.spineIndex);
|
||||
remoteChapterLabel = (remoteTocIndex >= 0)
|
||||
? epub->getTocItem(remoteTocIndex).title
|
||||
: (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(remotePosition.spineIndex + 1));
|
||||
}
|
||||
|
||||
void KOReaderSyncActivity::loop() {
|
||||
if (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE) {
|
||||
if (state == NO_CREDENTIALS || state == SYNC_FAILED || state == UPLOAD_COMPLETE || state == APPLY_COMPLETE) {
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Back)) {
|
||||
closeCancelled();
|
||||
// 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();
|
||||
} else {
|
||||
closeCancelled();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (state == UPLOAD_COMPLETE && millis() - uploadCompleteTime >= 3000) {
|
||||
closeCancelled();
|
||||
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();
|
||||
} else {
|
||||
closeCancelled();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -366,6 +642,15 @@ void KOReaderSyncActivity::loop() {
|
||||
|
||||
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
|
||||
if (selectedOption == 0) {
|
||||
if (!ensureRemotePositionMapped()) {
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
state = SYNC_FAILED;
|
||||
statusMessage = tr(STR_SYNC_FAILED_MSG);
|
||||
}
|
||||
requestUpdate(true);
|
||||
return;
|
||||
}
|
||||
// Wifi will be turned off in onExit()
|
||||
setResult(SyncResult{remotePosition.spineIndex, remotePosition.pageNumber, remotePosition.paragraphIndex,
|
||||
remotePosition.hasParagraphIndex});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include "ChapterXPathIndexer.h"
|
||||
#include "KOReaderSyncClient.h"
|
||||
#include "ProgressMapper.h"
|
||||
#include "activities/Activity.h"
|
||||
@@ -11,19 +12,37 @@
|
||||
/**
|
||||
* Activity for syncing reading progress with KOReader sync server.
|
||||
*
|
||||
* Flow:
|
||||
* Shared pipeline:
|
||||
* 1. Connect to WiFi (if not connected)
|
||||
* 2. Calculate document hash
|
||||
* 3. Fetch remote progress
|
||||
* 4. Show comparison and options (Apply/Upload)
|
||||
* 5. Apply or upload progress
|
||||
* 2. Optionally sync NTP (if stale)
|
||||
* 3. Calculate document hash
|
||||
*
|
||||
* Intent-specific behavior:
|
||||
* - COMPARE: fetch remote progress, show full comparison screen, let user
|
||||
* choose Apply or Upload.
|
||||
* - PULL_REMOTE: fetch and map remote progress, show success feedback, then
|
||||
* return applied SyncResult to reader.
|
||||
* - PUSH_LOCAL: compute local mapping, warm session with GET, then upload via
|
||||
* reused connection to avoid a second full TLS handshake.
|
||||
*/
|
||||
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>& epub, const std::string& epubPath, int currentSpineIndex,
|
||||
int currentPage, int totalPagesInSpine, uint16_t paragraphIndex = 0,
|
||||
bool hasParagraphIndex = false)
|
||||
bool hasParagraphIndex = false, SyncIntent syncIntent = SyncIntent::COMPARE)
|
||||
: Activity("KOReaderSync", renderer, mappedInput),
|
||||
epub(epub),
|
||||
epubPath(epubPath),
|
||||
@@ -32,6 +51,7 @@ class KOReaderSyncActivity final : public Activity {
|
||||
totalPagesInSpine(totalPagesInSpine),
|
||||
localParagraphIndex(paragraphIndex),
|
||||
hasLocalParagraphIndex(hasParagraphIndex),
|
||||
syncIntent(syncIntent),
|
||||
remoteProgress{},
|
||||
remotePosition{},
|
||||
localProgress{} {}
|
||||
@@ -50,6 +70,7 @@ class KOReaderSyncActivity final : public Activity {
|
||||
SHOWING_RESULT,
|
||||
UPLOADING,
|
||||
UPLOAD_COMPLETE,
|
||||
APPLY_COMPLETE,
|
||||
NO_REMOTE_PROGRESS,
|
||||
SYNC_FAILED,
|
||||
NO_CREDENTIALS
|
||||
@@ -62,6 +83,7 @@ class KOReaderSyncActivity final : public Activity {
|
||||
int totalPagesInSpine;
|
||||
uint16_t localParagraphIndex;
|
||||
bool hasLocalParagraphIndex;
|
||||
SyncIntent syncIntent = SyncIntent::COMPARE;
|
||||
|
||||
State state = WIFI_SELECTION;
|
||||
std::string statusMessage;
|
||||
@@ -69,16 +91,19 @@ class KOReaderSyncActivity final : public Activity {
|
||||
|
||||
// Remote progress data
|
||||
bool hasRemoteProgress = false;
|
||||
bool remotePositionMapped = false;
|
||||
KOReaderProgress remoteProgress;
|
||||
CrossPointPosition remotePosition;
|
||||
|
||||
// Local progress as KOReader format (for display)
|
||||
KOReaderPosition localProgress;
|
||||
std::string remoteChapterLabel;
|
||||
std::string localChapterLabel;
|
||||
|
||||
// Selection in result screen (0=Apply, 1=Upload)
|
||||
int selectedOption = 0;
|
||||
|
||||
// Timestamp when UPLOAD_COMPLETE state was entered (for auto-close)
|
||||
// Timestamp when completion state was entered (for auto-close)
|
||||
unsigned long uploadCompleteTime = 0;
|
||||
bool closeRequested = false;
|
||||
|
||||
@@ -86,4 +111,9 @@ class KOReaderSyncActivity final : public Activity {
|
||||
void performSync();
|
||||
void performUpload();
|
||||
void closeCancelled();
|
||||
bool ensureEpubLoadedForMapping();
|
||||
void releaseEpubForMapping();
|
||||
bool computeLocalProgressAndChapter();
|
||||
void computeRemoteChapter();
|
||||
bool ensureRemotePositionMapped();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user