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:
Xuan-Son Nguyen
2026-02-27 07:32:40 +01:00
committed by GitHub
parent 5b11e45a36
commit c4fc4effbd
69 changed files with 1095 additions and 1180 deletions

View File

@@ -52,20 +52,20 @@ void ButtonRemapActivity::loop() {
SETTINGS.frontButtonLeft = CrossPointSettings::FRONT_HW_LEFT;
SETTINGS.frontButtonRight = CrossPointSettings::FRONT_HW_RIGHT;
SETTINGS.saveToFile();
onBack();
finish();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Down)) {
// Exit without changing settings.
onBack();
finish();
return;
}
{
// Wait for the UI to refresh before accepting another assignment.
// Make sure UI done rendering before accepting another assignment.
// This avoids rapid double-presses that can advance the step without a visible redraw.
requestUpdateAndWait();
RenderLock lock(*this);
// Wait for a front button press to assign to the current role.
const int pressedButton = mappedInput.getPressedFrontButton();
@@ -86,7 +86,7 @@ void ButtonRemapActivity::loop() {
// All roles assigned; save to settings and exit.
applyTempMapping();
SETTINGS.saveToFile();
onBack();
finish();
return;
}
@@ -94,7 +94,7 @@ void ButtonRemapActivity::loop() {
}
}
void ButtonRemapActivity::render(Activity::RenderLock&&) {
void ButtonRemapActivity::render(RenderLock&&) {
const auto labelForHardware = [&](uint8_t hardwareIndex) -> const char* {
for (uint8_t i = 0; i < kRoleCount; i++) {
if (tempMapping[i] == hardwareIndex) {

View File

@@ -7,20 +7,17 @@
class ButtonRemapActivity final : public Activity {
public:
explicit ButtonRemapActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack)
: Activity("ButtonRemap", renderer, mappedInput), onBack(onBack) {}
explicit ButtonRemapActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("ButtonRemap", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
void render(RenderLock&&) override;
private:
// Rendering task state.
// Callback used to exit the remap flow back to the settings list.
const std::function<void()> onBack;
// Index of the logical role currently awaiting input.
uint8_t currentStep = 0;
// Temporary mapping from logical role -> hardware button index.

View File

@@ -17,22 +17,17 @@ const StrId menuNames[MENU_ITEMS] = {StrId::STR_CALIBRE_WEB_URL, StrId::STR_USER
} // namespace
void CalibreSettingsActivity::onEnter() {
ActivityWithSubactivity::onEnter();
Activity::onEnter();
selectedIndex = 0;
requestUpdate();
}
void CalibreSettingsActivity::onExit() { ActivityWithSubactivity::onExit(); }
void CalibreSettingsActivity::onExit() { Activity::onExit(); }
void CalibreSettingsActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onBack();
activityManager.popActivity();
return;
}
@@ -56,62 +51,44 @@ void CalibreSettingsActivity::loop() {
void CalibreSettingsActivity::handleSelection() {
if (selectedIndex == 0) {
// OPDS Server URL
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_CALIBRE_WEB_URL), SETTINGS.opdsServerUrl,
127, // maxLength
false, // not password
[this](const std::string& url) {
strncpy(SETTINGS.opdsServerUrl, url.c_str(), sizeof(SETTINGS.opdsServerUrl) - 1);
SETTINGS.opdsServerUrl[sizeof(SETTINGS.opdsServerUrl) - 1] = '\0';
SETTINGS.saveToFile();
exitActivity();
requestUpdate();
},
[this]() {
exitActivity();
requestUpdate();
}));
startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_CALIBRE_WEB_URL),
SETTINGS.opdsServerUrl, 127, false),
[this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& kb = std::get<KeyboardResult>(result.data);
strncpy(SETTINGS.opdsServerUrl, kb.text.c_str(), sizeof(SETTINGS.opdsServerUrl) - 1);
SETTINGS.opdsServerUrl[sizeof(SETTINGS.opdsServerUrl) - 1] = '\0';
SETTINGS.saveToFile();
}
});
} else if (selectedIndex == 1) {
// Username
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_USERNAME), SETTINGS.opdsUsername,
63, // maxLength
false, // not password
[this](const std::string& username) {
strncpy(SETTINGS.opdsUsername, username.c_str(), sizeof(SETTINGS.opdsUsername) - 1);
SETTINGS.opdsUsername[sizeof(SETTINGS.opdsUsername) - 1] = '\0';
SETTINGS.saveToFile();
exitActivity();
requestUpdate();
},
[this]() {
exitActivity();
requestUpdate();
}));
startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_USERNAME),
SETTINGS.opdsUsername, 63, false),
[this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& kb = std::get<KeyboardResult>(result.data);
strncpy(SETTINGS.opdsUsername, kb.text.c_str(), sizeof(SETTINGS.opdsUsername) - 1);
SETTINGS.opdsUsername[sizeof(SETTINGS.opdsUsername) - 1] = '\0';
SETTINGS.saveToFile();
}
});
} else if (selectedIndex == 2) {
// Password
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_PASSWORD), SETTINGS.opdsPassword,
63, // maxLength
false, // not password mode
[this](const std::string& password) {
strncpy(SETTINGS.opdsPassword, password.c_str(), sizeof(SETTINGS.opdsPassword) - 1);
SETTINGS.opdsPassword[sizeof(SETTINGS.opdsPassword) - 1] = '\0';
SETTINGS.saveToFile();
exitActivity();
requestUpdate();
},
[this]() {
exitActivity();
requestUpdate();
}));
startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_PASSWORD),
SETTINGS.opdsPassword, 63, false),
[this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& kb = std::get<KeyboardResult>(result.data);
strncpy(SETTINGS.opdsPassword, kb.text.c_str(), sizeof(SETTINGS.opdsPassword) - 1);
SETTINGS.opdsPassword[sizeof(SETTINGS.opdsPassword) - 1] = '\0';
SETTINGS.saveToFile();
}
});
}
}
void CalibreSettingsActivity::render(Activity::RenderLock&&) {
void CalibreSettingsActivity::render(RenderLock&&) {
renderer.clearScreen();
const auto& metrics = UITheme::getInstance().getMetrics();

View File

@@ -1,29 +1,25 @@
#pragma once
#include <functional>
#include "activities/ActivityWithSubactivity.h"
#include "activities/Activity.h"
#include "util/ButtonNavigator.h"
/**
* Submenu for OPDS Browser settings.
* Shows OPDS Server URL and HTTP authentication options.
*/
class CalibreSettingsActivity final : public ActivityWithSubactivity {
class CalibreSettingsActivity final : public Activity {
public:
explicit CalibreSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack)
: ActivityWithSubactivity("CalibreSettings", renderer, mappedInput), onBack(onBack) {}
explicit CalibreSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("CalibreSettings", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
void render(RenderLock&&) override;
private:
ButtonNavigator buttonNavigator;
size_t selectedIndex = 0;
const std::function<void()> onBack;
void handleSelection();
};

View File

@@ -10,15 +10,15 @@
#include "fontIds.h"
void ClearCacheActivity::onEnter() {
ActivityWithSubactivity::onEnter();
Activity::onEnter();
state = WARNING;
requestUpdate();
}
void ClearCacheActivity::onExit() { ActivityWithSubactivity::onExit(); }
void ClearCacheActivity::onExit() { Activity::onExit(); }
void ClearCacheActivity::render(Activity::RenderLock&&) {
void ClearCacheActivity::render(RenderLock&&) {
const auto& metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();

View File

@@ -2,26 +2,25 @@
#include <functional>
#include "activities/ActivityWithSubactivity.h"
#include "activities/Activity.h"
class ClearCacheActivity final : public ActivityWithSubactivity {
class ClearCacheActivity final : public Activity {
public:
explicit ClearCacheActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& goBack)
: ActivityWithSubactivity("ClearCache", renderer, mappedInput), goBack(goBack) {}
explicit ClearCacheActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("ClearCache", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
bool skipLoopDelay() override { return true; } // Prevent power-saving mode
void render(Activity::RenderLock&&) override;
void render(RenderLock&&) override;
private:
enum State { WARNING, CLEARING, SUCCESS, FAILED };
State state = WARNING;
const std::function<void()> goBack;
void goBack() { finish(); }
int clearedCount = 0;
int failedCount = 0;

View File

@@ -12,8 +12,6 @@
#include "fontIds.h"
void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) {
exitActivity();
if (!success) {
{
RenderLock lock(*this);
@@ -51,7 +49,7 @@ void KOReaderAuthActivity::performAuthentication() {
}
void KOReaderAuthActivity::onEnter() {
ActivityWithSubactivity::onEnter();
Activity::onEnter();
// Turn on WiFi
WiFi.mode(WIFI_STA);
@@ -74,12 +72,12 @@ void KOReaderAuthActivity::onEnter() {
}
// Launch WiFi selection
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 KOReaderAuthActivity::onExit() {
ActivityWithSubactivity::onExit();
Activity::onExit();
// Turn off wifi
WiFi.disconnect(false);
@@ -88,7 +86,7 @@ void KOReaderAuthActivity::onExit() {
delay(100);
}
void KOReaderAuthActivity::render(Activity::RenderLock&&) {
void KOReaderAuthActivity::render(RenderLock&&) {
renderer.clearScreen();
const auto& metrics = UITheme::getInstance().getMetrics();
@@ -115,15 +113,10 @@ void KOReaderAuthActivity::render(Activity::RenderLock&&) {
}
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();
finish();
}
}
}

View File

@@ -2,22 +2,21 @@
#include <functional>
#include "activities/ActivityWithSubactivity.h"
#include "activities/Activity.h"
/**
* Activity for testing KOReader credentials.
* Connects to WiFi and authenticates with the KOReader sync server.
*/
class KOReaderAuthActivity final : public ActivityWithSubactivity {
class KOReaderAuthActivity final : public Activity {
public:
explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onComplete)
: ActivityWithSubactivity("KOReaderAuth", renderer, mappedInput), onComplete(onComplete) {}
explicit KOReaderAuthActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("KOReaderAuth", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
void render(RenderLock&&) override;
bool preventAutoSleep() override { return state == CONNECTING || state == AUTHENTICATING; }
private:
@@ -27,8 +26,6 @@ class KOReaderAuthActivity final : public ActivityWithSubactivity {
std::string statusMessage;
std::string errorMessage;
const std::function<void()> onComplete;
void onWifiSelectionComplete(bool success);
void performAuthentication();
};

View File

@@ -19,22 +19,17 @@ const StrId menuNames[MENU_ITEMS] = {StrId::STR_USERNAME, StrId::STR_PASSWORD, S
} // namespace
void KOReaderSettingsActivity::onEnter() {
ActivityWithSubactivity::onEnter();
Activity::onEnter();
selectedIndex = 0;
requestUpdate();
}
void KOReaderSettingsActivity::onExit() { ActivityWithSubactivity::onExit(); }
void KOReaderSettingsActivity::onExit() { Activity::onExit(); }
void KOReaderSettingsActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onBack();
activityManager.popActivity();
return;
}
@@ -58,59 +53,46 @@ void KOReaderSettingsActivity::loop() {
void KOReaderSettingsActivity::handleSelection() {
if (selectedIndex == 0) {
// Username
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_KOREADER_USERNAME), KOREADER_STORE.getUsername(),
64, // maxLength
false, // not password
[this](const std::string& username) {
KOREADER_STORE.setCredentials(username, KOREADER_STORE.getPassword());
KOREADER_STORE.saveToFile();
exitActivity();
requestUpdate();
},
[this]() {
exitActivity();
requestUpdate();
}));
startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_KOREADER_USERNAME),
KOREADER_STORE.getUsername(),
64, // maxLength
false), // not password
[this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& kb = std::get<KeyboardResult>(result.data);
KOREADER_STORE.setCredentials(kb.text, KOREADER_STORE.getPassword());
KOREADER_STORE.saveToFile();
}
});
} else if (selectedIndex == 1) {
// Password
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_KOREADER_PASSWORD), KOREADER_STORE.getPassword(),
64, // maxLength
false, // show characters
[this](const std::string& password) {
KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), password);
KOREADER_STORE.saveToFile();
exitActivity();
requestUpdate();
},
[this]() {
exitActivity();
requestUpdate();
}));
startActivityForResult(std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_KOREADER_PASSWORD),
KOREADER_STORE.getPassword(),
64, // maxLength
false), // show characters
[this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& kb = std::get<KeyboardResult>(result.data);
KOREADER_STORE.setCredentials(KOREADER_STORE.getUsername(), kb.text);
KOREADER_STORE.saveToFile();
}
});
} else if (selectedIndex == 2) {
// Sync Server URL - prefill with https:// if empty to save typing
const std::string currentUrl = KOREADER_STORE.getServerUrl();
const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl;
exitActivity();
enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl,
128, // maxLength - URLs can be long
false, // not password
[this](const std::string& url) {
// Clear if user just left the prefilled https://
const std::string urlToSave = (url == "https://" || url == "http://") ? "" : url;
KOREADER_STORE.setServerUrl(urlToSave);
KOREADER_STORE.saveToFile();
exitActivity();
requestUpdate();
},
[this]() {
exitActivity();
requestUpdate();
}));
startActivityForResult(
std::make_unique<KeyboardEntryActivity>(renderer, mappedInput, tr(STR_SYNC_SERVER_URL), prefillUrl,
128, // maxLength - URLs can be long
false), // not password
[this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& kb = std::get<KeyboardResult>(result.data);
const std::string urlToSave = (kb.text == "https://" || kb.text == "http://") ? "" : kb.text;
KOREADER_STORE.setServerUrl(urlToSave);
KOREADER_STORE.saveToFile();
}
});
} else if (selectedIndex == 3) {
// Document Matching - toggle between Filename and Binary
const auto current = KOREADER_STORE.getMatchMethod();
@@ -125,15 +107,11 @@ void KOReaderSettingsActivity::handleSelection() {
// Can't authenticate without credentials - just show message briefly
return;
}
exitActivity();
enterNewActivity(new KOReaderAuthActivity(renderer, mappedInput, [this] {
exitActivity();
requestUpdate();
}));
startActivityForResult(std::make_unique<KOReaderAuthActivity>(renderer, mappedInput), [](const ActivityResult&) {});
}
}
void KOReaderSettingsActivity::render(Activity::RenderLock&&) {
void KOReaderSettingsActivity::render(RenderLock&&) {
renderer.clearScreen();
const auto& metrics = UITheme::getInstance().getMetrics();

View File

@@ -1,30 +1,26 @@
#pragma once
#include <functional>
#include "activities/ActivityWithSubactivity.h"
#include "activities/Activity.h"
#include "util/ButtonNavigator.h"
/**
* Submenu for KOReader Sync settings.
* Shows username, password, and authenticate options.
*/
class KOReaderSettingsActivity final : public ActivityWithSubactivity {
class KOReaderSettingsActivity final : public Activity {
public:
explicit KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack)
: ActivityWithSubactivity("KOReaderSettings", renderer, mappedInput), onBack(onBack) {}
explicit KOReaderSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("KOReaderSettings", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
void render(RenderLock&&) override;
private:
ButtonNavigator buttonNavigator;
size_t selectedIndex = 0;
const std::function<void()> onBack;
void handleSelection();
};

View File

@@ -58,7 +58,7 @@ void LanguageSelectActivity::handleSelection() {
onBack();
}
void LanguageSelectActivity::render(Activity::RenderLock&&) {
void LanguageSelectActivity::render(RenderLock&&) {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();

View File

@@ -5,7 +5,7 @@
#include <functional>
#include "../ActivityWithSubactivity.h"
#include "../Activity.h"
#include "components/UITheme.h"
#include "util/ButtonNavigator.h"
@@ -16,19 +16,18 @@ class MappedInputManager;
*/
class LanguageSelectActivity final : public Activity {
public:
explicit LanguageSelectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack)
: Activity("LanguageSelect", renderer, mappedInput), onBack(onBack) {}
explicit LanguageSelectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("LanguageSelect", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
void render(RenderLock&&) override;
private:
void handleSelection();
std::function<void()> onBack;
void onBack() { finish(); }
ButtonNavigator buttonNavigator;
int selectedIndex = 0;
constexpr static uint8_t totalItems = getLanguageCount();

View File

@@ -11,11 +11,9 @@
#include "network/OtaUpdater.h"
void OtaUpdateActivity::onWifiSelectionComplete(const bool success) {
exitActivity();
if (!success) {
LOG_ERR("OTA", "WiFi connection failed, exiting");
goBack();
activityManager.popActivity();
return;
}
@@ -34,7 +32,6 @@ void OtaUpdateActivity::onWifiSelectionComplete(const bool success) {
RenderLock lock(*this);
state = FAILED;
}
requestUpdate();
return;
}
@@ -44,7 +41,6 @@ void OtaUpdateActivity::onWifiSelectionComplete(const bool success) {
RenderLock lock(*this);
state = NO_UPDATE;
}
requestUpdate();
return;
}
@@ -52,11 +48,10 @@ void OtaUpdateActivity::onWifiSelectionComplete(const bool success) {
RenderLock lock(*this);
state = WAITING_CONFIRMATION;
}
requestUpdate();
}
void OtaUpdateActivity::onEnter() {
ActivityWithSubactivity::onEnter();
Activity::onEnter();
// Turn on WiFi immediately
LOG_DBG("OTA", "Turning on WiFi...");
@@ -64,12 +59,12 @@ void OtaUpdateActivity::onEnter() {
// Launch WiFi selection subactivity
LOG_DBG("OTA", "Launching WifiSelectionActivity...");
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 OtaUpdateActivity::onExit() {
ActivityWithSubactivity::onExit();
Activity::onExit();
// Turn off wifi
WiFi.disconnect(false); // false = don't erase credentials, send disconnect frame
@@ -78,12 +73,7 @@ void OtaUpdateActivity::onExit() {
delay(100); // Allow WiFi hardware to fully power down
}
void OtaUpdateActivity::render(Activity::RenderLock&&) {
if (subActivity) {
// Subactivity handles its own rendering
return;
}
void OtaUpdateActivity::render(RenderLock&&) {
const auto& metrics = UITheme::getInstance().getMetrics();
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
@@ -154,11 +144,6 @@ void OtaUpdateActivity::loop() {
requestUpdate();
}
if (subActivity) {
subActivity->loop();
return;
}
if (state == WAITING_CONFIRMATION) {
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
LOG_DBG("OTA", "New update available, starting download...");
@@ -166,7 +151,6 @@ void OtaUpdateActivity::loop() {
RenderLock lock(*this);
state = UPDATE_IN_PROGRESS;
}
requestUpdate();
requestUpdateAndWait();
const auto res = updater.installUpdate();
@@ -188,7 +172,7 @@ void OtaUpdateActivity::loop() {
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
goBack();
activityManager.popActivity();
}
return;
@@ -196,14 +180,14 @@ void OtaUpdateActivity::loop() {
if (state == FAILED) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
goBack();
activityManager.popActivity();
}
return;
}
if (state == NO_UPDATE) {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
goBack();
activityManager.popActivity();
}
return;
}

View File

@@ -1,9 +1,9 @@
#pragma once
#include "activities/ActivityWithSubactivity.h"
#include "activities/Activity.h"
#include "network/OtaUpdater.h"
class OtaUpdateActivity : public ActivityWithSubactivity {
class OtaUpdateActivity : public Activity {
enum State {
WIFI_SELECTION,
CHECKING_FOR_UPDATE,
@@ -18,7 +18,6 @@ class OtaUpdateActivity : public ActivityWithSubactivity {
// Can't initialize this to 0 or the first render doesn't happen
static constexpr unsigned int UNINITIALIZED_PERCENTAGE = 111;
const std::function<void()> goBack;
State state = WIFI_SELECTION;
unsigned int lastUpdaterPercentage = UNINITIALIZED_PERCENTAGE;
OtaUpdater updater;
@@ -26,13 +25,12 @@ class OtaUpdateActivity : public ActivityWithSubactivity {
void onWifiSelectionComplete(bool success);
public:
explicit OtaUpdateActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& goBack)
: ActivityWithSubactivity("OtaUpdate", renderer, mappedInput), goBack(goBack), updater() {}
explicit OtaUpdateActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("OtaUpdate", renderer, mappedInput), updater() {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
void render(RenderLock&&) override;
bool preventAutoSleep() override { return state == CHECKING_FOR_UPDATE || state == UPDATE_IN_PROGRESS; }
bool skipLoopDelay() override { return true; } // Prevent power-saving mode
};

View File

@@ -67,16 +67,12 @@ void SettingsActivity::onEnter() {
}
void SettingsActivity::onExit() {
ActivityWithSubactivity::onExit();
Activity::onExit();
UITheme::getInstance().reload(); // Re-apply theme in case it was changed
}
void SettingsActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
bool hasChangedCategory = false;
// Handle actions with early return
@@ -164,50 +160,38 @@ void SettingsActivity::toggleCurrentSetting() {
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
}
} else if (setting.type == SettingType::ACTION) {
auto enterSubActivity = [this](Activity* activity) {
exitActivity();
enterNewActivity(activity);
};
auto onComplete = [this] {
exitActivity();
requestUpdate();
};
auto onCompleteBool = [this](bool) {
exitActivity();
requestUpdate();
};
auto resultHandler = [this](const ActivityResult&) { SETTINGS.saveToFile(); };
switch (setting.action) {
case SettingAction::RemapFrontButtons:
enterSubActivity(new ButtonRemapActivity(renderer, mappedInput, onComplete));
startActivityForResult(std::make_unique<ButtonRemapActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::CustomiseStatusBar:
enterSubActivity(new StatusBarSettingsActivity(renderer, mappedInput, onComplete));
startActivityForResult(std::make_unique<StatusBarSettingsActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::KOReaderSync:
enterSubActivity(new KOReaderSettingsActivity(renderer, mappedInput, onComplete));
startActivityForResult(std::make_unique<KOReaderSettingsActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::OPDSBrowser:
enterSubActivity(new CalibreSettingsActivity(renderer, mappedInput, onComplete));
startActivityForResult(std::make_unique<CalibreSettingsActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::Network:
enterSubActivity(new WifiSelectionActivity(renderer, mappedInput, onCompleteBool, false));
startActivityForResult(std::make_unique<WifiSelectionActivity>(renderer, mappedInput, false), resultHandler);
break;
case SettingAction::ClearCache:
enterSubActivity(new ClearCacheActivity(renderer, mappedInput, onComplete));
startActivityForResult(std::make_unique<ClearCacheActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::CheckForUpdates:
enterSubActivity(new OtaUpdateActivity(renderer, mappedInput, onComplete));
startActivityForResult(std::make_unique<OtaUpdateActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::Language:
enterSubActivity(new LanguageSelectActivity(renderer, mappedInput, onComplete));
startActivityForResult(std::make_unique<LanguageSelectActivity>(renderer, mappedInput), resultHandler);
break;
case SettingAction::None:
// Do nothing
break;
}
return; // Results will be handled in the result handler, so we can return early here
} else {
return;
}
@@ -215,7 +199,7 @@ void SettingsActivity::toggleCurrentSetting() {
SETTINGS.saveToFile();
}
void SettingsActivity::render(Activity::RenderLock&&) {
void SettingsActivity::render(RenderLock&&) {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();

View File

@@ -5,7 +5,7 @@
#include <string>
#include <vector>
#include "activities/ActivityWithSubactivity.h"
#include "activities/Activity.h"
#include "util/ButtonNavigator.h"
class CrossPointSettings;
@@ -134,7 +134,7 @@ struct SettingInfo {
}
};
class SettingsActivity final : public ActivityWithSubactivity {
class SettingsActivity final : public Activity {
ButtonNavigator buttonNavigator;
int selectedCategoryIndex = 0; // Currently selected category
@@ -148,8 +148,6 @@ class SettingsActivity final : public ActivityWithSubactivity {
std::vector<SettingInfo> systemSettings;
const std::vector<SettingInfo>* currentSettings = nullptr;
const std::function<void()> onGoHome;
static constexpr int categoryCount = 4;
static const StrId categoryNames[categoryCount];
@@ -157,11 +155,10 @@ class SettingsActivity final : public ActivityWithSubactivity {
void toggleCurrentSetting();
public:
explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onGoHome)
: ActivityWithSubactivity("Settings", renderer, mappedInput), onGoHome(onGoHome) {}
explicit SettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("Settings", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
void render(RenderLock&&) override;
};

View File

@@ -58,7 +58,7 @@ void StatusBarSettingsActivity::onExit() { Activity::onExit(); }
void StatusBarSettingsActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onBack();
finish();
return;
}
@@ -114,7 +114,7 @@ void StatusBarSettingsActivity::handleSelection() {
SETTINGS.saveToFile();
}
void StatusBarSettingsActivity::render(Activity::RenderLock&&) {
void StatusBarSettingsActivity::render(RenderLock&&) {
renderer.clearScreen();
auto metrics = UITheme::getInstance().getMetrics();

View File

@@ -9,22 +9,18 @@
// Reader status bar configuration activity
class StatusBarSettingsActivity final : public Activity {
public:
explicit StatusBarSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::function<void()>& onBack)
: Activity("StatusBarSettings", renderer, mappedInput), onBack(onBack) {}
explicit StatusBarSettingsActivity(GfxRenderer& renderer, MappedInputManager& mappedInput)
: Activity("StatusBarSettings", renderer, mappedInput) {}
void onEnter() override;
void onExit() override;
void loop() override;
void render(Activity::RenderLock&&) override;
void render(RenderLock&&) override;
private:
ButtonNavigator buttonNavigator;
int selectedIndex = 0;
const std::function<void()> onBack;
static void taskTrampoline(void* param);
void handleSelection();
};