diff --git a/docs/solo_features/screen_lock/screen_lock.md b/docs/solo_features/screen_lock/screen_lock.md index 656a34c8..339eaa79 100644 --- a/docs/solo_features/screen_lock/screen_lock.md +++ b/docs/solo_features/screen_lock/screen_lock.md @@ -63,3 +63,15 @@ Fully autonomous, independent of Auto-lock and of any key combo: - **Magnet near (cover closed)** — locks and blanks the display immediately, no wake grace. - **Magnet away (cover opened)** — unlocks and wakes the display right away. + +--- + +### Lock-screen password + +Enable **LockPass** in **Settings › Display** to require a password when unlocking the device. + +Upon enabling the setting the keyboard will show, requiring you to submit a new password. +Attempting to unlock the device will then require the same password to be typed and submitted to reach the home screen. +Submitting an incorrect password clears the keyboard input. + +Disabling the setting clears the stored password and allows an instant unlock again. diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 0146ef4c..56a0b465 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2212,6 +2212,73 @@ void UITask::syncLockToHome() { if (home) static_cast(home)->setLocked(_locked); } +bool UITask::passwordLockEnabled() const { + // Is lock_screen_password setting not empty? + return _node_prefs && _node_prefs->lock_screen_password[0] != '\0'; +} + +void UITask::beginUnlockPrompt() { + _unlock_kb = true; // Track that keyboard is visible and is waiting for input + int max_len = _node_prefs ? (int)sizeof(_node_prefs->lock_screen_password) - 1 : 32; + _kb.begin("", max_len); + _kb.clearPlaceholders(); + _lock_wake_until = millis() + 5000; // keep the display on while typing + _next_refresh = 0; +} + +void UITask::cancelUnlockPrompt() { + _unlock_kb = false; + _next_refresh = 100; +} + +void UITask::handleUnlockKey(char c) { + auto res = _kb.handleInput(c); + if (res == KeyboardWidget::DONE) { // Process input on submit + if (_node_prefs && strcmp(_kb.buf, _node_prefs->lock_screen_password) == 0) { + // Match: Unlock + _unlock_kb = false; + _locked = false; + if (_display && !_display->isOn()) _display->turnOn(); + uint32_t aoff = autoOffMillis(); + if (aoff > 0) _auto_off = millis() + aoff; + syncLockToHome(); + } else { + // Invalid: Clear input and reset keyboard + _kb.len = 0; + _kb.buf[0] = '\0'; + _kb.cursor_pos = 0; + _kb.page = _kb.row = _kb.col = 0; + _kb.caps = _kb.caps_lock = false; + _next_refresh = 0; + } + } else if (res == KeyboardWidget::CANCELLED) { + cancelUnlockPrompt(); + } + _lock_wake_until = millis() + 5000; // keep the display on while typing +} + +void UITask::toggleLock() { + if (_unlock_kb) { + cancelUnlockPrompt(); + return; + } + if (_locked) { + if (passwordLockEnabled()) { // Prompt for password + beginUnlockPrompt(); + } else { // ...or unlock instantly + _locked = false; + if (_display && !_display->isOn()) _display->turnOn(); + uint32_t aoff = autoOffMillis(); + if (aoff > 0) _auto_off = millis() + aoff; + } + } else { // Device is currently unlocked -> lock it + _locked = true; + _lock_wake_until = millis() + 2000; // Briefly show lockscreen before blanking + } + syncLockToHome(); + _next_refresh = 0; +} + bool UITask::savePrefsIfDirty(bool& dirty) { if (!dirty) return false; the_mesh.savePrefs(); @@ -2561,15 +2628,7 @@ void UITask::pollCardKB() { // two-key combo, so it doesn't need the physical combo's extra 3x // repetition to guard against accidental triggering. if (_display && !_display->isOn()) _display->turnOn(); - _locked = !_locked; - if (_locked) { - _lock_wake_until = millis() + 2000; - } else { - uint32_t aoff = autoOffMillis(); - if (aoff > 0) _auto_off = millis() + aoff; - } - syncLockToHome(); - _next_refresh = 0; + toggleLock(); return; } else if (raw >= 0x80 && raw <= 0xAF) { // Fn+ -- open its accent popup char base = CARDKB_FN_BASE[raw - 0x80]; @@ -2623,6 +2682,7 @@ void UITask::pollHallSensor() { _hall_magnet_present = present; if (present) { // cover closed + cancelUnlockPrompt(); // a cover can't be over the password prompt _locked = true; syncLockToHome(); _lock_wake_until = 0; @@ -2663,15 +2723,7 @@ void UITask::loop() { if (_lock_seq_count >= 3) { _lock_seq_count = 0; _lock_seq_used = true; // suppress Back release click - _locked = !_locked; - if (_locked) { - _lock_wake_until = millis() + 2000; - } else { - if (_display && !_display->isOn()) _display->turnOn(); - uint32_t aoff = autoOffMillis(); - if (aoff > 0) _auto_off = millis() + aoff; - } - syncLockToHome(); + toggleLock(); } // eat the Enter — don't pass to curr } else { @@ -2864,7 +2916,12 @@ void UITask::loop() { } if (_kq_head != _kq_tail) { - if (!_locked && curr) { + if (_unlock_kb) { + // Lock-screen password keyboard: Consume every key press + char k; + while (dequeueKey(k)) handleUnlockKey(k); + _next_refresh = 100; // redraw immediately after key press + } else if (!_locked && curr) { // Apply the whole queued burst, then redraw once — N taps captured during // a blocking refresh become N navigation steps at the cost of one refresh. char k; @@ -2901,8 +2958,22 @@ void UITask::loop() { tickClockTools(); if (_display != NULL && _display->isOn()) { - if (_locked && (int32_t)(millis() - _lock_wake_until) >= 0) { + // Lock-screen password prompt + if (_locked && _unlock_kb && (int32_t)(millis() - _lock_wake_until) >= 0) { + cancelUnlockPrompt(); // Cancel unlock attempt on idle + _next_refresh = 0; + } + if (_locked && !_unlock_kb && (int32_t)(millis() - _lock_wake_until) >= 0) { _display->turnOff(); + } else if (_locked && _unlock_kb && millis() >= _next_refresh) { + // While the prompt is up the password keyboard replaces the lockscreen view + _display->startFrame(); + _kb.beginFrame(); + int delay_millis = _kb.render(*_display); + // Alert overlay on top // TODO: Is this necessary here? + // if (millis() < _alert_expiry) renderAlertOverlay(); + _display->endFrame(); + _next_refresh = millis() + delay_millis; } else if (_locked && millis() >= _next_refresh && home) { _display->startFrame(); home->render(*_display); @@ -2950,6 +3021,7 @@ void UITask::loop() { digitalWrite(PIN_LED, LOW); // turn off status LED with display to save power #endif if (_node_prefs && _node_prefs->auto_lock) { + cancelUnlockPrompt(); // idle-lock isn't a password prompt _locked = true; _lock_wake_until = 0; syncLockToHome(); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 52f5b020..8fe7056d 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -58,6 +58,8 @@ class UITask : public AbstractUITask { int _lock_seq_count; // Enter presses while Back held (lock/unlock sequence) unsigned long _lock_seq_ms; // millis() of last lock-sequence press (for timeout) bool _lock_seq_used; // true = suppress next back_btn CLICK (post-sequence release) + // True while the lock screen shows the on-screen keyboard and waits for submission + bool _unlock_kb = false; char _alert[80]; char _notif_mel_buf[220]; // persistent RTTTL buffer for custom notification melodies // Persistent RTTTL buffer for the bot !buzz command (see botBuzz()) -- sized @@ -288,6 +290,15 @@ private: // dedicated lock-screen code path in loop(). void syncLockToHome(); + // Whether a lockscreen password is set + bool passwordLockEnabled() const; + // Handles (un)locking and requesting password input when one is set + void toggleLock(); + void beginUnlockPrompt(); + void cancelUnlockPrompt(); + // Handles shortcuts during lockscreen password input + void handleUnlockKey(char c); + // Centred alert overlay (the showAlert() box). Wraps long text to up to // three lines inside the box instead of letting it overflow the border. // Shared by the normal render path and the lock screen (so a ringing @@ -566,7 +577,7 @@ public: #endif } - bool isBuzzerQuiet() { + bool isBuzzerQuiet() { #ifdef PIN_BUZZER return buzzer.isQuiet(); #else