Split menu actions for smaller footprint
This commit is contained in:
@@ -302,6 +302,8 @@ STR_HW_RIGHT_LABEL: "Right (4th button)"
|
|||||||
STR_GO_TO_PERCENT: "Go to %"
|
STR_GO_TO_PERCENT: "Go to %"
|
||||||
STR_GO_HOME_BUTTON: "Go Home"
|
STR_GO_HOME_BUTTON: "Go Home"
|
||||||
STR_SYNC_PROGRESS: "Sync Progress"
|
STR_SYNC_PROGRESS: "Sync Progress"
|
||||||
|
STR_PUSH_PROGRESS_FROM_THIS_DEVICE: "Push progress from this device"
|
||||||
|
STR_PULL_PROGRESS_FROM_OTHER_DEVICES: "Pull progress from other devices"
|
||||||
STR_DELETE_CACHE: "Delete Book Cache"
|
STR_DELETE_CACHE: "Delete Book Cache"
|
||||||
STR_DELETE: "Delete"
|
STR_DELETE: "Delete"
|
||||||
STR_REMOVE: "Remove"
|
STR_REMOVE: "Remove"
|
||||||
@@ -331,6 +333,7 @@ STR_UPLOAD_LOCAL: "Upload local progress"
|
|||||||
STR_NO_REMOTE_MSG: "No remote progress found"
|
STR_NO_REMOTE_MSG: "No remote progress found"
|
||||||
STR_UPLOAD_PROMPT: "Upload current position?"
|
STR_UPLOAD_PROMPT: "Upload current position?"
|
||||||
STR_UPLOAD_SUCCESS: "Progress uploaded!"
|
STR_UPLOAD_SUCCESS: "Progress uploaded!"
|
||||||
|
STR_PULL_SUCCESS: "Remote progress applied!"
|
||||||
STR_SYNC_FAILED_MSG: "Sync failed"
|
STR_SYNC_FAILED_MSG: "Sync failed"
|
||||||
STR_SECTION_PREFIX: "Section "
|
STR_SECTION_PREFIX: "Section "
|
||||||
STR_UPLOAD: "Upload"
|
STR_UPLOAD: "Upload"
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstring>
|
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
|
||||||
#include "KOReaderCredentialStore.h"
|
#include "KOReaderCredentialStore.h"
|
||||||
@@ -50,7 +50,9 @@ constexpr char DEVICE_ID[] = "crosspoint-reader";
|
|||||||
// Payloads are tiny JSON, so throughput impact is minimal while avoiding
|
// Payloads are tiny JSON, so throughput impact is minimal while avoiding
|
||||||
// large transient allocations from default client buffer sizes.
|
// large transient allocations from default client buffer sizes.
|
||||||
constexpr int HTTP_BUF_SIZE = 1024;
|
constexpr int HTTP_BUF_SIZE = 1024;
|
||||||
constexpr unsigned TLS_CONTIG_HEAP_TOLERANCE = 512;
|
// Keep strict thresholding here. A small tolerance caused repeated handshake
|
||||||
|
// attempts in borderline-fragmented states that still failed in mbedTLS.
|
||||||
|
constexpr unsigned TLS_CONTIG_HEAP_TOLERANCE = 0;
|
||||||
|
|
||||||
// Captures radio/link state around failed connects.
|
// Captures radio/link state around failed connects.
|
||||||
// Why: many field failures look like TLS errors but are actually weak WiFi.
|
// Why: many field failures look like TLS errors but are actually weak WiFi.
|
||||||
@@ -135,10 +137,9 @@ std::string base64Encode(const std::string& input) {
|
|||||||
// lastFailureDetail() will report the heap shortage instead of attempting a doomed handshake.
|
// lastFailureDetail() will report the heap shortage instead of attempting a doomed handshake.
|
||||||
bool checkHeapForTls() {
|
bool checkHeapForTls() {
|
||||||
const bool hasReusableSession = g_keepSessionOpen && g_sessionClient != nullptr;
|
const bool hasReusableSession = g_keepSessionOpen && g_sessionClient != nullptr;
|
||||||
const bool isUpload = (KOReaderSyncClient::lastOperation &&
|
const bool isUpload =
|
||||||
strcmp(KOReaderSyncClient::lastOperation, "update progress") == 0);
|
(KOReaderSyncClient::lastOperation && strcmp(KOReaderSyncClient::lastOperation, "update progress") == 0);
|
||||||
const unsigned requiredContig =
|
const unsigned requiredContig = KOReaderSyncClient::MIN_CONTIG_HEAP_FOR_TLS;
|
||||||
isUpload ? KOReaderSyncClient::MIN_CONTIG_HEAP_FOR_TLS_UPLOAD : KOReaderSyncClient::MIN_CONTIG_HEAP_FOR_TLS;
|
|
||||||
|
|
||||||
// Upload can often reuse the already-established GET connection. In that case
|
// Upload can often reuse the already-established GET connection. In that case
|
||||||
// a full handshake allocation is typically unnecessary, so avoid failing fast
|
// a full handshake allocation is typically unnecessary, so avoid failing fast
|
||||||
@@ -158,6 +159,30 @@ bool checkHeapForTls() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void refreshHeapSnapshot() {
|
||||||
|
KOReaderSyncClient::lastHeapAtFailure = ESP.getFreeHeap();
|
||||||
|
KOReaderSyncClient::lastContigHeapAtFailure = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT | MALLOC_CAP_DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void logTlsAttemptPlan(const char* operation, int attempt) {
|
||||||
|
const bool isUpload = (operation && strcmp(operation, "update progress") == 0);
|
||||||
|
const bool hasReusableSession = g_keepSessionOpen && g_sessionClient != nullptr;
|
||||||
|
const unsigned requiredContig = (isUpload && hasReusableSession) ? KOReaderSyncClient::MIN_CONTIG_HEAP_FOR_TLS_UPLOAD
|
||||||
|
: KOReaderSyncClient::MIN_CONTIG_HEAP_FOR_TLS;
|
||||||
|
|
||||||
|
LOG_DBG("KOSync", "%s attempt %d: keep_session=%s reusable_session=%s tls_mode=%s heap=%u contig=%u need=%u",
|
||||||
|
operation ? operation : "request", attempt, g_keepSessionOpen ? "yes" : "no",
|
||||||
|
hasReusableSession ? "yes" : "no", (isUpload && hasReusableSession) ? "reuse" : "handshake",
|
||||||
|
KOReaderSyncClient::lastHeapAtFailure, KOReaderSyncClient::lastContigHeapAtFailure, requiredContig);
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetSessionClientForRetry() {
|
||||||
|
if (g_sessionClient) {
|
||||||
|
esp_http_client_cleanup(g_sessionClient);
|
||||||
|
g_sessionClient = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create configured esp_http_client with small TLS buffers
|
// Create configured esp_http_client with small TLS buffers
|
||||||
esp_http_client_handle_t createClient(const char* url, ResponseBuffer* buf,
|
esp_http_client_handle_t createClient(const char* url, ResponseBuffer* buf,
|
||||||
esp_http_client_method_t method = HTTP_METHOD_GET) {
|
esp_http_client_method_t method = HTTP_METHOD_GET) {
|
||||||
@@ -352,6 +377,14 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
|
|||||||
int httpCode = 0;
|
int httpCode = 0;
|
||||||
|
|
||||||
for (int attempt = 1; attempt <= 2; attempt++) {
|
for (int attempt = 1; attempt <= 2; attempt++) {
|
||||||
|
// Retry attempts can happen after memory churn from a failed handshake.
|
||||||
|
// Refresh heap snapshot each pass so preflight and diagnostics use current values.
|
||||||
|
refreshHeapSnapshot();
|
||||||
|
logTlsAttemptPlan("get progress", attempt);
|
||||||
|
if (!checkHeapForTls()) {
|
||||||
|
return NETWORK_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
clearResponseBuffer(activeBuf);
|
clearResponseBuffer(activeBuf);
|
||||||
|
|
||||||
esp_http_client_handle_t client = createClient(url.c_str(), &buf);
|
esp_http_client_handle_t client = createClient(url.c_str(), &buf);
|
||||||
@@ -378,6 +411,10 @@ KOReaderSyncClient::Error KOReaderSyncClient::getProgress(const std::string& doc
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Failed connect can leave a persistent client handle in a bad state.
|
||||||
|
// Recreate it before retry so we don't repeat work on a stale transport.
|
||||||
|
resetSessionClientForRetry();
|
||||||
|
|
||||||
LOG_ERR("KOSync", "getProgress connect failed on attempt %d, retrying once", attempt);
|
LOG_ERR("KOSync", "getProgress connect failed on attempt %d, retrying once", attempt);
|
||||||
logWifiSnapshot("WiFi before getProgress retry");
|
logWifiSnapshot("WiFi before getProgress retry");
|
||||||
delay(400);
|
delay(400);
|
||||||
@@ -442,6 +479,14 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
|
|||||||
int httpCode = 0;
|
int httpCode = 0;
|
||||||
|
|
||||||
for (int attempt = 1; attempt <= 2; attempt++) {
|
for (int attempt = 1; attempt <= 2; attempt++) {
|
||||||
|
// Retry attempts can happen after memory churn from a failed handshake.
|
||||||
|
// Refresh heap snapshot each pass so preflight and diagnostics use current values.
|
||||||
|
refreshHeapSnapshot();
|
||||||
|
logTlsAttemptPlan("update progress", attempt);
|
||||||
|
if (!checkHeapForTls()) {
|
||||||
|
return NETWORK_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
clearResponseBuffer(activeBuf);
|
clearResponseBuffer(activeBuf);
|
||||||
|
|
||||||
esp_http_client_handle_t client = createClient(url.c_str(), &buf, HTTP_METHOD_PUT);
|
esp_http_client_handle_t client = createClient(url.c_str(), &buf, HTTP_METHOD_PUT);
|
||||||
@@ -462,8 +507,7 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
|
|||||||
esp_http_client_cleanup(client);
|
esp_http_client_cleanup(client);
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DBG("KOSync", "Update progress response: %d (err: %s) [attempt %d]", httpCode, esp_err_to_name(err),
|
LOG_DBG("KOSync", "Update progress response: %d (err: %s) [attempt %d]", httpCode, esp_err_to_name(err), attempt);
|
||||||
attempt);
|
|
||||||
|
|
||||||
// Retry exactly once for connect-level failures only.
|
// Retry exactly once for connect-level failures only.
|
||||||
// Why: same policy as GET keeps behavior predictable across both endpoints.
|
// Why: same policy as GET keeps behavior predictable across both endpoints.
|
||||||
@@ -471,6 +515,10 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Failed connect can leave a persistent client handle in a bad state.
|
||||||
|
// Recreate it before retry so we don't repeat work on a stale transport.
|
||||||
|
resetSessionClientForRetry();
|
||||||
|
|
||||||
LOG_ERR("KOSync", "updateProgress connect failed on attempt %d, retrying once", attempt);
|
LOG_ERR("KOSync", "updateProgress connect failed on attempt %d, retrying once", attempt);
|
||||||
logWifiSnapshot("WiFi before updateProgress retry");
|
logWifiSnapshot("WiFi before updateProgress retry");
|
||||||
delay(400);
|
delay(400);
|
||||||
@@ -484,7 +532,9 @@ KOReaderSyncClient::Error KOReaderSyncClient::updateProgress(const KOReaderProgr
|
|||||||
|
|
||||||
const char* KOReaderSyncClient::lastFailureDetail() {
|
const char* KOReaderSyncClient::lastFailureDetail() {
|
||||||
const bool isUpload = (lastOperation && strcmp(lastOperation, "update progress") == 0);
|
const bool isUpload = (lastOperation && strcmp(lastOperation, "update progress") == 0);
|
||||||
const unsigned requiredContig = isUpload ? MIN_CONTIG_HEAP_FOR_TLS_UPLOAD : MIN_CONTIG_HEAP_FOR_TLS;
|
const bool hasReusableSession = g_keepSessionOpen && g_sessionClient != nullptr;
|
||||||
|
const unsigned requiredContig =
|
||||||
|
(isUpload && hasReusableSession) ? MIN_CONTIG_HEAP_FOR_TLS_UPLOAD : MIN_CONTIG_HEAP_FOR_TLS;
|
||||||
|
|
||||||
// Heap-pressure case: surfaced when checkHeapForTls() refused before any TCP/TLS work happened.
|
// Heap-pressure case: surfaced when checkHeapForTls() refused before any TCP/TLS work happened.
|
||||||
if (lastEspError == ESP_ERR_NO_MEM && lastHttpCode == 0) {
|
if (lastEspError == ESP_ERR_NO_MEM && lastHttpCode == 0) {
|
||||||
|
|||||||
@@ -112,5 +112,7 @@ class KOReaderSyncClient {
|
|||||||
* reports a heap-pressure message instead of attempting (and crashing) the TLS handshake.
|
* reports a heap-pressure message instead of attempting (and crashing) the TLS handshake.
|
||||||
*/
|
*/
|
||||||
static constexpr unsigned MIN_CONTIG_HEAP_FOR_TLS = 36 * 1024;
|
static constexpr unsigned MIN_CONTIG_HEAP_FOR_TLS = 36 * 1024;
|
||||||
|
// Relaxed threshold only for upload when reusing an already-established session.
|
||||||
|
// Uploads that must perform a fresh handshake still require MIN_CONTIG_HEAP_FOR_TLS.
|
||||||
static constexpr unsigned MIN_CONTIG_HEAP_FOR_TLS_UPLOAD = 34 * 1024;
|
static constexpr unsigned MIN_CONTIG_HEAP_FOR_TLS_UPLOAD = 34 * 1024;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -145,10 +145,12 @@ void EpubReaderActivity::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Long press CONFIRM (1s+) goes directly to KOReader sync when credentials are configured.
|
// 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.
|
// Without credentials, fall through to the regular menu on release.
|
||||||
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) &&
|
if (mappedInput.isPressed(MappedInputManager::Button::Confirm) &&
|
||||||
mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS && KOREADER_STORE.hasCredentials()) {
|
mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS && KOREADER_STORE.hasCredentials()) {
|
||||||
launchKOReaderSync();
|
launchKOReaderSync(SyncLaunchMode::COMPARE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,16 +460,26 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
|||||||
requestUpdate();
|
requestUpdate();
|
||||||
break;
|
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()) {
|
if (KOREADER_STORE.hasCredentials()) {
|
||||||
launchKOReaderSync();
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderActivity::launchKOReaderSync() {
|
void EpubReaderActivity::launchKOReaderSync(const SyncLaunchMode mode) {
|
||||||
if (!epub) {
|
if (!epub) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -499,10 +511,19 @@ void EpubReaderActivity::launchKOReaderSync() {
|
|||||||
LOG_DBG("ERS", "Pre-sync trim: spine=%d page=%d/%d heap=%lu", currentSpineIndex, currentPage, totalPages,
|
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()));
|
static_cast<unsigned long>(esp_get_free_heap_size()));
|
||||||
|
|
||||||
startActivityForResult(std::make_unique<KOReaderSyncActivity>(renderer, mappedInput, std::shared_ptr<Epub>{},
|
// Map reader-level launch mode to activity-level intent once, then pass a
|
||||||
syncEpubPath,
|
// stable intent into KOReaderSyncActivity so it can own the sync state machine.
|
||||||
currentSpineIndex, currentPage, totalPages),
|
KOReaderSyncActivity::SyncIntent syncIntent = KOReaderSyncActivity::SyncIntent::COMPARE;
|
||||||
[this](const ActivityResult& result) { handleSyncResult(result); });
|
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) {
|
void EpubReaderActivity::handleSyncResult(const ActivityResult& result) {
|
||||||
|
|||||||
@@ -9,6 +9,17 @@
|
|||||||
#include "activities/Activity.h"
|
#include "activities/Activity.h"
|
||||||
|
|
||||||
class EpubReaderActivity final : public Activity {
|
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::shared_ptr<Epub> epub;
|
||||||
std::unique_ptr<Section> section = nullptr;
|
std::unique_ptr<Section> section = nullptr;
|
||||||
int currentSpineIndex = 0;
|
int currentSpineIndex = 0;
|
||||||
@@ -58,7 +69,7 @@ class EpubReaderActivity final : public Activity {
|
|||||||
// Jump to a percentage of the book (0-100), mapping it to spine and page.
|
// Jump to a percentage of the book (0-100), mapping it to spine and page.
|
||||||
void jumpToPercent(int percent);
|
void jumpToPercent(int percent);
|
||||||
void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action);
|
void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action);
|
||||||
void launchKOReaderSync();
|
void launchKOReaderSync(SyncLaunchMode mode = SyncLaunchMode::COMPARE);
|
||||||
void handleSyncResult(const ActivityResult& result);
|
void handleSyncResult(const ActivityResult& result);
|
||||||
void applyOrientation(uint8_t orientation);
|
void applyOrientation(uint8_t orientation);
|
||||||
void toggleAutoPageTurn(uint8_t selectedPageTurnOption);
|
void toggleAutoPageTurn(uint8_t selectedPageTurnOption);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ EpubReaderMenuActivity::EpubReaderMenuActivity(GfxRenderer& renderer, MappedInpu
|
|||||||
|
|
||||||
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) {
|
std::vector<EpubReaderMenuActivity::MenuItem> EpubReaderMenuActivity::buildMenuItems(bool hasFootnotes) {
|
||||||
std::vector<MenuItem> items;
|
std::vector<MenuItem> items;
|
||||||
items.reserve(10);
|
items.reserve(12);
|
||||||
items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER});
|
items.push_back({MenuAction::SELECT_CHAPTER, StrId::STR_SELECT_CHAPTER});
|
||||||
if (hasFootnotes) {
|
if (hasFootnotes) {
|
||||||
items.push_back({MenuAction::FOOTNOTES, StrId::STR_FOOTNOTES});
|
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::DISPLAY_QR, StrId::STR_DISPLAY_QR});
|
||||||
items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON});
|
items.push_back({MenuAction::GO_HOME, StrId::STR_GO_HOME_BUTTON});
|
||||||
if (KOREADER_STORE.hasCredentials()) {
|
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});
|
items.push_back({MenuAction::DELETE_CACHE, StrId::STR_DELETE_CACHE});
|
||||||
return items;
|
return items;
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ class EpubReaderMenuActivity final : public Activity {
|
|||||||
SCREENSHOT,
|
SCREENSHOT,
|
||||||
DISPLAY_QR,
|
DISPLAY_QR,
|
||||||
GO_HOME,
|
GO_HOME,
|
||||||
SYNC,
|
PULL_REMOTE,
|
||||||
|
PUSH_LOCAL,
|
||||||
DELETE_CACHE
|
DELETE_CACHE
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#include "KOReaderSyncActivity.h"
|
#include "KOReaderSyncActivity.h"
|
||||||
|
|
||||||
|
#include <FontCacheManager.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
#include <HalClock.h>
|
#include <HalClock.h>
|
||||||
#include <I18n.h>
|
#include <I18n.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <FontCacheManager.h>
|
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <esp_heap_caps.h>
|
#include <esp_heap_caps.h>
|
||||||
#include <esp_system.h>
|
#include <esp_system.h>
|
||||||
@@ -116,19 +116,52 @@ void KOReaderSyncActivity::performSync() {
|
|||||||
|
|
||||||
LOG_DBG("KOSync", "Document hash: %s", documentHash.c_str());
|
LOG_DBG("KOSync", "Document hash: %s", documentHash.c_str());
|
||||||
|
|
||||||
// Precompute local mapping before first network request so the expensive
|
// Local mapping is only needed for compare/upload paths.
|
||||||
// inflate/index work happens before TLS. This avoids a second local mapping
|
// Pull-only mode can skip this expensive step and go straight to remote fetch.
|
||||||
// pass later and keeps the upload path lightweight.
|
if (syncIntent != SyncIntent::PULL_REMOTE) {
|
||||||
{
|
// Precompute local mapping before first network request so the expensive
|
||||||
RenderLock lock(*this);
|
// inflate/index work happens before TLS. This avoids a second local mapping
|
||||||
statusMessage = tr(STR_MAPPING_LOCAL);
|
// pass later and keeps the upload path lightweight.
|
||||||
|
{
|
||||||
|
RenderLock lock(*this);
|
||||||
|
statusMessage = tr(STR_MAPPING_LOCAL);
|
||||||
|
}
|
||||||
|
requestUpdateAndWait();
|
||||||
|
computeLocalProgressAndChapter();
|
||||||
}
|
}
|
||||||
requestUpdateAndWait();
|
|
||||||
computeLocalProgressAndChapter();
|
|
||||||
|
|
||||||
// Drop EPUB state before HTTPS to maximize contiguous heap for TLS.
|
// Drop EPUB state before HTTPS to maximize contiguous heap for TLS.
|
||||||
releaseEpubForMapping();
|
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);
|
RenderLock lock(*this);
|
||||||
statusMessage = tr(STR_FETCH_PROGRESS);
|
statusMessage = tr(STR_FETCH_PROGRESS);
|
||||||
@@ -143,6 +176,19 @@ void KOReaderSyncActivity::performSync() {
|
|||||||
const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress);
|
const auto result = KOReaderSyncClient::getProgress(documentHash, remoteProgress);
|
||||||
|
|
||||||
if (result == KOReaderSyncClient::NOT_FOUND) {
|
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.
|
// Keep session open so an immediate upload can reuse the same connection.
|
||||||
// No remote progress - offer to upload
|
// No remote progress - offer to upload
|
||||||
{
|
{
|
||||||
@@ -189,6 +235,34 @@ void KOReaderSyncActivity::performSync() {
|
|||||||
}
|
}
|
||||||
remoteChapterLabel = tr(STR_UNNAMED);
|
remoteChapterLabel = tr(STR_UNNAMED);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
// Local progress was precomputed before network; keep using the cached value.
|
||||||
releaseEpubForMapping();
|
releaseEpubForMapping();
|
||||||
|
|
||||||
@@ -242,8 +316,9 @@ void KOReaderSyncActivity::performUpload() {
|
|||||||
// that only appear on PUT due to allocator state changes.
|
// that only appear on PUT due to allocator state changes.
|
||||||
logSyncMemSnapshot("before_updateProgress");
|
logSyncMemSnapshot("before_updateProgress");
|
||||||
|
|
||||||
// Ensure a session exists for upload. When GET succeeded, this should reuse
|
// Ensure a session exists for upload. In compare flow this comes from the
|
||||||
// the existing connection and typically skip a second handshake.
|
// 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();
|
KOReaderSyncClient::beginPersistentSession();
|
||||||
|
|
||||||
KOReaderProgress progress;
|
KOReaderProgress progress;
|
||||||
@@ -341,7 +416,7 @@ void KOReaderSyncActivity::render(RenderLock&&) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state == SYNCING || state == UPLOADING) {
|
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();
|
renderer.displayBuffer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -422,6 +497,15 @@ void KOReaderSyncActivity::render(RenderLock&&) {
|
|||||||
return;
|
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) {
|
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, 280, tr(STR_SYNC_FAILED_MSG), true, EpdFontFamily::BOLD);
|
||||||
renderer.drawCenteredText(UI_10_FONT_ID, 320, statusMessage.c_str());
|
renderer.drawCenteredText(UI_10_FONT_ID, 320, statusMessage.c_str());
|
||||||
@@ -488,9 +572,9 @@ void KOReaderSyncActivity::computeLocalProgressAndChapter() {
|
|||||||
localProgress = ProgressMapper::toKOReader(epub, localPos);
|
localProgress = ProgressMapper::toKOReader(epub, localPos);
|
||||||
|
|
||||||
const int localTocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
|
const int localTocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex);
|
||||||
localChapterLabel =
|
localChapterLabel = (localTocIndex >= 0)
|
||||||
(localTocIndex >= 0) ? epub->getTocItem(localTocIndex).title
|
? epub->getTocItem(localTocIndex).title
|
||||||
: (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(currentSpineIndex + 1));
|
: (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(currentSpineIndex + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
void KOReaderSyncActivity::computeRemoteChapter() {
|
void KOReaderSyncActivity::computeRemoteChapter() {
|
||||||
@@ -498,20 +582,31 @@ void KOReaderSyncActivity::computeRemoteChapter() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const int remoteTocIndex = epub->getTocIndexForSpineIndex(remotePosition.spineIndex);
|
const int remoteTocIndex = epub->getTocIndexForSpineIndex(remotePosition.spineIndex);
|
||||||
remoteChapterLabel =
|
remoteChapterLabel = (remoteTocIndex >= 0)
|
||||||
(remoteTocIndex >= 0) ? epub->getTocItem(remoteTocIndex).title
|
? epub->getTocItem(remoteTocIndex).title
|
||||||
: (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(remotePosition.spineIndex + 1));
|
: (std::string(tr(STR_SECTION_PREFIX)) + std::to_string(remotePosition.spineIndex + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
void KOReaderSyncActivity::loop() {
|
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)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state == UPLOAD_COMPLETE && millis() - uploadCompleteTime >= 3000) {
|
if ((state == UPLOAD_COMPLETE || state == APPLY_COMPLETE) && millis() - uploadCompleteTime >= 3000) {
|
||||||
closeCancelled();
|
// Keep pull/apply result on auto-close; upload-complete remains cancel-style.
|
||||||
|
if (state == APPLY_COMPLETE) {
|
||||||
|
finish();
|
||||||
|
} else {
|
||||||
|
closeCancelled();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,19 +12,37 @@
|
|||||||
/**
|
/**
|
||||||
* Activity for syncing reading progress with KOReader sync server.
|
* Activity for syncing reading progress with KOReader sync server.
|
||||||
*
|
*
|
||||||
* Flow:
|
* Shared pipeline:
|
||||||
* 1. Connect to WiFi (if not connected)
|
* 1. Connect to WiFi (if not connected)
|
||||||
* 2. Calculate document hash
|
* 2. Optionally sync NTP (if stale)
|
||||||
* 3. Fetch remote progress
|
* 3. Calculate document hash
|
||||||
* 4. Show comparison and options (Apply/Upload)
|
*
|
||||||
* 5. Apply or upload progress
|
* 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 {
|
class KOReaderSyncActivity final : public Activity {
|
||||||
public:
|
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,
|
explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||||
const std::shared_ptr<Epub>& epub, const std::string& epubPath, int currentSpineIndex,
|
const std::shared_ptr<Epub>& epub, const std::string& epubPath, int currentSpineIndex,
|
||||||
int currentPage, int totalPagesInSpine, uint16_t paragraphIndex = 0,
|
int currentPage, int totalPagesInSpine, uint16_t paragraphIndex = 0,
|
||||||
bool hasParagraphIndex = false)
|
bool hasParagraphIndex = false, SyncIntent syncIntent = SyncIntent::COMPARE)
|
||||||
: Activity("KOReaderSync", renderer, mappedInput),
|
: Activity("KOReaderSync", renderer, mappedInput),
|
||||||
epub(epub),
|
epub(epub),
|
||||||
epubPath(epubPath),
|
epubPath(epubPath),
|
||||||
@@ -33,6 +51,7 @@ class KOReaderSyncActivity final : public Activity {
|
|||||||
totalPagesInSpine(totalPagesInSpine),
|
totalPagesInSpine(totalPagesInSpine),
|
||||||
localParagraphIndex(paragraphIndex),
|
localParagraphIndex(paragraphIndex),
|
||||||
hasLocalParagraphIndex(hasParagraphIndex),
|
hasLocalParagraphIndex(hasParagraphIndex),
|
||||||
|
syncIntent(syncIntent),
|
||||||
remoteProgress{},
|
remoteProgress{},
|
||||||
remotePosition{},
|
remotePosition{},
|
||||||
localProgress{} {}
|
localProgress{} {}
|
||||||
@@ -51,6 +70,7 @@ class KOReaderSyncActivity final : public Activity {
|
|||||||
SHOWING_RESULT,
|
SHOWING_RESULT,
|
||||||
UPLOADING,
|
UPLOADING,
|
||||||
UPLOAD_COMPLETE,
|
UPLOAD_COMPLETE,
|
||||||
|
APPLY_COMPLETE,
|
||||||
NO_REMOTE_PROGRESS,
|
NO_REMOTE_PROGRESS,
|
||||||
SYNC_FAILED,
|
SYNC_FAILED,
|
||||||
NO_CREDENTIALS
|
NO_CREDENTIALS
|
||||||
@@ -63,6 +83,7 @@ class KOReaderSyncActivity final : public Activity {
|
|||||||
int totalPagesInSpine;
|
int totalPagesInSpine;
|
||||||
uint16_t localParagraphIndex;
|
uint16_t localParagraphIndex;
|
||||||
bool hasLocalParagraphIndex;
|
bool hasLocalParagraphIndex;
|
||||||
|
SyncIntent syncIntent = SyncIntent::COMPARE;
|
||||||
|
|
||||||
State state = WIFI_SELECTION;
|
State state = WIFI_SELECTION;
|
||||||
std::string statusMessage;
|
std::string statusMessage;
|
||||||
@@ -82,7 +103,7 @@ class KOReaderSyncActivity final : public Activity {
|
|||||||
// Selection in result screen (0=Apply, 1=Upload)
|
// Selection in result screen (0=Apply, 1=Upload)
|
||||||
int selectedOption = 0;
|
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;
|
unsigned long uploadCompleteTime = 0;
|
||||||
bool closeRequested = false;
|
bool closeRequested = false;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user