From 054d1286b26daae8881181ad76914bad4ba46637 Mon Sep 17 00:00:00 2001 From: Adolfo Reyna Date: Sat, 12 Sep 2026 09:03:37 -0400 Subject: [PATCH] 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 --- .../Tactility/settings/DisplaySettings.h | 1 + .../service/displayidle/DisplayIdleService.h | 12 ++- Tactility/Source/app/display/Display.cpp | 14 ++-- .../service/displayidle/DisplayIdle.cpp | 76 +++++++++++++++++-- Tactility/Source/settings/DisplaySettings.cpp | 5 ++ 5 files changed, 93 insertions(+), 15 deletions(-) diff --git a/Tactility/Include/Tactility/settings/DisplaySettings.h b/Tactility/Include/Tactility/settings/DisplaySettings.h index 7fc118f8..4a8da8c6 100644 --- a/Tactility/Include/Tactility/settings/DisplaySettings.h +++ b/Tactility/Include/Tactility/settings/DisplaySettings.h @@ -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 }; diff --git a/Tactility/Private/Tactility/service/displayidle/DisplayIdleService.h b/Tactility/Private/Tactility/service/displayidle/DisplayIdleService.h index f32dd658..aaf3eea3 100644 --- a/Tactility/Private/Tactility/service/displayidle/DisplayIdleService.h +++ b/Tactility/Private/Tactility/service/displayidle/DisplayIdleService.h @@ -5,6 +5,8 @@ #include #include +#include + #include #include @@ -29,9 +31,15 @@ class DisplayIdleService final : public Service { // Active screensaver instance std::unique_ptr 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; diff --git a/Tactility/Source/app/display/Display.cpp b/Tactility/Source/app/display/Display.cpp index dbe5b239..2c350d17 100644 --- a/Tactility/Source/app/display/Display.cpp +++ b/Tactility/Source/app/display/Display.cpp @@ -135,8 +135,8 @@ void onTimeoutChanged(lv_event_t* event) { auto* ctx = static_cast(lv_event_get_user_data(event)); auto* dropdown = static_cast(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(ctx->displaySettings.screensaverType)); diff --git a/Tactility/Source/service/displayidle/DisplayIdle.cpp b/Tactility/Source/service/displayidle/DisplayIdle.cpp index 6938d762..be53282f 100644 --- a/Tactility/Source/service/displayidle/DisplayIdle.cpp +++ b/Tactility/Source/service/displayidle/DisplayIdle.cpp @@ -5,6 +5,9 @@ #include #include +#include +#include + #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() { diff --git a/Tactility/Source/settings/DisplaySettings.cpp b/Tactility/Source/settings/DisplaySettings.cpp index ac84cd06..13a78f41 100644 --- a/Tactility/Source/settings/DisplaySettings.cpp +++ b/Tactility/Source/settings/DisplaySettings.cpp @@ -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; }