feat(display): Bible Verse screensaver + longer idle timeouts
- ScreensaverType::BibleVerse launches one.tactility.bibleverse on idle (falls back to black when not installed); tap stops the app and returns to launcher; 30-minute backlight auto-off stops it too - Timeout options extended to 15/30 minutes; auto-off 5m -> 30m - Screensaver dropdown lists Bible Verse
This commit is contained in:
@@ -19,6 +19,7 @@ enum class ScreensaverType {
|
||||
MatrixRain,
|
||||
StackChan,
|
||||
McpScreen,
|
||||
BibleVerse, // Launch the Bible Verse app (if installed)
|
||||
Count // Sentinel for bounds checking - must be last
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
#include <Tactility/settings/DisplaySettings.h>
|
||||
#include <Tactility/Timer.h>
|
||||
|
||||
#include <app/instance.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
|
||||
@@ -29,9 +31,15 @@ class DisplayIdleService final : public Service {
|
||||
// Active screensaver instance
|
||||
std::unique_ptr<Screensaver> screensaver;
|
||||
|
||||
// Screensaver auto-off: turn off backlight after 5 minutes of screensaver
|
||||
// App launched as screensaver (Bible Verse): instance id, 0 when none.
|
||||
// A separate flag (not just the id) tracks intent, so a user-closed app
|
||||
// doesn't look like an active screensaver and vice versa.
|
||||
AppInstanceId screensaverAppInstanceId = 0;
|
||||
bool screensaverAppActive = false;
|
||||
|
||||
// Screensaver auto-off: turn off backlight after 30 minutes of screensaver
|
||||
static constexpr uint32_t TICK_INTERVAL_MS = 50;
|
||||
static constexpr uint32_t SCREENSAVER_AUTO_OFF_MS = 5 * 60 * 1000; // 5 minutes
|
||||
static constexpr uint32_t SCREENSAVER_AUTO_OFF_MS = 30 * 60 * 1000; // 30 minutes
|
||||
static constexpr int SCREENSAVER_AUTO_OFF_TICKS = SCREENSAVER_AUTO_OFF_MS / TICK_INTERVAL_MS;
|
||||
int screensaverActiveCounter = 0;
|
||||
bool backlightOff = false;
|
||||
|
||||
@@ -135,8 +135,8 @@ void onTimeoutChanged(lv_event_t* event) {
|
||||
auto* ctx = static_cast<Context*>(lv_event_get_user_data(event));
|
||||
auto* dropdown = static_cast<lv_obj_t*>(lv_event_get_target(event));
|
||||
uint32_t idx = lv_dropdown_get_selected(dropdown);
|
||||
// Map dropdown index to ms: 0=15s,1=30s,2=1m,3=2m,4=5m,5=Never
|
||||
static const uint32_t values_ms[] = {15000, 30000, 60000, 120000, 300000, 0};
|
||||
// Map dropdown index to ms: 0=15s,1=30s,2=1m,3=2m,4=5m,5=15m,6=30m,7=Never
|
||||
static const uint32_t values_ms[] = {15000, 30000, 60000, 120000, 300000, 900000, 1800000, 0};
|
||||
if (idx < (sizeof(values_ms)/sizeof(values_ms[0]))) {
|
||||
ctx->displaySettings.backlightTimeoutMs = values_ms[idx];
|
||||
ctx->displaySettingsUpdated = true;
|
||||
@@ -353,7 +353,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
lv_obj_align(timeout_value_label, LV_ALIGN_LEFT_MID, 0, 0);
|
||||
|
||||
ctx->timeoutDropdown = lv_dropdown_create(timeout_select_wrapper);
|
||||
lv_dropdown_set_options(ctx->timeoutDropdown, "15 seconds\n30 seconds\n1 minute\n2 minutes\n5 minutes\nNever");
|
||||
lv_dropdown_set_options(ctx->timeoutDropdown, "15 seconds\n30 seconds\n1 minute\n2 minutes\n5 minutes\n15 minutes\n30 minutes\nNever");
|
||||
lv_obj_align(ctx->timeoutDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(ctx->timeoutDropdown, onTimeoutChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
// Initialize dropdown selection from settings
|
||||
@@ -368,8 +368,12 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
idx = 3;
|
||||
else if (ms == 300000)
|
||||
idx = 4;
|
||||
else if (ms == 0)
|
||||
else if (ms == 900000)
|
||||
idx = 5;
|
||||
else if (ms == 1800000)
|
||||
idx = 6;
|
||||
else if (ms == 0)
|
||||
idx = 7;
|
||||
lv_dropdown_set_selected(ctx->timeoutDropdown, idx);
|
||||
if (!ctx->displaySettings.backlightTimeoutEnabled) {
|
||||
lv_obj_add_state(ctx->timeoutDropdown, LV_STATE_DISABLED);
|
||||
@@ -387,7 +391,7 @@ void createWidgets(lv_obj_t* parent, void* userData) {
|
||||
|
||||
ctx->screensaverDropdown = lv_dropdown_create(screensaver_wrapper);
|
||||
// Note: order correlates with settings::display::ScreensaverType enum order
|
||||
lv_dropdown_set_options(ctx->screensaverDropdown, "None\nBouncing Balls\nMystify\nMatrix Rain\nStackChan\nMCP Screen");
|
||||
lv_dropdown_set_options(ctx->screensaverDropdown, "None\nBouncing Balls\nMystify\nMatrix Rain\nStackChan\nMCP Screen\nBible Verse");
|
||||
lv_obj_align(ctx->screensaverDropdown, LV_ALIGN_RIGHT_MID, 0, 0);
|
||||
lv_obj_add_event_cb(ctx->screensaverDropdown, onScreensaverChanged, LV_EVENT_VALUE_CHANGED, ctx);
|
||||
lv_dropdown_set_selected(ctx->screensaverDropdown, static_cast<uint16_t>(ctx->displaySettings.screensaverType));
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
#include <Tactility/service/ServiceRegistration.h>
|
||||
#include <Tactility/mcp/McpSystem.h>
|
||||
|
||||
#include <app/manager.h>
|
||||
#include <app/start.h>
|
||||
|
||||
#include "BouncingBallsScreensaver.h"
|
||||
#include "MatrixRainScreensaver.h"
|
||||
#include "MystifyScreensaver.h"
|
||||
@@ -107,6 +110,17 @@ void DisplayIdleService::stopScreensaver() {
|
||||
screensaverOverlay = nullptr;
|
||||
}
|
||||
lvgl_unlock();
|
||||
|
||||
// Stop an app launched as screensaver (e.g. Bible Verse): on wake the
|
||||
// device returns to the launcher. Stopping is safe when the user already
|
||||
// closed it (unknown ids report STOPPED / stop is a no-op for them).
|
||||
if (screensaverAppActive) {
|
||||
screensaverAppActive = false;
|
||||
if (screensaverAppInstanceId != 0) {
|
||||
app_manager_stop(screensaverAppInstanceId);
|
||||
screensaverAppInstanceId = 0;
|
||||
}
|
||||
}
|
||||
stopScreensaverRequested.store(false, std::memory_order_relaxed);
|
||||
|
||||
// Reset auto-off state
|
||||
@@ -122,14 +136,31 @@ void DisplayIdleService::stopScreensaver() {
|
||||
}
|
||||
|
||||
void DisplayIdleService::activateScreensaver() {
|
||||
lv_obj_t* top = lv_layer_top();
|
||||
|
||||
if (screensaverOverlay != nullptr) return;
|
||||
|
||||
// Reset auto-off counter when starting screensaver
|
||||
screensaverActiveCounter = 0;
|
||||
backlightOff = false;
|
||||
|
||||
// Bible Verse screensaver: launch the app instead of drawing an overlay.
|
||||
// Falls back to a plain black overlay when the app is not installed.
|
||||
if (cachedDisplaySettings.screensaverType == settings::display::ScreensaverType::BibleVerse) {
|
||||
constexpr auto* BIBLE_VERSE_APP_ID = "one.tactility.bibleverse";
|
||||
AppManifest manifest {};
|
||||
AppInstanceId instance_id = 0;
|
||||
if (app_manager_find_manifest(BIBLE_VERSE_APP_ID, &manifest) == ERROR_NONE
|
||||
&& app_start(BIBLE_VERSE_APP_ID, 0, nullptr, &instance_id) == ERROR_NONE) {
|
||||
LOG_I(TAG, "Bible Verse screensaver: launched instance %u", (unsigned)instance_id);
|
||||
screensaverAppInstanceId = instance_id;
|
||||
screensaverAppActive = true;
|
||||
setBacklightBrightness(screensaverBacklightDuty(cachedDisplaySettings));
|
||||
return;
|
||||
}
|
||||
LOG_W(TAG, "Bible Verse app not installed, falling back to black screen");
|
||||
}
|
||||
|
||||
lv_obj_t* top = lv_layer_top();
|
||||
|
||||
if (screensaverOverlay != nullptr) return;
|
||||
|
||||
lv_coord_t screenW = lv_display_get_horizontal_resolution(nullptr);
|
||||
lv_coord_t screenH = lv_display_get_vertical_resolution(nullptr);
|
||||
|
||||
@@ -201,8 +232,8 @@ void DisplayIdleService::tick() {
|
||||
inactive_ms = lv_display_get_inactive_time(nullptr);
|
||||
|
||||
// Only update if not stopping (prevents lag on touch)
|
||||
if (displayDimmed && screensaverOverlay && !stopScreensaverRequested.load(std::memory_order_acquire)) {
|
||||
// Check if screensaver should auto-off after 5 minutes
|
||||
if (displayDimmed && (screensaverOverlay || screensaverAppActive) && !stopScreensaverRequested.load(std::memory_order_acquire)) {
|
||||
// Check if screensaver should auto-off after 30 minutes
|
||||
if (!backlightOff) {
|
||||
screensaverActiveCounter++;
|
||||
if (screensaverActiveCounter >= SCREENSAVER_AUTO_OFF_TICKS) {
|
||||
@@ -211,6 +242,13 @@ void DisplayIdleService::tick() {
|
||||
screensaver->stop();
|
||||
screensaver.reset();
|
||||
}
|
||||
if (screensaverAppActive) {
|
||||
screensaverAppActive = false;
|
||||
if (screensaverAppInstanceId != 0) {
|
||||
app_manager_stop(screensaverAppInstanceId);
|
||||
screensaverAppInstanceId = 0;
|
||||
}
|
||||
}
|
||||
setBacklightBrightness(0);
|
||||
backlightOff = true;
|
||||
} else {
|
||||
@@ -246,6 +284,10 @@ void DisplayIdleService::tick() {
|
||||
}
|
||||
displayDimmed = true;
|
||||
} else if (displayDimmed && (inactive_ms < kWakeActivityThresholdMs)) {
|
||||
// The Bible Verse app keeps LVGL timers running (minute-advance),
|
||||
// so plain inactivity never drops while it is up. Taps still
|
||||
// register as activity: stop the verse app to return to launcher.
|
||||
// If the user already closed it, stopScreensaver() is a safe no-op.
|
||||
stopScreensaver();
|
||||
}
|
||||
}
|
||||
@@ -282,6 +324,13 @@ void DisplayIdleService::onStop(ServiceContext& service) {
|
||||
LOG_W(TAG, "Failed to stop screensaver during shutdown - potential resource leak");
|
||||
}
|
||||
}
|
||||
if (screensaverAppActive) {
|
||||
screensaverAppActive = false;
|
||||
if (screensaverAppInstanceId != 0) {
|
||||
app_manager_stop(screensaverAppInstanceId);
|
||||
screensaverAppInstanceId = 0;
|
||||
}
|
||||
}
|
||||
screensaver.reset();
|
||||
}
|
||||
|
||||
@@ -294,8 +343,19 @@ void DisplayIdleService::startScreensaver() {
|
||||
// Note: This is safe because we hold the LVGL lock which serializes with tick()
|
||||
cachedDisplaySettings = settings::display::loadOrGetDefault();
|
||||
|
||||
// The Bible Verse path launches an app task (not an overlay), which must
|
||||
// not run under the LVGL lock: drop it before activating.
|
||||
const bool launches_app =
|
||||
cachedDisplaySettings.screensaverType == settings::display::ScreensaverType::BibleVerse;
|
||||
if (launches_app) {
|
||||
lvgl_unlock();
|
||||
}
|
||||
|
||||
activateScreensaver();
|
||||
lvgl_unlock();
|
||||
|
||||
if (!launches_app) {
|
||||
lvgl_unlock();
|
||||
}
|
||||
|
||||
// Turn off backlight for "None" screensaver
|
||||
if (hasDisplayWithBacklight() && cachedDisplaySettings.screensaverType == settings::display::ScreensaverType::None) {
|
||||
@@ -305,7 +365,7 @@ void DisplayIdleService::startScreensaver() {
|
||||
}
|
||||
|
||||
bool DisplayIdleService::isScreensaverActive() const {
|
||||
return screensaverOverlay != nullptr;
|
||||
return screensaverOverlay != nullptr || screensaverAppActive;
|
||||
}
|
||||
|
||||
void DisplayIdleService::startMcpScreensaver() {
|
||||
|
||||
@@ -121,6 +121,8 @@ static std::string toString(ScreensaverType type) {
|
||||
return "StackChan";
|
||||
case McpScreen:
|
||||
return "McpScreen";
|
||||
case BibleVerse:
|
||||
return "BibleVerse";
|
||||
default:
|
||||
std::unreachable();
|
||||
}
|
||||
@@ -145,6 +147,9 @@ static bool fromString(const std::string& str, ScreensaverType& type) {
|
||||
} else if (str == "McpScreen") {
|
||||
type = ScreensaverType::McpScreen;
|
||||
return true;
|
||||
} else if (str == "BibleVerse") {
|
||||
type = ScreensaverType::BibleVerse;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user