mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-04-29 10:26:52 -07:00
refactor: implement ActivityManager (#1016)
## Summary Ref comment: https://github.com/crosspoint-reader/crosspoint-reader/pull/1010#pullrequestreview-3828854640 This PR introduces `ActivityManager`, which mirrors the same concept of Activity in Android, where an activity represents a single screen of the UI. The manager is responsible for launching activities, and ensuring that only one activity is active at a time. Main differences from Android's ActivityManager: - No concept of Bundle or Intent extras - No onPause/onResume, since we don't have a concept of background activities - onActivityResult is implemented via a callback instead of a separate method, for simplicity ## Key changes - Single `renderTask` shared across all activities - No more sub-activity, we manage them using a stack; Results can be passed via `startActivityForResult` and `setResult` - Activity can call `finish()` to destroy themself, but the actual deletion will be handled by `ActivityManager` to avoid `delete this` pattern As a bonus: the manager will automatically call `requestUpdate()` when returning from another activity ## Example usage **BEFORE**: ```cpp // caller enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, [this](const bool connected) { onWifiSelectionComplete(connected); })); // subactivity onComplete(true); // will eventually call exitActivity(), which deletes the caller instance (dangerous behavior) ``` **AFTER**: (mirrors the `startActivityForResult` and `setResult` from android) ```cpp // caller startActivityForResult(new NetworkModeSelectionActivity(renderer, mappedInput), [this](const ActivityResult& result) { onNetworkModeSelected(result.selectedNetworkMode); }); // subactivity ActivityResult result; result.isCancelled = false; result.selectedNetworkMode = mode; setResult(result); finish(); // signals to ActivityManager to go back to last activity AFTER this function returns ``` TODO: - [x] Reconsider if the `Intent` is really necessary or it should be removed (note: it's inspired by [Intent](https://developer.android.com/guide/components/intents-common) from Android API) ==> I decided to keep this pattern fr clarity - [x] Verify if behavior is still correct (i.e. back from sub-activity) - [x] Refactor the `ActivityWithSubactivity` to just simple `Activity` --> We are using a stack for keeping track of sub-activity now - [x] Use single task for rendering --> avoid allocating 8KB stack per activity - [x] Implement the idea of [Activity result](https://developer.android.com/training/basics/intents/result) --> Allow sub-activity like Wifi to report back the status (connected, failed, etc) --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? **PARTIALLY**, some repetitive migrations are done by Claude, but I'm the one how ultimately approve it --------- Co-authored-by: Zach Nelson <zach@zdnelson.com>
This commit is contained in:
co-authored by
Zach Nelson
parent
5b11e45a36
commit
c4fc4effbd
+14
-47
@@ -1,61 +1,28 @@
|
||||
#include "Activity.h"
|
||||
|
||||
#include <HalPowerManager.h>
|
||||
#include "ActivityManager.h"
|
||||
|
||||
void Activity::renderTaskTrampoline(void* param) {
|
||||
auto* self = static_cast<Activity*>(param);
|
||||
self->renderTaskLoop();
|
||||
}
|
||||
void Activity::onEnter() { LOG_DBG("ACT", "Entering activity: %s", name.c_str()); }
|
||||
|
||||
void Activity::renderTaskLoop() {
|
||||
while (true) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
{
|
||||
HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering
|
||||
RenderLock lock(*this);
|
||||
render(std::move(lock));
|
||||
}
|
||||
}
|
||||
}
|
||||
void Activity::onExit() { LOG_DBG("ACT", "Exiting activity: %s", name.c_str()); }
|
||||
|
||||
void Activity::onEnter() {
|
||||
xTaskCreate(&renderTaskTrampoline, name.c_str(),
|
||||
8192, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&renderTaskHandle // Task handle
|
||||
);
|
||||
assert(renderTaskHandle != nullptr && "Failed to create render task");
|
||||
LOG_DBG("ACT", "Entering activity: %s", name.c_str());
|
||||
}
|
||||
|
||||
void Activity::onExit() {
|
||||
RenderLock lock(*this); // Ensure we don't delete the task while it's rendering
|
||||
if (renderTaskHandle) {
|
||||
vTaskDelete(renderTaskHandle);
|
||||
renderTaskHandle = nullptr;
|
||||
}
|
||||
|
||||
LOG_DBG("ACT", "Exiting activity: %s", name.c_str());
|
||||
}
|
||||
|
||||
void Activity::requestUpdate() {
|
||||
// Using direct notification to signal the render task to update
|
||||
// Increment counter so multiple rapid calls won't be lost
|
||||
if (renderTaskHandle) {
|
||||
xTaskNotify(renderTaskHandle, 1, eIncrement);
|
||||
}
|
||||
}
|
||||
void Activity::requestUpdate(bool immediate) { activityManager.requestUpdate(immediate); }
|
||||
|
||||
void Activity::requestUpdateAndWait() {
|
||||
// FIXME @ngxson : properly implement this using freeRTOS notification
|
||||
activityManager.requestUpdate(true);
|
||||
delay(100);
|
||||
}
|
||||
|
||||
// RenderLock
|
||||
void Activity::onGoHome() { activityManager.goHome(); }
|
||||
|
||||
Activity::RenderLock::RenderLock(Activity& activity) : activity(activity) {
|
||||
xSemaphoreTake(activity.renderingMutex, portMAX_DELAY);
|
||||
void Activity::onSelectBook(const std::string& path) { activityManager.goToReader(path); }
|
||||
|
||||
void Activity::startActivityForResult(std::unique_ptr<Activity>&& activity, ActivityResultHandler resultHandler) {
|
||||
this->resultHandler = std::move(resultHandler);
|
||||
activityManager.pushActivity(std::move(activity));
|
||||
}
|
||||
|
||||
Activity::RenderLock::~RenderLock() { xSemaphoreGive(activity.renderingMutex); }
|
||||
void Activity::setResult(ActivityResult&& result) { this->result = std::move(result); }
|
||||
|
||||
void Activity::finish() { activityManager.popActivity(); }
|
||||
|
||||
+28
-30
@@ -1,16 +1,16 @@
|
||||
#pragma once
|
||||
#include <HardwareSerial.h>
|
||||
#include <Logging.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "ActivityManager.h" // for using the ActivityManager singleton
|
||||
#include "ActivityResult.h"
|
||||
#include "GfxRenderer.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "RenderLock.h"
|
||||
|
||||
class Activity {
|
||||
protected:
|
||||
@@ -18,44 +18,42 @@ class Activity {
|
||||
GfxRenderer& renderer;
|
||||
MappedInputManager& mappedInput;
|
||||
|
||||
// Task to render and display the activity
|
||||
TaskHandle_t renderTaskHandle = nullptr;
|
||||
[[noreturn]] static void renderTaskTrampoline(void* param);
|
||||
[[noreturn]] virtual void renderTaskLoop();
|
||||
|
||||
// Mutex to protect rendering operations from being deleted mid-render
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
|
||||
public:
|
||||
ActivityResultHandler resultHandler;
|
||||
ActivityResult result;
|
||||
|
||||
explicit Activity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: name(std::move(name)), renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) {
|
||||
assert(renderingMutex != nullptr && "Failed to create rendering mutex");
|
||||
}
|
||||
virtual ~Activity() {
|
||||
vSemaphoreDelete(renderingMutex);
|
||||
renderingMutex = nullptr;
|
||||
};
|
||||
class RenderLock;
|
||||
: name(std::move(name)), renderer(renderer), mappedInput(mappedInput) {}
|
||||
virtual ~Activity() = default;
|
||||
virtual void onEnter();
|
||||
virtual void onExit();
|
||||
virtual void loop() {}
|
||||
|
||||
virtual void render(RenderLock&&) {}
|
||||
virtual void requestUpdate();
|
||||
|
||||
// If immediate is true, the update will be triggered immediately.
|
||||
// Otherwise, it will be deferred until the end of the current loop iteration.
|
||||
virtual void requestUpdate(bool immediate = false);
|
||||
|
||||
// Request an immediate render and block until it completes.
|
||||
virtual void requestUpdateAndWait();
|
||||
|
||||
virtual bool skipLoopDelay() { return false; }
|
||||
virtual bool preventAutoSleep() { return false; }
|
||||
virtual bool isReaderActivity() const { return false; }
|
||||
|
||||
// RAII helper to lock rendering mutex for the duration of a scope.
|
||||
class RenderLock {
|
||||
Activity& activity;
|
||||
// Start a new activity without destroying the current one
|
||||
// Note: requestUpdate() will be invoked automatically once resultHandler finishes
|
||||
void startActivityForResult(std::unique_ptr<Activity>&& activity, ActivityResultHandler resultHandler);
|
||||
|
||||
public:
|
||||
explicit RenderLock(Activity& activity);
|
||||
RenderLock(const RenderLock&) = delete;
|
||||
RenderLock& operator=(const RenderLock&) = delete;
|
||||
~RenderLock();
|
||||
};
|
||||
// Set the result to be passed back to the previous activity when this activity finishes
|
||||
void setResult(ActivityResult&& result);
|
||||
|
||||
// Finish this activity and return to the previous one on the stack (if any)
|
||||
void finish();
|
||||
|
||||
// Convenience method to facilitate API transition to ActivityManager
|
||||
// TODO: remove this in near future
|
||||
void onGoHome();
|
||||
void onSelectBook(const std::string& path);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
#include "ActivityManager.h"
|
||||
|
||||
#include <HalPowerManager.h>
|
||||
|
||||
#include "boot_sleep/BootActivity.h"
|
||||
#include "boot_sleep/SleepActivity.h"
|
||||
#include "browser/OpdsBookBrowserActivity.h"
|
||||
#include "home/HomeActivity.h"
|
||||
#include "home/MyLibraryActivity.h"
|
||||
#include "home/RecentBooksActivity.h"
|
||||
#include "network/CrossPointWebServerActivity.h"
|
||||
#include "reader/ReaderActivity.h"
|
||||
#include "settings/SettingsActivity.h"
|
||||
#include "util/FullScreenMessageActivity.h"
|
||||
|
||||
void ActivityManager::begin() {
|
||||
xTaskCreate(&renderTaskTrampoline, "ActivityManagerRender",
|
||||
8192, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&renderTaskHandle // Task handle
|
||||
);
|
||||
assert(renderTaskHandle != nullptr && "Failed to create render task");
|
||||
}
|
||||
|
||||
void ActivityManager::renderTaskTrampoline(void* param) {
|
||||
auto* self = static_cast<ActivityManager*>(param);
|
||||
self->renderTaskLoop();
|
||||
}
|
||||
|
||||
void ActivityManager::renderTaskLoop() {
|
||||
while (true) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
// Acquire the lock before reading currentActivity to avoid a TOCTOU race
|
||||
// where the main task deletes the activity between the null-check and render().
|
||||
RenderLock lock;
|
||||
if (currentActivity) {
|
||||
HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering
|
||||
currentActivity->render(std::move(lock));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityManager::loop() {
|
||||
if (currentActivity) {
|
||||
// Note: do not hold a lock here, the loop() method must be responsible for acquire one if needed
|
||||
currentActivity->loop();
|
||||
}
|
||||
|
||||
while (pendingAction != PendingAction::None) {
|
||||
if (pendingAction == PendingAction::Pop) {
|
||||
RenderLock lock;
|
||||
|
||||
if (!currentActivity) {
|
||||
// Should never happen in practice
|
||||
LOG_ERR("ACT", "Pop set but currentActivity is null; ignoring pop request");
|
||||
pendingAction = PendingAction::None;
|
||||
continue;
|
||||
}
|
||||
|
||||
ActivityResult pendingResult = std::move(currentActivity->result);
|
||||
|
||||
// Destroy the current activity
|
||||
exitActivity(lock);
|
||||
pendingAction = PendingAction::None;
|
||||
|
||||
if (stackActivities.empty()) {
|
||||
LOG_DBG("ACT", "No more activities on stack, going home");
|
||||
lock.unlock(); // goHome may acquire its own lock
|
||||
goHome();
|
||||
continue; // Will launch goHome immediately
|
||||
|
||||
} else {
|
||||
currentActivity = std::move(stackActivities.back());
|
||||
stackActivities.pop_back();
|
||||
LOG_DBG("ACT", "Popped from activity stack, new size = %zu", stackActivities.size());
|
||||
// Handle result if necessary
|
||||
if (currentActivity->resultHandler) {
|
||||
LOG_DBG("ACT", "Handling result for popped activity");
|
||||
|
||||
// Move it here to avoid the case where handler calling another startActivityForResult()
|
||||
auto handler = std::move(currentActivity->resultHandler);
|
||||
currentActivity->resultHandler = nullptr;
|
||||
lock.unlock(); // Handler may acquire its own lock
|
||||
handler(pendingResult);
|
||||
}
|
||||
|
||||
// Request an update to ensure the popped activity gets re-rendered
|
||||
if (pendingAction == PendingAction::None) {
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
// Handler may request another pending action, we will handle it in the next loop iteration
|
||||
continue;
|
||||
}
|
||||
|
||||
} else if (pendingActivity) {
|
||||
// Current activity has requested a new activity to be launched
|
||||
RenderLock lock;
|
||||
|
||||
if (pendingAction == PendingAction::Replace) {
|
||||
// Destroy the current activity
|
||||
exitActivity(lock);
|
||||
// Clear the stack
|
||||
while (!stackActivities.empty()) {
|
||||
stackActivities.back()->onExit();
|
||||
stackActivities.pop_back();
|
||||
}
|
||||
} else if (pendingAction == PendingAction::Push) {
|
||||
// Move current activity to stack
|
||||
stackActivities.push_back(std::move(currentActivity));
|
||||
LOG_DBG("ACT", "Pushed to activity stack, new size = %zu", stackActivities.size());
|
||||
}
|
||||
pendingAction = PendingAction::None;
|
||||
currentActivity = std::move(pendingActivity);
|
||||
|
||||
lock.unlock(); // onEnter may acquire its own lock
|
||||
currentActivity->onEnter();
|
||||
|
||||
// onEnter may request another pending action, we will handle it in the next loop iteration
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (requestedUpdate) {
|
||||
requestedUpdate = false;
|
||||
// Using direct notification to signal the render task to update
|
||||
// Increment counter so multiple rapid calls won't be lost
|
||||
if (renderTaskHandle) {
|
||||
xTaskNotify(renderTaskHandle, 1, eIncrement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityManager::exitActivity(const RenderLock& lock) {
|
||||
// Note: lock must be held by the caller
|
||||
if (currentActivity) {
|
||||
currentActivity->onExit();
|
||||
currentActivity.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityManager::replaceActivity(std::unique_ptr<Activity>&& newActivity) {
|
||||
// Note: no lock here, this is usually called by loop() and we may run into deadlock
|
||||
if (currentActivity) {
|
||||
// Defer launch if we're currently in an activity, to avoid deleting the current activity
|
||||
// leading to the "delete this" problem
|
||||
pendingActivity = std::move(newActivity);
|
||||
pendingAction = PendingAction::Replace;
|
||||
} else {
|
||||
// No current activity, safe to launch immediately
|
||||
currentActivity = std::move(newActivity);
|
||||
currentActivity->onEnter();
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityManager::goToFileTransfer() {
|
||||
replaceActivity(std::make_unique<CrossPointWebServerActivity>(renderer, mappedInput));
|
||||
}
|
||||
|
||||
void ActivityManager::goToSettings() { replaceActivity(std::make_unique<SettingsActivity>(renderer, mappedInput)); }
|
||||
|
||||
void ActivityManager::goToMyLibrary(std::string path) {
|
||||
replaceActivity(std::make_unique<MyLibraryActivity>(renderer, mappedInput, std::move(path)));
|
||||
}
|
||||
|
||||
void ActivityManager::goToRecentBooks() {
|
||||
replaceActivity(std::make_unique<RecentBooksActivity>(renderer, mappedInput));
|
||||
}
|
||||
|
||||
void ActivityManager::goToBrowser() {
|
||||
replaceActivity(std::make_unique<OpdsBookBrowserActivity>(renderer, mappedInput));
|
||||
}
|
||||
|
||||
void ActivityManager::goToReader(std::string path) {
|
||||
replaceActivity(std::make_unique<ReaderActivity>(renderer, mappedInput, std::move(path)));
|
||||
}
|
||||
|
||||
void ActivityManager::goToSleep() {
|
||||
replaceActivity(std::make_unique<SleepActivity>(renderer, mappedInput));
|
||||
loop(); // Important: sleep screen must be rendered immediately, the caller will go to sleep right after this returns
|
||||
}
|
||||
|
||||
void ActivityManager::goToBoot() { replaceActivity(std::make_unique<BootActivity>(renderer, mappedInput)); }
|
||||
|
||||
void ActivityManager::goToFullScreenMessage(std::string message, EpdFontFamily::Style style) {
|
||||
replaceActivity(std::make_unique<FullScreenMessageActivity>(renderer, mappedInput, std::move(message), style));
|
||||
}
|
||||
|
||||
void ActivityManager::goHome() { replaceActivity(std::make_unique<HomeActivity>(renderer, mappedInput)); }
|
||||
|
||||
void ActivityManager::pushActivity(std::unique_ptr<Activity>&& activity) {
|
||||
if (pendingActivity) {
|
||||
// Should never happen in practice
|
||||
LOG_ERR("ACT", "pendingActivity while pushActivity is not expected");
|
||||
pendingActivity.reset();
|
||||
}
|
||||
pendingActivity = std::move(activity);
|
||||
pendingAction = PendingAction::Push;
|
||||
}
|
||||
|
||||
void ActivityManager::popActivity() {
|
||||
if (pendingActivity) {
|
||||
// Should never happen in practice
|
||||
LOG_ERR("ACT", "pendingActivity while popActivity is not expected");
|
||||
pendingActivity.reset();
|
||||
}
|
||||
pendingAction = PendingAction::Pop;
|
||||
}
|
||||
|
||||
bool ActivityManager::preventAutoSleep() const { return currentActivity && currentActivity->preventAutoSleep(); }
|
||||
|
||||
bool ActivityManager::isReaderActivity() const { return currentActivity && currentActivity->isReaderActivity(); }
|
||||
|
||||
bool ActivityManager::skipLoopDelay() const { return currentActivity && currentActivity->skipLoopDelay(); }
|
||||
|
||||
void ActivityManager::requestUpdate(bool immediate) {
|
||||
if (immediate) {
|
||||
if (renderTaskHandle) {
|
||||
xTaskNotify(renderTaskHandle, 1, eIncrement);
|
||||
}
|
||||
} else {
|
||||
// Deferring the update until current loop is finished
|
||||
// This is to avoid multiple updates being requested in the same loop
|
||||
requestedUpdate = true;
|
||||
}
|
||||
}
|
||||
// RenderLock
|
||||
|
||||
RenderLock::RenderLock() {
|
||||
xSemaphoreTake(activityManager.renderingMutex, portMAX_DELAY);
|
||||
isLocked = true;
|
||||
}
|
||||
|
||||
RenderLock::RenderLock(Activity& /* unused */) {
|
||||
xSemaphoreTake(activityManager.renderingMutex, portMAX_DELAY);
|
||||
isLocked = true;
|
||||
}
|
||||
|
||||
RenderLock::~RenderLock() {
|
||||
if (isLocked) {
|
||||
xSemaphoreGive(activityManager.renderingMutex);
|
||||
isLocked = false;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderLock::unlock() {
|
||||
if (isLocked) {
|
||||
xSemaphoreGive(activityManager.renderingMutex);
|
||||
isLocked = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "GfxRenderer.h"
|
||||
#include "MappedInputManager.h"
|
||||
#include "RenderLock.h"
|
||||
|
||||
class Activity; // forward declaration
|
||||
class RenderLock; // forward declaration
|
||||
|
||||
/**
|
||||
* ActivityManager
|
||||
*
|
||||
* This mirrors the same concept of Activity in Android, where an activity represents a single screen of the UI. The
|
||||
* manager is responsible for launching activities, and ensuring that only one activity is active at a time.
|
||||
*
|
||||
* It also provides a stack mechanism to allow activities to launch sub-activities and get back the results when the
|
||||
* sub-activity is done. For example, the WebServer activity can launch a WifiSelect activity to let the user choose a
|
||||
* wifi network, and get back the selected network when the user is done.
|
||||
*
|
||||
* Main differences from Android's ActivityManager:
|
||||
* - No onPause/onResume, since we don't have a concept of background activities
|
||||
* - onActivityResult is implemented via a callback instead of a separate method, for simplicity
|
||||
*/
|
||||
class ActivityManager {
|
||||
protected:
|
||||
GfxRenderer& renderer;
|
||||
MappedInputManager& mappedInput;
|
||||
std::vector<std::unique_ptr<Activity>> stackActivities;
|
||||
std::unique_ptr<Activity> currentActivity;
|
||||
|
||||
void exitActivity(const RenderLock& lock);
|
||||
|
||||
// Pending activity to be launched on next loop iteration
|
||||
std::unique_ptr<Activity> pendingActivity;
|
||||
enum class PendingAction { None, Push, Pop, Replace };
|
||||
PendingAction pendingAction = PendingAction::None;
|
||||
|
||||
// Task to render and display the activity
|
||||
TaskHandle_t renderTaskHandle = nullptr;
|
||||
static void renderTaskTrampoline(void* param);
|
||||
[[noreturn]] virtual void renderTaskLoop();
|
||||
|
||||
// Whether to trigger a render after the current loop()
|
||||
// This variable must only be set by the main loop, to avoid race conditions
|
||||
bool requestedUpdate = false;
|
||||
|
||||
public:
|
||||
explicit ActivityManager(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: renderer(renderer), mappedInput(mappedInput), renderingMutex(xSemaphoreCreateMutex()) {
|
||||
assert(renderingMutex != nullptr && "Failed to create rendering mutex");
|
||||
stackActivities.reserve(10);
|
||||
}
|
||||
~ActivityManager() { assert(false); /* should never be called */ };
|
||||
|
||||
// Mutex to protect rendering operations from race conditions
|
||||
// Must only be used via RenderLock
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
|
||||
void begin();
|
||||
void loop();
|
||||
|
||||
// Will replace currentActivity and drop all activities on stack
|
||||
void replaceActivity(std::unique_ptr<Activity>&& newActivity);
|
||||
|
||||
// goTo... functions are convenient wrapper for replaceActivity()
|
||||
void goToFileTransfer();
|
||||
void goToSettings();
|
||||
void goToMyLibrary(std::string path = {});
|
||||
void goToRecentBooks();
|
||||
void goToBrowser();
|
||||
void goToReader(std::string path);
|
||||
void goToSleep();
|
||||
void goToBoot();
|
||||
void goToFullScreenMessage(std::string message, EpdFontFamily::Style style = EpdFontFamily::REGULAR);
|
||||
void goHome();
|
||||
|
||||
// This will move current activity to stack instead of deleting it
|
||||
void pushActivity(std::unique_ptr<Activity>&& activity);
|
||||
|
||||
// Remove the currentActivity, returning the last one on stack
|
||||
// Note: if popActivity() on last activity on the stack, we will goHome()
|
||||
void popActivity();
|
||||
|
||||
bool preventAutoSleep() const;
|
||||
bool isReaderActivity() const;
|
||||
bool skipLoopDelay() const;
|
||||
|
||||
// If immediate is true, the update will be triggered immediately.
|
||||
// Otherwise, it will be deferred until the end of the current loop iteration.
|
||||
void requestUpdate(bool immediate = false);
|
||||
};
|
||||
|
||||
extern ActivityManager activityManager; // singleton, to be defined in main.cpp
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
struct WifiResult {
|
||||
bool connected = false;
|
||||
std::string ssid;
|
||||
std::string ip;
|
||||
};
|
||||
|
||||
struct KeyboardResult {
|
||||
std::string text;
|
||||
};
|
||||
|
||||
struct MenuResult {
|
||||
int action = -1;
|
||||
uint8_t orientation = 0;
|
||||
};
|
||||
|
||||
struct ChapterResult {
|
||||
int spineIndex = 0;
|
||||
};
|
||||
|
||||
struct PercentResult {
|
||||
int percent = 0;
|
||||
};
|
||||
|
||||
struct PageResult {
|
||||
uint32_t page = 0;
|
||||
};
|
||||
|
||||
struct SyncResult {
|
||||
int spineIndex = 0;
|
||||
int page = 0;
|
||||
};
|
||||
|
||||
enum class NetworkMode;
|
||||
|
||||
struct NetworkModeResult {
|
||||
NetworkMode mode;
|
||||
};
|
||||
|
||||
struct FootnoteResult {
|
||||
std::string href;
|
||||
};
|
||||
|
||||
using ResultVariant = std::variant<std::monostate, WifiResult, KeyboardResult, MenuResult, ChapterResult, PercentResult,
|
||||
PageResult, SyncResult, NetworkModeResult, FootnoteResult>;
|
||||
|
||||
struct ActivityResult {
|
||||
bool isCancelled = false;
|
||||
ResultVariant data;
|
||||
|
||||
explicit ActivityResult() = default;
|
||||
|
||||
template <typename ResultType, typename = std::enable_if_t<std::is_constructible_v<ResultVariant, ResultType&&>>>
|
||||
// cppcheck-suppress noExplicitConstructor
|
||||
ActivityResult(ResultType&& result) : data{std::forward<ResultType>(result)} {}
|
||||
};
|
||||
|
||||
using ActivityResultHandler = std::function<void(const ActivityResult&)>;
|
||||
@@ -1,53 +0,0 @@
|
||||
#include "ActivityWithSubactivity.h"
|
||||
|
||||
#include <HalPowerManager.h>
|
||||
|
||||
void ActivityWithSubactivity::renderTaskLoop() {
|
||||
while (true) {
|
||||
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
|
||||
{
|
||||
HalPowerManager::Lock powerLock; // Ensure we don't go into low-power mode while rendering
|
||||
RenderLock lock(*this);
|
||||
if (!subActivity) {
|
||||
render(std::move(lock));
|
||||
}
|
||||
// If subActivity is set, consume the notification but skip parent render
|
||||
// Note: the sub-activity will call its render() from its own display task
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityWithSubactivity::exitActivity() {
|
||||
// No need to lock, since onExit() already acquires its own lock
|
||||
if (subActivity) {
|
||||
LOG_DBG("ACT", "Exiting subactivity...");
|
||||
subActivity->onExit();
|
||||
subActivity.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityWithSubactivity::enterNewActivity(Activity* activity) {
|
||||
// Acquire lock to avoid 2 activities rendering at the same time during transition
|
||||
RenderLock lock(*this);
|
||||
subActivity.reset(activity);
|
||||
subActivity->onEnter();
|
||||
}
|
||||
|
||||
void ActivityWithSubactivity::loop() {
|
||||
if (subActivity) {
|
||||
subActivity->loop();
|
||||
}
|
||||
}
|
||||
|
||||
void ActivityWithSubactivity::requestUpdate() {
|
||||
if (!subActivity) {
|
||||
Activity::requestUpdate();
|
||||
}
|
||||
// Sub-activity should call their own requestUpdate() from their loop() function
|
||||
}
|
||||
|
||||
void ActivityWithSubactivity::onExit() {
|
||||
// No need to lock, onExit() already acquires its own lock
|
||||
exitActivity();
|
||||
Activity::onExit();
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
#include <memory>
|
||||
|
||||
#include "Activity.h"
|
||||
|
||||
class ActivityWithSubactivity : public Activity {
|
||||
protected:
|
||||
std::unique_ptr<Activity> subActivity = nullptr;
|
||||
void exitActivity();
|
||||
void enterNewActivity(Activity* activity);
|
||||
[[noreturn]] void renderTaskLoop() override;
|
||||
|
||||
public:
|
||||
explicit ActivityWithSubactivity(std::string name, GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: Activity(std::move(name), renderer, mappedInput) {}
|
||||
void loop() override;
|
||||
// Note: when a subactivity is active, parent requestUpdate() calls are ignored;
|
||||
// the subactivity should request its own renders. This pauses parent rendering until exit.
|
||||
void requestUpdate() override;
|
||||
void onExit() override;
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
class Activity; // forward declaration
|
||||
|
||||
// RAII helper to lock rendering mutex for the duration of a scope.
|
||||
class RenderLock {
|
||||
bool isLocked = false;
|
||||
|
||||
public:
|
||||
explicit RenderLock();
|
||||
explicit RenderLock(Activity&); // unused for now, but keep for compatibility
|
||||
RenderLock(const RenderLock&) = delete;
|
||||
RenderLock& operator=(const RenderLock&) = delete;
|
||||
~RenderLock();
|
||||
void unlock();
|
||||
};
|
||||
@@ -21,7 +21,7 @@ constexpr int PAGE_ITEMS = 23;
|
||||
} // namespace
|
||||
|
||||
void OpdsBookBrowserActivity::onEnter() {
|
||||
ActivityWithSubactivity::onEnter();
|
||||
Activity::onEnter();
|
||||
|
||||
state = BrowserState::CHECK_WIFI;
|
||||
entries.clear();
|
||||
@@ -37,7 +37,7 @@ void OpdsBookBrowserActivity::onEnter() {
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::onExit() {
|
||||
ActivityWithSubactivity::onExit();
|
||||
Activity::onExit();
|
||||
|
||||
// Turn off WiFi when exiting
|
||||
WiFi.mode(WIFI_OFF);
|
||||
@@ -49,7 +49,7 @@ void OpdsBookBrowserActivity::onExit() {
|
||||
void OpdsBookBrowserActivity::loop() {
|
||||
// Handle WiFi selection subactivity
|
||||
if (state == BrowserState::WIFI_SELECTION) {
|
||||
ActivityWithSubactivity::loop();
|
||||
// Should already handled by the WifiSelectionActivity
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ void OpdsBookBrowserActivity::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::render(Activity::RenderLock&&) {
|
||||
void OpdsBookBrowserActivity::render(RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
@@ -279,7 +279,7 @@ void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) {
|
||||
statusMessage = tr(STR_LOADING);
|
||||
entries.clear();
|
||||
selectorIndex = 0;
|
||||
requestUpdate();
|
||||
requestUpdate(true); // Force update to show loading state immediately before fetch
|
||||
|
||||
fetchFeed(currentPath);
|
||||
}
|
||||
@@ -308,7 +308,7 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
||||
statusMessage = book.title;
|
||||
downloadProgress = 0;
|
||||
downloadTotal = 0;
|
||||
requestUpdate();
|
||||
requestUpdate(true);
|
||||
|
||||
// Build full download URL
|
||||
std::string downloadUrl = UrlUtils::buildUrl(SETTINGS.opdsServerUrl, book.href);
|
||||
@@ -326,7 +326,7 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
|
||||
HttpDownloader::downloadToFile(downloadUrl, filename, [this](const size_t downloaded, const size_t total) {
|
||||
downloadProgress = downloaded;
|
||||
downloadTotal = total;
|
||||
requestUpdate();
|
||||
requestUpdate(true); // Force update to refresh progress bar
|
||||
});
|
||||
|
||||
if (result == HttpDownloader::OK) {
|
||||
@@ -364,18 +364,16 @@ void OpdsBookBrowserActivity::launchWifiSelection() {
|
||||
state = BrowserState::WIFI_SELECTION;
|
||||
requestUpdate();
|
||||
|
||||
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
||||
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
||||
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
|
||||
[this](const ActivityResult& result) { onWifiSelectionComplete(!result.isCancelled); });
|
||||
}
|
||||
|
||||
void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) {
|
||||
exitActivity();
|
||||
|
||||
if (connected) {
|
||||
LOG_DBG("OPDS", "WiFi connected via selection, fetching feed");
|
||||
state = BrowserState::LOADING;
|
||||
statusMessage = tr(STR_LOADING);
|
||||
requestUpdate();
|
||||
requestUpdate(true); // Force update to show loading state immediately before fetch
|
||||
fetchFeed(currentPath);
|
||||
} else {
|
||||
LOG_DBG("OPDS", "WiFi selection cancelled/failed");
|
||||
@@ -385,6 +383,5 @@ void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) {
|
||||
WiFi.mode(WIFI_OFF);
|
||||
state = BrowserState::ERROR;
|
||||
errorMessage = tr(STR_WIFI_CONN_FAILED);
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "../ActivityWithSubactivity.h"
|
||||
#include "../Activity.h"
|
||||
#include "util/ButtonNavigator.h"
|
||||
|
||||
/**
|
||||
@@ -13,7 +13,7 @@
|
||||
* Supports navigation through catalog hierarchy and downloading EPUBs.
|
||||
* When WiFi connection fails, launches WiFi selection to let user connect.
|
||||
*/
|
||||
class OpdsBookBrowserActivity final : public ActivityWithSubactivity {
|
||||
class OpdsBookBrowserActivity final : public Activity {
|
||||
public:
|
||||
enum class BrowserState {
|
||||
CHECK_WIFI, // Checking WiFi connection
|
||||
@@ -24,14 +24,13 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity {
|
||||
ERROR // Error state with message
|
||||
};
|
||||
|
||||
explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onGoHome)
|
||||
: ActivityWithSubactivity("OpdsBookBrowser", renderer, mappedInput), onGoHome(onGoHome) {}
|
||||
explicit OpdsBookBrowserActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: Activity("OpdsBookBrowser", renderer, mappedInput) {}
|
||||
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(Activity::RenderLock&&) override;
|
||||
void render(RenderLock&&) override;
|
||||
|
||||
private:
|
||||
ButtonNavigator buttonNavigator;
|
||||
@@ -45,8 +44,6 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity {
|
||||
size_t downloadProgress = 0;
|
||||
size_t downloadTotal = 0;
|
||||
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
void checkAndConnectWifi();
|
||||
void launchWifiSelection();
|
||||
void onWifiSelectionComplete(bool connected);
|
||||
|
||||
@@ -211,7 +211,7 @@ void HomeActivity::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void HomeActivity::render(Activity::RenderLock&&) {
|
||||
void HomeActivity::render(RenderLock&&) {
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
@@ -258,3 +258,15 @@ void HomeActivity::render(Activity::RenderLock&&) {
|
||||
loadRecentCovers(metrics.homeCoverHeight);
|
||||
}
|
||||
}
|
||||
|
||||
void HomeActivity::onSelectBook(const std::string& path) { activityManager.goToReader(path); }
|
||||
|
||||
void HomeActivity::onMyLibraryOpen() { activityManager.goToMyLibrary(); }
|
||||
|
||||
void HomeActivity::onRecentsOpen() { activityManager.goToRecentBooks(); }
|
||||
|
||||
void HomeActivity::onSettingsOpen() { activityManager.goToSettings(); }
|
||||
|
||||
void HomeActivity::onFileTransferOpen() { activityManager.goToFileTransfer(); }
|
||||
|
||||
void HomeActivity::onOpdsBrowserOpen() { activityManager.goToBrowser(); }
|
||||
|
||||
@@ -20,12 +20,12 @@ class HomeActivity final : public Activity {
|
||||
bool coverBufferStored = false; // Track if cover buffer is stored
|
||||
uint8_t* coverBuffer = nullptr; // HomeActivity's own buffer for cover image
|
||||
std::vector<RecentBook> recentBooks;
|
||||
const std::function<void(const std::string& path)> onSelectBook;
|
||||
const std::function<void()> onMyLibraryOpen;
|
||||
const std::function<void()> onRecentsOpen;
|
||||
const std::function<void()> onSettingsOpen;
|
||||
const std::function<void()> onFileTransferOpen;
|
||||
const std::function<void()> onOpdsBrowserOpen;
|
||||
void onSelectBook(const std::string& path);
|
||||
void onMyLibraryOpen();
|
||||
void onRecentsOpen();
|
||||
void onSettingsOpen();
|
||||
void onFileTransferOpen();
|
||||
void onOpdsBrowserOpen();
|
||||
|
||||
int getMenuItemCount() const;
|
||||
bool storeCoverBuffer(); // Store frame buffer for cover image
|
||||
@@ -35,20 +35,10 @@ class HomeActivity final : public Activity {
|
||||
void loadRecentCovers(int coverHeight);
|
||||
|
||||
public:
|
||||
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void(const std::string& path)>& onSelectBook,
|
||||
const std::function<void()>& onMyLibraryOpen, const std::function<void()>& onRecentsOpen,
|
||||
const std::function<void()>& onSettingsOpen, const std::function<void()>& onFileTransferOpen,
|
||||
const std::function<void()>& onOpdsBrowserOpen)
|
||||
: Activity("Home", renderer, mappedInput),
|
||||
onSelectBook(onSelectBook),
|
||||
onMyLibraryOpen(onMyLibraryOpen),
|
||||
onRecentsOpen(onRecentsOpen),
|
||||
onSettingsOpen(onSettingsOpen),
|
||||
onFileTransferOpen(onFileTransferOpen),
|
||||
onOpdsBrowserOpen(onOpdsBrowserOpen) {}
|
||||
explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: Activity("Home", renderer, mappedInput) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(Activity::RenderLock&&) override;
|
||||
void render(RenderLock&&) override;
|
||||
};
|
||||
|
||||
@@ -196,7 +196,7 @@ std::string getFileName(std::string filename) {
|
||||
return filename.substr(0, pos);
|
||||
}
|
||||
|
||||
void MyLibraryActivity::render(Activity::RenderLock&&) {
|
||||
void MyLibraryActivity::render(RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
|
||||
@@ -17,25 +17,15 @@ class MyLibraryActivity final : public Activity {
|
||||
std::string basepath = "/";
|
||||
std::vector<std::string> files;
|
||||
|
||||
// Callbacks
|
||||
const std::function<void(const std::string& path)> onSelectBook;
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
// Data loading
|
||||
void loadFiles();
|
||||
size_t findEntry(const std::string& name) const;
|
||||
|
||||
public:
|
||||
explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onGoHome,
|
||||
const std::function<void(const std::string& path)>& onSelectBook,
|
||||
std::string initialPath = "/")
|
||||
: Activity("MyLibrary", renderer, mappedInput),
|
||||
basepath(initialPath.empty() ? "/" : std::move(initialPath)),
|
||||
onSelectBook(onSelectBook),
|
||||
onGoHome(onGoHome) {}
|
||||
explicit MyLibraryActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string initialPath = "/")
|
||||
: Activity("MyLibrary", renderer, mappedInput), basepath(initialPath.empty() ? "/" : std::move(initialPath)) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(Activity::RenderLock&&) override;
|
||||
void render(RenderLock&&) override;
|
||||
};
|
||||
|
||||
@@ -83,7 +83,7 @@ void RecentBooksActivity::loop() {
|
||||
});
|
||||
}
|
||||
|
||||
void RecentBooksActivity::render(Activity::RenderLock&&) {
|
||||
void RecentBooksActivity::render(RenderLock&&) {
|
||||
renderer.clearScreen();
|
||||
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
|
||||
@@ -18,20 +18,14 @@ class RecentBooksActivity final : public Activity {
|
||||
// Recent tab state
|
||||
std::vector<RecentBook> recentBooks;
|
||||
|
||||
// Callbacks
|
||||
const std::function<void(const std::string& path)> onSelectBook;
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
// Data loading
|
||||
void loadRecentBooks();
|
||||
|
||||
public:
|
||||
explicit RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onGoHome,
|
||||
const std::function<void(const std::string& path)>& onSelectBook)
|
||||
: Activity("RecentBooks", renderer, mappedInput), onSelectBook(onSelectBook), onGoHome(onGoHome) {}
|
||||
explicit RecentBooksActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: Activity("RecentBooks", renderer, mappedInput) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(Activity::RenderLock&&) override;
|
||||
void render(RenderLock&&) override;
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ constexpr const char* HOSTNAME = "crosspoint";
|
||||
} // namespace
|
||||
|
||||
void CalibreConnectActivity::onEnter() {
|
||||
ActivityWithSubactivity::onEnter();
|
||||
Activity::onEnter();
|
||||
|
||||
requestUpdate();
|
||||
state = CalibreConnectState::WIFI_SELECTION;
|
||||
@@ -32,8 +32,15 @@ void CalibreConnectActivity::onEnter() {
|
||||
exitRequested = false;
|
||||
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
||||
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
||||
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
const auto& wifi = std::get<WifiResult>(result.data);
|
||||
connectedIP = wifi.ip;
|
||||
connectedSSID = wifi.ssid;
|
||||
}
|
||||
onWifiSelectionComplete(!result.isCancelled);
|
||||
});
|
||||
} else {
|
||||
connectedIP = WiFi.localIP().toString().c_str();
|
||||
connectedSSID = WiFi.SSID().c_str();
|
||||
@@ -42,7 +49,7 @@ void CalibreConnectActivity::onEnter() {
|
||||
}
|
||||
|
||||
void CalibreConnectActivity::onExit() {
|
||||
ActivityWithSubactivity::onExit();
|
||||
Activity::onExit();
|
||||
|
||||
stopWebServer();
|
||||
MDNS.end();
|
||||
@@ -56,18 +63,10 @@ void CalibreConnectActivity::onExit() {
|
||||
|
||||
void CalibreConnectActivity::onWifiSelectionComplete(const bool connected) {
|
||||
if (!connected) {
|
||||
exitActivity();
|
||||
onComplete();
|
||||
activityManager.popActivity();
|
||||
return;
|
||||
}
|
||||
|
||||
if (subActivity) {
|
||||
connectedIP = static_cast<WifiSelectionActivity*>(subActivity.get())->getConnectedIP();
|
||||
} else {
|
||||
connectedIP = WiFi.localIP().toString().c_str();
|
||||
}
|
||||
connectedSSID = WiFi.SSID().c_str();
|
||||
exitActivity();
|
||||
startWebServer();
|
||||
}
|
||||
|
||||
@@ -100,11 +99,6 @@ void CalibreConnectActivity::stopWebServer() {
|
||||
}
|
||||
|
||||
void CalibreConnectActivity::loop() {
|
||||
if (subActivity) {
|
||||
subActivity->loop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
exitRequested = true;
|
||||
}
|
||||
@@ -168,12 +162,12 @@ void CalibreConnectActivity::loop() {
|
||||
}
|
||||
|
||||
if (exitRequested) {
|
||||
onComplete();
|
||||
activityManager.popActivity();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void CalibreConnectActivity::render(Activity::RenderLock&&) {
|
||||
void CalibreConnectActivity::render(RenderLock&&) {
|
||||
const auto& metrics = UITheme::getInstance().getMetrics();
|
||||
const auto pageWidth = renderer.getScreenWidth();
|
||||
const auto pageHeight = renderer.getScreenHeight();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "activities/ActivityWithSubactivity.h"
|
||||
#include "activities/Activity.h"
|
||||
#include "network/CrossPointWebServer.h"
|
||||
|
||||
enum class CalibreConnectState { WIFI_SELECTION, SERVER_STARTING, SERVER_RUNNING, ERROR };
|
||||
@@ -13,9 +13,8 @@ enum class CalibreConnectState { WIFI_SELECTION, SERVER_STARTING, SERVER_RUNNING
|
||||
* CalibreConnectActivity starts the file transfer server in STA mode,
|
||||
* but renders Calibre-specific instructions instead of the web transfer UI.
|
||||
*/
|
||||
class CalibreConnectActivity final : public ActivityWithSubactivity {
|
||||
class CalibreConnectActivity final : public Activity {
|
||||
CalibreConnectState state = CalibreConnectState::WIFI_SELECTION;
|
||||
const std::function<void()> onComplete;
|
||||
|
||||
std::unique_ptr<CrossPointWebServer> webServer;
|
||||
std::string connectedIP;
|
||||
@@ -36,13 +35,12 @@ class CalibreConnectActivity final : public ActivityWithSubactivity {
|
||||
void stopWebServer();
|
||||
|
||||
public:
|
||||
explicit CalibreConnectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onComplete)
|
||||
: ActivityWithSubactivity("CalibreConnect", renderer, mappedInput), onComplete(onComplete) {}
|
||||
explicit CalibreConnectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: Activity("CalibreConnect", renderer, mappedInput) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(Activity::RenderLock&&) override;
|
||||
void render(RenderLock&&) override;
|
||||
bool skipLoopDelay() override { return webServer && webServer->isRunning(); }
|
||||
bool preventAutoSleep() override { return webServer && webServer->isRunning(); }
|
||||
};
|
||||
|
||||
@@ -33,7 +33,7 @@ constexpr uint16_t DNS_PORT = 53;
|
||||
} // namespace
|
||||
|
||||
void CrossPointWebServerActivity::onEnter() {
|
||||
ActivityWithSubactivity::onEnter();
|
||||
Activity::onEnter();
|
||||
|
||||
LOG_DBG("WEBACT", "Free heap at onEnter: %d bytes", ESP.getFreeHeap());
|
||||
|
||||
@@ -48,14 +48,18 @@ void CrossPointWebServerActivity::onEnter() {
|
||||
|
||||
// Launch network mode selection subactivity
|
||||
LOG_DBG("WEBACT", "Launching NetworkModeSelectionActivity...");
|
||||
enterNewActivity(new NetworkModeSelectionActivity(
|
||||
renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
|
||||
[this]() { onGoBack(); } // Cancel goes back to home
|
||||
));
|
||||
startActivityForResult(std::make_unique<NetworkModeSelectionActivity>(renderer, mappedInput),
|
||||
[this](const ActivityResult& result) {
|
||||
if (result.isCancelled) {
|
||||
onGoHome();
|
||||
} else {
|
||||
onNetworkModeSelected(std::get<NetworkModeResult>(result.data).mode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void CrossPointWebServerActivity::onExit() {
|
||||
ActivityWithSubactivity::onExit();
|
||||
Activity::onExit();
|
||||
|
||||
LOG_DBG("WEBACT", "Free heap at onExit start: %d bytes", ESP.getFreeHeap());
|
||||
|
||||
@@ -107,18 +111,20 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
|
||||
networkMode = mode;
|
||||
isApMode = (mode == NetworkMode::CREATE_HOTSPOT);
|
||||
|
||||
// Exit mode selection subactivity
|
||||
exitActivity();
|
||||
|
||||
if (mode == NetworkMode::CONNECT_CALIBRE) {
|
||||
exitActivity();
|
||||
enterNewActivity(new CalibreConnectActivity(renderer, mappedInput, [this] {
|
||||
exitActivity();
|
||||
state = WebServerActivityState::MODE_SELECTION;
|
||||
enterNewActivity(new NetworkModeSelectionActivity(
|
||||
renderer, mappedInput, [this](const NetworkMode nextMode) { onNetworkModeSelected(nextMode); },
|
||||
[this]() { onGoBack(); }));
|
||||
}));
|
||||
startActivityForResult(
|
||||
std::make_unique<CalibreConnectActivity>(renderer, mappedInput), [this](const ActivityResult& result) {
|
||||
state = WebServerActivityState::MODE_SELECTION;
|
||||
|
||||
startActivityForResult(std::make_unique<NetworkModeSelectionActivity>(renderer, mappedInput),
|
||||
[this](const ActivityResult& result) {
|
||||
if (result.isCancelled) {
|
||||
onGoHome();
|
||||
} else {
|
||||
onNetworkModeSelected(std::get<NetworkModeResult>(result.data).mode);
|
||||
}
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -129,8 +135,15 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode)
|
||||
|
||||
state = WebServerActivityState::WIFI_SELECTION;
|
||||
LOG_DBG("WEBACT", "Launching WifiSelectionActivity...");
|
||||
enterNewActivity(new WifiSelectionActivity(renderer, mappedInput,
|
||||
[this](const bool connected) { onWifiSelectionComplete(connected); }));
|
||||
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput),
|
||||
[this](const ActivityResult& result) {
|
||||
if (!result.isCancelled) {
|
||||
const auto& wifi = std::get<WifiResult>(result.data);
|
||||
connectedIP = wifi.ip;
|
||||
connectedSSID = wifi.ssid;
|
||||
}
|
||||
onWifiSelectionComplete(!result.isCancelled);
|
||||
});
|
||||
} else {
|
||||
// AP mode - start access point
|
||||
state = WebServerActivityState::AP_STARTING;
|
||||
@@ -144,12 +157,8 @@ void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected)
|
||||
|
||||
if (connected) {
|
||||
// Get connection info before exiting subactivity
|
||||
connectedIP = static_cast<WifiSelectionActivity*>(subActivity.get())->getConnectedIP();
|
||||
connectedSSID = WiFi.SSID().c_str();
|
||||
isApMode = false;
|
||||
|
||||
exitActivity();
|
||||
|
||||
// Start mDNS for hostname resolution
|
||||
if (MDNS.begin(AP_HOSTNAME)) {
|
||||
LOG_DBG("WEBACT", "mDNS started: http://%s.local/", AP_HOSTNAME);
|
||||
@@ -159,11 +168,16 @@ void CrossPointWebServerActivity::onWifiSelectionComplete(const bool connected)
|
||||
startWebServer();
|
||||
} else {
|
||||
// User cancelled - go back to mode selection
|
||||
exitActivity();
|
||||
state = WebServerActivityState::MODE_SELECTION;
|
||||
enterNewActivity(new NetworkModeSelectionActivity(
|
||||
renderer, mappedInput, [this](const NetworkMode mode) { onNetworkModeSelected(mode); },
|
||||
[this]() { onGoBack(); }));
|
||||
|
||||
startActivityForResult(std::make_unique<NetworkModeSelectionActivity>(renderer, mappedInput),
|
||||
[this](const ActivityResult& result) {
|
||||
if (result.isCancelled) {
|
||||
onGoHome();
|
||||
} else {
|
||||
onNetworkModeSelected(std::get<NetworkModeResult>(result.data).mode);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +200,7 @@ void CrossPointWebServerActivity::startAccessPoint() {
|
||||
|
||||
if (!apStarted) {
|
||||
LOG_ERR("WEBACT", "ERROR: Failed to start Access Point!");
|
||||
onGoBack();
|
||||
onGoHome();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -236,16 +250,12 @@ void CrossPointWebServerActivity::startWebServer() {
|
||||
|
||||
// Force an immediate render since we're transitioning from a subactivity
|
||||
// that had its own rendering task. We need to make sure our display is shown.
|
||||
{
|
||||
RenderLock lock(*this);
|
||||
render(std::move(lock));
|
||||
}
|
||||
LOG_DBG("WEBACT", "Rendered File Transfer screen");
|
||||
requestUpdate();
|
||||
} else {
|
||||
LOG_ERR("WEBACT", "ERROR: Failed to start web server!");
|
||||
webServer.reset();
|
||||
// Go back on error
|
||||
onGoBack();
|
||||
onGoHome();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,12 +269,6 @@ void CrossPointWebServerActivity::stopWebServer() {
|
||||
}
|
||||
|
||||
void CrossPointWebServerActivity::loop() {
|
||||
if (subActivity) {
|
||||
// Forward loop to subactivity
|
||||
subActivity->loop();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle different states
|
||||
if (state == WebServerActivityState::SERVER_RUNNING) {
|
||||
// Handle DNS requests for captive portal (AP mode only)
|
||||
@@ -322,7 +326,7 @@ void CrossPointWebServerActivity::loop() {
|
||||
mappedInput.update();
|
||||
// Check for exit button inside loop for responsiveness
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
onGoBack();
|
||||
onGoHome();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -332,13 +336,13 @@ void CrossPointWebServerActivity::loop() {
|
||||
|
||||
// Handle exit on Back button (also check outside loop)
|
||||
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
|
||||
onGoBack();
|
||||
onGoHome();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CrossPointWebServerActivity::render(Activity::RenderLock&&) {
|
||||
void CrossPointWebServerActivity::render(RenderLock&&) {
|
||||
// Only render our own UI when server is running
|
||||
// Subactivities handle their own rendering
|
||||
if (state == WebServerActivityState::SERVER_RUNNING || state == WebServerActivityState::AP_STARTING) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include <string>
|
||||
|
||||
#include "NetworkModeSelectionActivity.h"
|
||||
#include "activities/ActivityWithSubactivity.h"
|
||||
#include "activities/Activity.h"
|
||||
#include "network/CrossPointWebServer.h"
|
||||
|
||||
// Web server activity states
|
||||
@@ -27,9 +27,8 @@ enum class WebServerActivityState {
|
||||
* - Handles client requests in its loop() function
|
||||
* - Cleans up the server and shuts down WiFi on exit
|
||||
*/
|
||||
class CrossPointWebServerActivity final : public ActivityWithSubactivity {
|
||||
class CrossPointWebServerActivity final : public Activity {
|
||||
WebServerActivityState state = WebServerActivityState::MODE_SELECTION;
|
||||
const std::function<void()> onGoBack;
|
||||
|
||||
// Network mode
|
||||
NetworkMode networkMode = NetworkMode::JOIN_NETWORK;
|
||||
@@ -54,13 +53,12 @@ class CrossPointWebServerActivity final : public ActivityWithSubactivity {
|
||||
void stopWebServer();
|
||||
|
||||
public:
|
||||
explicit CrossPointWebServerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
|
||||
const std::function<void()>& onGoBack)
|
||||
: ActivityWithSubactivity("CrossPointWebServer", renderer, mappedInput), onGoBack(onGoBack) {}
|
||||
explicit CrossPointWebServerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
|
||||
: Activity("CrossPointWebServer", renderer, mappedInput) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
void render(Activity::RenderLock&&) override;
|
||||
void render(RenderLock&&) override;
|
||||
bool skipLoopDelay() override { return webServer && webServer->isRunning(); }
|
||||
bool preventAutoSleep() override { return webServer && webServer->isRunning(); }
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user