crosspoint-reader/src/activities/settings/KOReaderAuthActivity.cpp
Justin Mitchell f69cddf2cc
Adds KOReader Sync support (#232)
## Summary

- Adds KOReader progress sync integration, allowing CrossPoint to sync
reading positions with other
KOReader-compatible devices
- Stores credentials securely with XOR obfuscation
- Uses KOReader's partial MD5 document hashing for cross-device book
matching
  - Syncs position via percentage with estimated XPath for compatibility

# Features
- Settings: KOReader Username, Password, and Authenticate options
- Sync from chapters menu: "Sync Progress" option appears when
credentials are configured
- Bidirectional sync: Can apply remote progress or upload local progress

---------

Co-authored-by: Dave Allie <dave@daveallie.com>
2026-01-19 11:55:35 +00:00

168 lines
4.6 KiB
C++

#include "KOReaderAuthActivity.h"
#include <GfxRenderer.h>
#include <WiFi.h>
#include "KOReaderCredentialStore.h"
#include "KOReaderSyncClient.h"
#include "MappedInputManager.h"
#include "activities/network/WifiSelectionActivity.h"
#include "fontIds.h"
void KOReaderAuthActivity::taskTrampoline(void* param) {
auto* self = static_cast<KOReaderAuthActivity*>(param);
self->displayTaskLoop();
}
void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) {
exitActivity();
if (!success) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
state = FAILED;
errorMessage = "WiFi connection failed";
xSemaphoreGive(renderingMutex);
updateRequired = true;
return;
}
xSemaphoreTake(renderingMutex, portMAX_DELAY);
state = AUTHENTICATING;
statusMessage = "Authenticating...";
xSemaphoreGive(renderingMutex);
updateRequired = true;
performAuthentication();
}
void KOReaderAuthActivity::performAuthentication() {
const auto result = KOReaderSyncClient::authenticate();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (result == KOReaderSyncClient::OK) {
state = SUCCESS;
statusMessage = "Successfully authenticated!";
} else {
state = FAILED;
errorMessage = KOReaderSyncClient::errorString(result);
}
xSemaphoreGive(renderingMutex);
updateRequired = true;
}
void KOReaderAuthActivity::onEnter() {
ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
xTaskCreate(&KOReaderAuthActivity::taskTrampoline, "KOAuthTask",
4096, // Stack size
this, // Parameters
1, // Priority
&displayTaskHandle // Task handle
);
// Turn on WiFi
WiFi.mode(WIFI_STA);
// Check if already connected
if (WiFi.status() == WL_CONNECTED) {
state = AUTHENTICATING;
statusMessage = "Authenticating...";
updateRequired = true;
// Perform authentication in a separate task
xTaskCreate(
[](void* param) {
auto* self = static_cast<KOReaderAuthActivity*>(param);
self->performAuthentication();
vTaskDelete(nullptr);
},
"AuthTask", 4096, this, 1, nullptr);
return;
}
// Launch WiFi selection
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
[this](const bool connected) { onWifiSelectionComplete(connected); }));
}
void KOReaderAuthActivity::onExit() {
ActivityWithSubactivity::onExit();
// Turn off wifi
WiFi.disconnect(false);
delay(100);
WiFi.mode(WIFI_OFF);
delay(100);
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void KOReaderAuthActivity::displayTaskLoop() {
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void KOReaderAuthActivity::render() {
if (subActivity) {
return;
}
renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Auth", true, EpdFontFamily::BOLD);
if (state == AUTHENTICATING) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD);
renderer.displayBuffer();
return;
}
if (state == SUCCESS) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Success!", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, "KOReader sync is ready to use");
const auto labels = mappedInput.mapLabels("Done", "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Authentication Failed", true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, errorMessage.c_str());
const auto labels = mappedInput.mapLabels("Back", "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer();
return;
}
}
void KOReaderAuthActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
if (state == SUCCESS || state == FAILED) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back) ||
mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
onComplete();
}
}
}