From d248b3686c1f7479aea5ede451e6af1ef7267a02 Mon Sep 17 00:00:00 2001 From: GuoHui <767428+guohui85@users.noreply.github.com> Date: Fri, 13 Nov 2026 10:31:24 +0800 Subject: [PATCH] feat(settings): add a Lock Screen toggle that keeps the idle screen-off - Settings > Screen > Lock Screen (config key "lock_screen", default on), using the same GetInt/SetInt/Save pattern as Bluetooth "Named Only". - Off: the idle timeout still blacks the panel and turns the backlight off, but no lock sound plays and the first key press wakes straight back to the launcher. The waking key is consumed, so it does not act on the page. On keeps today's TAB then ENTER unlock. - ui_screensaver.cpp reads the setting on every lock, so a change takes effect at the next lock without a restart. - Tests: the Screen menu order test lists the new entry, the lock-screen contract pins the wiring, and the LVGL harness answers config reads per key and covers the off path (no sound, any key wakes, consumed) and switching back on. Closes #245 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0135vUaG2FpgpGRJfB9LEPvZ --- .../main/ui/settings/settings_page.cpp | 66 +++++++++++++++++++ projects/APPLaunch/main/ui/ui_screensaver.cpp | 26 +++++++- .../low_battery_ui/test_low_battery_ui.cpp | 49 +++++++++++++- .../tests/test_lockscreen_contract.py | 10 +++ .../tests/test_settings_menu_order.py | 2 +- 5 files changed, 149 insertions(+), 4 deletions(-) diff --git a/projects/APPLaunch/main/ui/settings/settings_page.cpp b/projects/APPLaunch/main/ui/settings/settings_page.cpp index 2ab89903..d6f8ee71 100644 --- a/projects/APPLaunch/main/ui/settings/settings_page.cpp +++ b/projects/APPLaunch/main/ui/settings/settings_page.cpp @@ -578,6 +578,71 @@ void bluetooth_named_only_api(int cmd, void *data) bluetooth_named_only_state = previous; } +// "lock_screen" (1 = on, the default): when off, the idle timeout still turns the +// screen black and the backlight off, but any key wakes straight back to the launcher +// instead of asking for TAB then ENTER. Read by ui_screensaver.cpp on each lock. +constexpr const char *kLockScreenConfigKey = "lock_screen"; +bool lock_screen_state = true; +std::mutex lock_screen_mutex; + +bool read_lock_screen_config(bool fallback) +{ + int value = fallback ? 1 : 0; + try { + cp0_signal_config_api( + {"GetInt", kLockScreenConfigKey, "1"}, + [&](int code, std::string data) { + int parsed = 0; + const char *begin = data.data(); + const char *end = begin + data.size(); + const auto result = std::from_chars(begin, end, parsed); + if (code == 0 && result.ec == std::errc{} && result.ptr == end && + (parsed == 0 || parsed == 1)) + value = parsed; + }); + } catch (...) { + } + return value != 0; +} + +bool write_lock_screen_config(bool enabled) +{ + bool succeeded = false; + try { + cp0_signal_config_api({"SetInt", kLockScreenConfigKey, enabled ? "1" : "0"}, + [&](int code, std::string) { succeeded = code == 0; }); + } catch (...) { + } + return succeeded; +} + +void lock_screen_api(int cmd, void *data) +{ + std::lock_guard state_lock(lock_screen_mutex); + if ((cmd == SettingApiReadFlag || cmd == SettingApiReadFlagTimeStart) && data) { + lock_screen_state = read_lock_screen_config(lock_screen_state); + if (cmd == SettingApiReadFlag) + *static_cast(data) = lock_screen_state; + else + std::get<0>(*static_cast(data)) = lock_screen_state; + return; + } + if (cmd != SettingApiActivate) return; + + const bool previous = read_lock_screen_config(lock_screen_state); + const bool desired = !previous; + if (!write_lock_screen_config(desired)) { + lock_screen_state = previous; + return; + } + if (save_bluetooth_config()) { // "Save" persists the whole config file + lock_screen_state = desired; + return; + } + if (write_lock_screen_config(previous)) (void)save_bluetooth_config(); + lock_screen_state = previous; +} + static void append_numeric_options(Tree &tree, const NodeIter &parent, int first, int last) { for (int value = first; value <= last; ++value) tree.append_child(parent, SettingEntry{std::to_string(value)}); @@ -621,6 +686,7 @@ void UISettingTreePage::create_page_detail() mode_tree.append_child(dark_time, SettingEntry{"60S"}); mode_tree.append_child(dark_time, SettingEntry{"300S"}); } + mode_tree.append_child(screen, SettingEntry{"Lock Screen", lock_screen_api, true}); } { diff --git a/projects/APPLaunch/main/ui/ui_screensaver.cpp b/projects/APPLaunch/main/ui/ui_screensaver.cpp index 4676f496..ec5f9c13 100644 --- a/projects/APPLaunch/main/ui/ui_screensaver.cpp +++ b/projects/APPLaunch/main/ui/ui_screensaver.cpp @@ -88,6 +88,8 @@ LockscreenBackgroundCache s_background_cache; /* Raw backlight value captured when the panel is forced dark, or -1 while the * backlight is under normal control. */ int s_screen_off_backlight_raw = -1; +/* Read from "lock_screen" on every lock: when off, any key wakes straight back. */ +bool s_lock_enabled = true; struct ScreensaverPanel { @@ -360,6 +362,20 @@ void hint_delete_cb(lv_event_t *event) noexcept } } +bool lock_screen_enabled() noexcept +{ + int value = 1; + try { + cp0_signal_config_api({"GetInt", "lock_screen", "1"}, + [&](int code, std::string data) { + if (code == 0 && (data == "0" || data == "1")) + value = data == "1" ? 1 : 0; + }); + } catch (...) { + } + return value != 0; +} + int timeout_seconds() noexcept { try { @@ -482,9 +498,11 @@ void enter_lockscreen() s_lock.reset(now); launcher_battery_ui::refresh_visibility(); + s_lock_enabled = lock_screen_enabled(); suspend_system_sound(); suspend_screen_off_backlight(); - play_lockscreen_sound("lock.mp3"); + if (s_lock_enabled) + play_lockscreen_sound("lock.mp3"); lv_obj_move_foreground(s_overlay); lv_obj_clear_flag(s_overlay, LV_OBJ_FLAG_HIDDEN); @@ -598,6 +616,12 @@ extern "C" int ui_screensaver_filter_key(const struct key_item *item) return 1; if (s_model.active()) { + if (!s_lock_enabled) { + /* Lock screen off: the first key only wakes the screen, without a sound. */ + if (pressed) + stop_screensaver(true, true); + return 1; + } /* The lock screen owns every key while it is up. */ const LockscreenDecision decision = s_lock.handle_key(item->key_code, pressed, released, now); diff --git a/projects/APPLaunch/tests/low_battery_ui/test_low_battery_ui.cpp b/projects/APPLaunch/tests/low_battery_ui/test_low_battery_ui.cpp index 902a9d17..e68d3184 100644 --- a/projects/APPLaunch/tests/low_battery_ui/test_low_battery_ui.cpp +++ b/projects/APPLaunch/tests/low_battery_ui/test_low_battery_ui.cpp @@ -47,6 +47,7 @@ static int backlight_suspends; static int backlight_restores; static bool fail_backlight_write; static std::string screensaver_timeout = "0"; +static std::string lock_screen = "1"; decltype(cp0_signal_config_api) cp0_signal_config_api; decltype(cp0_signal_audio_api) cp0_signal_audio_api; @@ -447,6 +448,48 @@ static void test_unlock_restarts_idle_timeout() screensaver_timeout = "0"; } +/* Settings > Screen > Lock Screen off: the idle timeout still blacks the panel and + * turns the backlight off, but without the lock sound, and the first fresh key + * wakes straight back (consumed, so it does not also act on the page). */ +static void test_lock_screen_off_wakes_on_any_key() +{ + set_battery(80); + advance(3000); + assert(!visible()); + screensaver_timeout = "10"; + lock_screen = "0"; + backlight_raw = 128; + s_last_sound_asset = ""; + + key(KEY_ESC, KBD_KEY_PRESSED, false); + key(KEY_ESC, KBD_KEY_RELEASED, false); + advance(10000); + ::timer_cb(s_timer); + assert(ui_screensaver_is_active() && s_panel.black); + assert(backlight_raw == 0); + assert_sound(""); + + key(KEY_A, KBD_KEY_PRESSED, true); + assert(s_exiting && ui_screensaver_is_active()); + key(KEY_A, KBD_KEY_RELEASED, true); + advance(400); + lv_anim_refr_now(); + assert(!ui_screensaver_is_active()); + assert(backlight_raw == 128); + assert_sound(""); + + // The setting is read on each lock: back on, the next lock asks for TAB, ENTER. + lock_screen = "1"; + advance(10000); + ::timer_cb(s_timer); + assert(ui_screensaver_is_active() && s_panel.black); + assert_sound("lock.mp3"); + unlock_screensaver(); + key(KEY_ESC, KBD_KEY_PRESSED, false); + key(KEY_ESC, KBD_KEY_RELEASED, false); + screensaver_timeout = "0"; +} + static void test_screensaver_black_is_independent_of_backlight() { set_battery(80); @@ -693,8 +736,9 @@ int main() static_cast(LV_EVENT_KEYBOARD), nullptr); cp0_keyboard_set_global_key_handler([](const key_item *) { ++global_events; }); cp0_keyboard_set_key_filter(previous_filter); - cp0_signal_config_api.append([](std::list, std::function reply) { - if (reply) reply(0, screensaver_timeout); + cp0_signal_config_api.append([](std::list arguments, std::function reply) { + const bool lock_key = arguments.size() > 1 && *std::next(arguments.begin()) == "lock_screen"; + if (reply) reply(0, lock_key ? lock_screen : screensaver_timeout); }); cp0_signal_audio_api.append([](std::list, std::function reply) { if (reply) reply(0, ""); @@ -836,6 +880,7 @@ int main() test_hold_gesture_enters_lock(); test_screensaver_panel(); test_unlock_restarts_idle_timeout(); + test_lock_screen_off_wakes_on_any_key(); test_screensaver_black_is_independent_of_backlight(); shutdown_warning(); assert(!overlay && !warning_timer); diff --git a/projects/APPLaunch/tests/test_lockscreen_contract.py b/projects/APPLaunch/tests/test_lockscreen_contract.py index ab007382..c685efeb 100644 --- a/projects/APPLaunch/tests/test_lockscreen_contract.py +++ b/projects/APPLaunch/tests/test_lockscreen_contract.py @@ -142,3 +142,13 @@ assert "void restore_backlight(int raw);" in CONTROLS_HEADER assert "write_backlight_raw(0)" in CONTROLS assert "if (raw <= 0) return;" in CONTROLS + +# Settings > Screen > Lock Screen ("lock_screen", default on). Off keeps the idle +# screen-off but skips the lock: the setting is read on every lock, no lock sound +# plays, and the first key wakes the screen before the lock model sees it. +SETTINGS = (ROOT / "main/ui/settings/settings_page.cpp").read_text() +assert '"lock_screen"' in SETTINGS and '"lock_screen"' in SOURCE +assert "s_lock_enabled = lock_screen_enabled();" in SOURCE +assert "if (s_lock_enabled)\n play_lockscreen_sound(\"lock.mp3\");" in SOURCE +bypass = SOURCE.index("if (!s_lock_enabled) {") +assert bypass < SOURCE.index("s_lock.handle_key(item->key_code") diff --git a/projects/APPLaunch/tests/test_settings_menu_order.py b/projects/APPLaunch/tests/test_settings_menu_order.py index cae1446b..b3ceadf7 100644 --- a/projects/APPLaunch/tests/test_settings_menu_order.py +++ b/projects/APPLaunch/tests/test_settings_menu_order.py @@ -58,7 +58,7 @@ def test_screen_keeps_lock_timeout_options(): screen_entries = re.findall( r'append_child\(\s*screen,\s*SettingEntry\{"([^"]+)"', builder ) - assert screen_entries == ["Brightness", "DarkTime"] + assert screen_entries == ["Brightness", "DarkTime", "Lock Screen"] timeouts = re.findall( r'append_child\(\s*dark_time,\s*SettingEntry\{"([^"]+)"', builder )