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:
Adolfo Reyna
2026-09-12 09:03:37 -04:00
parent 85da419a98
commit 054d1286b2
5 changed files with 93 additions and 15 deletions
@@ -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() {