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 )