Browse Source

[FL-2430] Automatic Desktop Locking (#1101)

* Add Auto Lock Time setting
* Update .gitignore
* Add value_index toolbox module
* Auto locking basic implementation
* Better AutoLock implementation, edge cases and cleanup
* Fix NULL pointer crash
* Turn off backlight shortly in locked mode
* Re-enable auto lock after pin lock
* Correctly handle start when pin locked
* Use timer to hide locked hint
* Use a single state variable instead of multiple bools
* Do not call update callback recursively
* Allow input when the Unlocked hint is shown
* Add a delay to backlight switch off while locking
* Better user input handling
* Switch backlight off after pin timeout
* Correct grammar in notification settings

Co-authored-by: あく <alleteam@gmail.com>
Georgii Surkov 3 years ago
parent
commit
917be9c6d3

+ 3 - 0
.gitignore

@@ -36,3 +36,6 @@ CMakeLists.txt
 
 # bundle output
 dist
+
+# kde
+.directory

+ 102 - 7
applications/desktop/desktop.c

@@ -1,6 +1,9 @@
 #include <storage/storage.h>
 #include <assets_icons.h>
+#include <gui/gui.h>
 #include <gui/view_stack.h>
+#include <notification/notification.h>
+#include <notification/notification_messages.h>
 #include <furi.h>
 #include <furi_hal.h>
 
@@ -13,6 +16,10 @@
 #include "desktop_i.h"
 #include "desktop_helpers.h"
 
+static void desktop_auto_lock_arm(Desktop*);
+static void desktop_auto_lock_inhibit(Desktop*);
+static void desktop_start_auto_lock_timer(Desktop*);
+
 static void desktop_loader_callback(const void* message, void* context) {
     furi_assert(context);
     Desktop* desktop = context;
@@ -37,9 +44,19 @@ static bool desktop_custom_event_callback(void* context, uint32_t event) {
     switch(event) {
     case DesktopGlobalBeforeAppStarted:
         animation_manager_unload_and_stall_animation(desktop->animation_manager);
+        desktop_auto_lock_inhibit(desktop);
         return true;
     case DesktopGlobalAfterAppFinished:
         animation_manager_load_and_continue_animation(desktop->animation_manager);
+        // TODO: Implement a message mechanism for loading settings and (optionally)
+        // locking and unlocking
+        LOAD_DESKTOP_SETTINGS(&desktop->settings);
+        desktop_auto_lock_arm(desktop);
+        return true;
+    case DesktopGlobalAutoLock:
+        if(!loader_is_locked(desktop->loader)) {
+            desktop_lock(desktop);
+        }
         return true;
     }
 
@@ -58,6 +75,63 @@ static void desktop_tick_event_callback(void* context) {
     scene_manager_handle_tick_event(app->scene_manager);
 }
 
+static void desktop_input_event_callback(const void* value, void* context) {
+    furi_assert(value);
+    furi_assert(context);
+    const InputEvent* event = value;
+    Desktop* desktop = context;
+    if(event->type == InputTypePress) {
+        desktop_start_auto_lock_timer(desktop);
+    }
+}
+
+static void desktop_auto_lock_timer_callback(void* context) {
+    furi_assert(context);
+    Desktop* desktop = context;
+    view_dispatcher_send_custom_event(desktop->view_dispatcher, DesktopGlobalAutoLock);
+}
+
+static void desktop_start_auto_lock_timer(Desktop* desktop) {
+    osTimerStart(
+        desktop->auto_lock_timer, furi_hal_ms_to_ticks(desktop->settings.auto_lock_delay_ms));
+}
+
+static void desktop_stop_auto_lock_timer(Desktop* desktop) {
+    osTimerStop(desktop->auto_lock_timer);
+}
+
+static void desktop_auto_lock_arm(Desktop* desktop) {
+    if(desktop->settings.auto_lock_delay_ms) {
+        desktop->input_events_subscription = furi_pubsub_subscribe(
+            desktop->input_events_pubsub, desktop_input_event_callback, desktop);
+        desktop_start_auto_lock_timer(desktop);
+    }
+}
+
+static void desktop_auto_lock_inhibit(Desktop* desktop) {
+    desktop_stop_auto_lock_timer(desktop);
+    if(desktop->input_events_subscription) {
+        furi_pubsub_unsubscribe(desktop->input_events_pubsub, desktop->input_events_subscription);
+        desktop->input_events_subscription = NULL;
+    }
+}
+
+void desktop_lock(Desktop* desktop) {
+    desktop_auto_lock_inhibit(desktop);
+    scene_manager_set_scene_state(
+        desktop->scene_manager, DesktopSceneLocked, SCENE_LOCKED_FIRST_ENTER);
+    scene_manager_next_scene(desktop->scene_manager, DesktopSceneLocked);
+    notification_message(desktop->notification, &sequence_display_off_delay_1000);
+}
+
+void desktop_unlock(Desktop* desktop) {
+    furi_hal_rtc_set_pin_fails(0);
+    desktop_helpers_unlock_system(desktop);
+    desktop_view_locked_unlock(desktop->locked_view);
+    scene_manager_search_and_switch_to_previous_scene(desktop->scene_manager, DesktopSceneMain);
+    desktop_auto_lock_arm(desktop);
+}
+
 Desktop* desktop_alloc() {
     Desktop* desktop = malloc(sizeof(Desktop));
 
@@ -146,9 +220,17 @@ Desktop* desktop_alloc() {
        animation_manager_is_animation_loaded(desktop->animation_manager)) {
         animation_manager_unload_and_stall_animation(desktop->animation_manager);
     }
+
+    desktop->notification = furi_record_open("notification");
     desktop->app_start_stop_subscription = furi_pubsub_subscribe(
         loader_get_pubsub(desktop->loader), desktop_loader_callback, desktop);
 
+    desktop->input_events_pubsub = furi_record_open("input_events");
+    desktop->input_events_subscription = NULL;
+
+    desktop->auto_lock_timer =
+        osTimerNew(desktop_auto_lock_timer_callback, osTimerOnce, desktop, NULL);
+
     return desktop;
 }
 
@@ -157,8 +239,17 @@ void desktop_free(Desktop* desktop) {
 
     furi_pubsub_unsubscribe(
         loader_get_pubsub(desktop->loader), desktop->app_start_stop_subscription);
+
+    if(desktop->input_events_subscription) {
+        furi_pubsub_unsubscribe(desktop->input_events_pubsub, desktop->input_events_subscription);
+        desktop->input_events_subscription = NULL;
+    }
+
     desktop->loader = NULL;
+    desktop->input_events_pubsub = NULL;
     furi_record_close("loader");
+    furi_record_close("notification");
+    furi_record_close("input_events");
 
     view_dispatcher_remove_view(desktop->view_dispatcher, DesktopViewIdMain);
     view_dispatcher_remove_view(desktop->view_dispatcher, DesktopViewIdLockMenu);
@@ -191,6 +282,8 @@ void desktop_free(Desktop* desktop) {
 
     furi_record_close("menu");
 
+    osTimerDelete(desktop->auto_lock_timer);
+
     free(desktop);
 }
 
@@ -214,14 +307,16 @@ int32_t desktop_srv(void* p) {
 
     scene_manager_next_scene(desktop->scene_manager, DesktopSceneMain);
 
-    if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagLock)) {
-        if(desktop->settings.pin_code.length > 0) {
-            scene_manager_set_scene_state(
-                desktop->scene_manager, DesktopSceneLocked, SCENE_LOCKED_FIRST_ENTER);
-            scene_manager_next_scene(desktop->scene_manager, DesktopSceneLocked);
-        } else {
-            furi_hal_rtc_reset_flag(FuriHalRtcFlagLock);
+    if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagLock) && !desktop->settings.pin_code.length) {
+        furi_hal_rtc_reset_flag(FuriHalRtcFlagLock);
+    }
+
+    if(!furi_hal_rtc_is_flag_set(FuriHalRtcFlagLock)) {
+        if(!loader_is_locked(desktop->loader)) {
+            desktop_auto_lock_arm(desktop);
         }
+    } else {
+        desktop_lock(desktop);
     }
 
     if(desktop_is_first_start()) {

+ 9 - 0
applications/desktop/desktop_i.h

@@ -19,6 +19,7 @@
 #include <gui/scene_manager.h>
 
 #include <loader/loader.h>
+#include <notification/notification_app.h>
 
 #define STATUS_BAR_Y_SHIFT 13
 
@@ -59,10 +60,18 @@ struct Desktop {
     ViewPort* lock_viewport;
 
     AnimationManager* animation_manager;
+
     Loader* loader;
+    NotificationApp* notification;
+
     FuriPubSubSubscription* app_start_stop_subscription;
+    FuriPubSub* input_events_pubsub;
+    FuriPubSubSubscription* input_events_subscription;
+    osTimerId_t auto_lock_timer;
 };
 
 Desktop* desktop_alloc();
 
 void desktop_free(Desktop* desktop);
+void desktop_lock(Desktop* desktop);
+void desktop_unlock(Desktop* desktop);

+ 2 - 1
applications/desktop/desktop_settings/desktop_settings.h

@@ -5,7 +5,7 @@
 #include <stdbool.h>
 #include <toolbox/saved_struct.h>
 
-#define DESKTOP_SETTINGS_VER (1)
+#define DESKTOP_SETTINGS_VER (2)
 #define DESKTOP_SETTINGS_PATH "/int/desktop.settings"
 #define DESKTOP_SETTINGS_MAGIC (0x17)
 #define PIN_MAX_LENGTH 12
@@ -39,6 +39,7 @@ typedef struct {
 typedef struct {
     uint16_t favorite;
     PinCode pin_code;
+    uint32_t auto_lock_delay_ms;
 } DesktopSettings;
 
 static inline bool pins_are_equal(const PinCode* pin_code1, const PinCode* pin_code2) {

+ 7 - 0
applications/desktop/desktop_settings/desktop_settings_app.c

@@ -36,12 +36,17 @@ DesktopSettingsApp* desktop_settings_app_alloc() {
 
     app->popup = popup_alloc();
     app->submenu = submenu_alloc();
+    app->variable_item_list = variable_item_list_alloc();
     app->pin_input_view = desktop_view_pin_input_alloc();
     app->pin_setup_howto_view = desktop_settings_view_pin_setup_howto_alloc();
     app->pin_setup_howto2_view = desktop_settings_view_pin_setup_howto2_alloc();
 
     view_dispatcher_add_view(
         app->view_dispatcher, DesktopSettingsAppViewMenu, submenu_get_view(app->submenu));
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        DesktopSettingsAppViewVarItemList,
+        variable_item_list_get_view(app->variable_item_list));
     view_dispatcher_add_view(
         app->view_dispatcher, DesktopSettingsAppViewIdPopup, popup_get_view(app->popup));
     view_dispatcher_add_view(
@@ -63,10 +68,12 @@ void desktop_settings_app_free(DesktopSettingsApp* app) {
     furi_assert(app);
     // Variable item list
     view_dispatcher_remove_view(app->view_dispatcher, DesktopSettingsAppViewMenu);
+    view_dispatcher_remove_view(app->view_dispatcher, DesktopSettingsAppViewVarItemList);
     view_dispatcher_remove_view(app->view_dispatcher, DesktopSettingsAppViewIdPopup);
     view_dispatcher_remove_view(app->view_dispatcher, DesktopSettingsAppViewIdPinInput);
     view_dispatcher_remove_view(app->view_dispatcher, DesktopSettingsAppViewIdPinSetupHowto);
     view_dispatcher_remove_view(app->view_dispatcher, DesktopSettingsAppViewIdPinSetupHowto2);
+    variable_item_list_free(app->variable_item_list);
     submenu_free(app->submenu);
     popup_free(app->popup);
     desktop_view_pin_input_free(app->pin_input_view);

+ 3 - 0
applications/desktop/desktop_settings/desktop_settings_app.h

@@ -5,6 +5,7 @@
 #include <gui/view_dispatcher.h>
 #include <gui/scene_manager.h>
 #include <gui/modules/submenu.h>
+#include <gui/modules/variable_item_list.h>
 
 #include "desktop_settings.h"
 #include "desktop/views/desktop_view_pin_input.h"
@@ -13,6 +14,7 @@
 
 typedef enum {
     DesktopSettingsAppViewMenu,
+    DesktopSettingsAppViewVarItemList,
     DesktopSettingsAppViewIdPopup,
     DesktopSettingsAppViewIdPinInput,
     DesktopSettingsAppViewIdPinSetupHowto,
@@ -25,6 +27,7 @@ typedef struct {
     Gui* gui;
     SceneManager* scene_manager;
     ViewDispatcher* view_dispatcher;
+    VariableItemList* variable_item_list;
     Submenu* submenu;
     Popup* popup;
     DesktopViewPinInput* pin_input_view;

+ 49 - 15
applications/desktop/desktop_settings/scenes/desktop_settings_scene_start.c

@@ -1,35 +1,65 @@
 #include <applications.h>
+#include <lib/toolbox/value_index.h>
 
 #include "../desktop_settings_app.h"
 #include "desktop_settings_scene.h"
 
 #define SCENE_EVENT_SELECT_FAVORITE 0
 #define SCENE_EVENT_SELECT_PIN_SETUP 1
+#define SCENE_EVENT_SELECT_AUTO_LOCK_DELAY 2
 
-static void desktop_settings_scene_start_submenu_callback(void* context, uint32_t index) {
+#define AUTO_LOCK_DELAY_COUNT 6
+const char* const auto_lock_delay_text[AUTO_LOCK_DELAY_COUNT] = {
+    "OFF",
+    "30s",
+    "60s",
+    "2min",
+    "5min",
+    "10min",
+};
+
+const uint32_t auto_lock_delay_value[AUTO_LOCK_DELAY_COUNT] =
+    {0, 30000, 60000, 120000, 300000, 600000};
+
+static void desktop_settings_scene_start_var_list_enter_callback(void* context, uint32_t index) {
     DesktopSettingsApp* app = context;
     view_dispatcher_send_custom_event(app->view_dispatcher, index);
 }
 
+static void desktop_settings_scene_start_auto_lock_delay_changed(VariableItem* item) {
+    DesktopSettingsApp* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    variable_item_set_current_value_text(item, auto_lock_delay_text[index]);
+    app->settings.auto_lock_delay_ms = auto_lock_delay_value[index];
+}
+
 void desktop_settings_scene_start_on_enter(void* context) {
     DesktopSettingsApp* app = context;
-    Submenu* submenu = app->submenu;
+    VariableItemList* variable_item_list = app->variable_item_list;
 
-    submenu_add_item(
-        submenu,
-        "Favorite App",
-        SCENE_EVENT_SELECT_FAVORITE,
-        desktop_settings_scene_start_submenu_callback,
-        app);
+    VariableItem* item;
+    uint8_t value_index;
 
-    submenu_add_item(
-        submenu,
-        "PIN Setup",
-        SCENE_EVENT_SELECT_PIN_SETUP,
-        desktop_settings_scene_start_submenu_callback,
+    variable_item_list_add(variable_item_list, "Favorite App", 1, NULL, NULL);
+
+    variable_item_list_add(variable_item_list, "PIN Setup", 1, NULL, NULL);
+
+    item = variable_item_list_add(
+        variable_item_list,
+        "Auto Lock Time",
+        AUTO_LOCK_DELAY_COUNT,
+        desktop_settings_scene_start_auto_lock_delay_changed,
         app);
 
-    view_dispatcher_switch_to_view(app->view_dispatcher, DesktopSettingsAppViewMenu);
+    variable_item_list_set_enter_callback(
+        variable_item_list, desktop_settings_scene_start_var_list_enter_callback, app);
+    value_index = value_index_uint32(
+        app->settings.auto_lock_delay_ms, auto_lock_delay_value, AUTO_LOCK_DELAY_COUNT);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, auto_lock_delay_text[value_index]);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, DesktopSettingsAppViewVarItemList);
 }
 
 bool desktop_settings_scene_start_on_event(void* context, SceneManagerEvent event) {
@@ -46,6 +76,9 @@ bool desktop_settings_scene_start_on_event(void* context, SceneManagerEvent even
             scene_manager_next_scene(app->scene_manager, DesktopSettingsAppScenePinMenu);
             consumed = true;
             break;
+        case SCENE_EVENT_SELECT_AUTO_LOCK_DELAY:
+            consumed = true;
+            break;
         }
     }
     return consumed;
@@ -53,5 +86,6 @@ bool desktop_settings_scene_start_on_event(void* context, SceneManagerEvent even
 
 void desktop_settings_scene_start_on_exit(void* context) {
     DesktopSettingsApp* app = context;
-    submenu_reset(app->submenu);
+    variable_item_list_reset(app->variable_item_list);
+    SAVE_DESKTOP_SETTINGS(&app->settings);
 }

+ 2 - 6
applications/desktop/scenes/desktop_scene_lock_menu.c

@@ -48,17 +48,13 @@ bool desktop_scene_lock_menu_on_event(void* context, SceneManagerEvent event) {
         switch(event.event) {
         case DesktopLockMenuEventLock:
             scene_manager_set_scene_state(desktop->scene_manager, DesktopSceneLockMenu, 0);
-            scene_manager_set_scene_state(
-                desktop->scene_manager, DesktopSceneLocked, SCENE_LOCKED_FIRST_ENTER);
-            scene_manager_next_scene(desktop->scene_manager, DesktopSceneLocked);
+            desktop_lock(desktop);
             consumed = true;
             break;
         case DesktopLockMenuEventPinLock:
             if(desktop->settings.pin_code.length > 0) {
                 furi_hal_rtc_set_flag(FuriHalRtcFlagLock);
-                scene_manager_set_scene_state(
-                    desktop->scene_manager, DesktopSceneLocked, SCENE_LOCKED_FIRST_ENTER);
-                scene_manager_next_scene(desktop->scene_manager, DesktopSceneLocked);
+                desktop_lock(desktop);
             } else {
                 LoaderStatus status =
                     loader_start(desktop->loader, "Desktop", DESKTOP_SETTINGS_RUN_PIN_SETUP_ARG);

+ 4 - 4
applications/desktop/scenes/desktop_scene_locked.c

@@ -81,13 +81,13 @@ bool desktop_scene_locked_on_event(void* context, SceneManagerEvent event) {
     if(event.type == SceneManagerEventTypeCustom) {
         switch(event.event) {
         case DesktopLockedEventUnlocked:
-            furi_hal_rtc_set_pin_fails(0);
-            desktop_helpers_unlock_system(desktop);
-            scene_manager_search_and_switch_to_previous_scene(
-                desktop->scene_manager, DesktopSceneMain);
+            desktop_unlock(desktop);
             consumed = true;
             break;
         case DesktopLockedEventUpdate:
+            if(desktop_view_locked_is_locked_hint_visible(desktop->locked_view)) {
+                notification_message(desktop->notification, &sequence_display_off);
+            }
             desktop_view_locked_update(desktop->locked_view);
             consumed = true;
             break;

+ 2 - 5
applications/desktop/scenes/desktop_scene_pin_input.c

@@ -129,16 +129,13 @@ bool desktop_scene_pin_input_on_event(void* context, SceneManagerEvent event) {
             consumed = true;
             break;
         case DesktopPinInputEventUnlocked:
-            desktop_view_locked_unlock(desktop->locked_view);
-            furi_hal_rtc_set_pin_fails(0);
-            desktop_helpers_unlock_system(desktop);
-            scene_manager_search_and_switch_to_previous_scene(
-                desktop->scene_manager, DesktopSceneMain);
+            desktop_unlock(desktop);
             consumed = true;
             break;
         case DesktopPinInputEventBack:
             scene_manager_search_and_switch_to_previous_scene(
                 desktop->scene_manager, DesktopSceneLocked);
+            notification_message(desktop->notification, &sequence_display_off);
             consumed = true;
             break;
         }

+ 1 - 0
applications/desktop/views/desktop_events.h

@@ -38,4 +38,5 @@ typedef enum {
     // Global events
     DesktopGlobalBeforeAppStarted,
     DesktopGlobalAfterAppFinished,
+    DesktopGlobalAutoLock,
 } DesktopEvent;

+ 74 - 66
applications/desktop/views/desktop_view_locked.c

@@ -11,6 +11,7 @@
 #include "desktop_view_locked.h"
 
 #define DOOR_MOVING_INTERVAL_MS (1000 / 16)
+#define LOCKED_HINT_TIMEOUT_MS (1000)
 #define UNLOCKED_HINT_TIMEOUT_MS (2000)
 
 #define DOOR_OFFSET_START -55
@@ -32,14 +33,18 @@ struct DesktopViewLocked {
     uint32_t lock_lastpress;
 };
 
+typedef enum {
+    DesktopViewLockedStateUnlocked,
+    DesktopViewLockedStateLocked,
+    DesktopViewLockedStateDoorsClosing,
+    DesktopViewLockedStateLockedHintShown,
+    DesktopViewLockedStateUnlockedHintShown
+} DesktopViewLockedState;
+
 typedef struct {
-    uint32_t hint_icon_expire_at;
-    bool unlocked_hint;
-    bool locked;
     bool pin_locked;
-
     int8_t door_offset;
-    bool doors_closing;
+    DesktopViewLockedState view_state;
 } DesktopViewLockedModel;
 
 void desktop_view_locked_set_callback(
@@ -78,51 +83,54 @@ static bool desktop_view_locked_doors_move(DesktopViewLockedModel* model) {
 
 static void desktop_view_locked_update_hint_icon_timeout(DesktopViewLocked* locked_view) {
     DesktopViewLockedModel* model = view_get_model(locked_view->view);
-    model->hint_icon_expire_at = osKernelGetTickCount() + osKernelGetTickFreq();
-    view_commit_model(locked_view->view, true);
+    const bool change_state = (model->view_state == DesktopViewLockedStateLocked) &&
+                              !model->pin_locked;
+    if(change_state) {
+        model->view_state = DesktopViewLockedStateLockedHintShown;
+    }
+    view_commit_model(locked_view->view, change_state);
+    xTimerChangePeriod(locked_view->timer, pdMS_TO_TICKS(LOCKED_HINT_TIMEOUT_MS), portMAX_DELAY);
 }
 
 void desktop_view_locked_update(DesktopViewLocked* locked_view) {
-    bool stop_timer = false;
-
     DesktopViewLockedModel* model = view_get_model(locked_view->view);
-    if(model->locked) {
-        model->doors_closing = desktop_view_locked_doors_move(model);
-        stop_timer = !model->doors_closing;
-    } else {
-        model->unlocked_hint = false;
-        stop_timer = true;
+    DesktopViewLockedState view_state = model->view_state;
+
+    if(view_state == DesktopViewLockedStateDoorsClosing &&
+       !desktop_view_locked_doors_move(model)) {
+        model->view_state = DesktopViewLockedStateLocked;
+    } else if(view_state == DesktopViewLockedStateLockedHintShown) {
+        model->view_state = DesktopViewLockedStateLocked;
+    } else if(view_state == DesktopViewLockedStateUnlockedHintShown) {
+        model->view_state = DesktopViewLockedStateUnlocked;
     }
+
     view_commit_model(locked_view->view, true);
 
-    if(stop_timer) {
+    if(view_state != DesktopViewLockedStateDoorsClosing) {
         xTimerStop(locked_view->timer, portMAX_DELAY);
     }
 }
 
 static void desktop_view_locked_draw(Canvas* canvas, void* model) {
     DesktopViewLockedModel* m = model;
-    uint32_t now = osKernelGetTickCount();
+    DesktopViewLockedState view_state = m->view_state;
     canvas_set_color(canvas, ColorBlack);
 
-    if(m->locked) {
-        if(m->doors_closing) {
-            desktop_view_locked_doors_draw(canvas, m);
-            canvas_set_font(canvas, FontPrimary);
-            elements_multiline_text_framed(canvas, 42, 30 + STATUS_BAR_Y_SHIFT, "Locked");
-        } else if((now < m->hint_icon_expire_at) && !m->pin_locked) {
-            canvas_set_font(canvas, FontSecondary);
-            elements_bold_rounded_frame(canvas, 14, 2 + STATUS_BAR_Y_SHIFT, 99, 48);
-            elements_multiline_text(canvas, 65, 20 + STATUS_BAR_Y_SHIFT, "To unlock\npress:");
-            canvas_draw_icon(canvas, 65, 36 + STATUS_BAR_Y_SHIFT, &I_Back3_45x8);
-            canvas_draw_icon(canvas, 16, 7 + STATUS_BAR_Y_SHIFT, &I_WarningDolphin_45x42);
-            canvas_draw_dot(canvas, 17, 61);
-        }
-    } else {
-        if(m->unlocked_hint) {
-            canvas_set_font(canvas, FontPrimary);
-            elements_multiline_text_framed(canvas, 42, 30 + STATUS_BAR_Y_SHIFT, "Unlocked");
-        }
+    if(view_state == DesktopViewLockedStateDoorsClosing) {
+        desktop_view_locked_doors_draw(canvas, m);
+        canvas_set_font(canvas, FontPrimary);
+        elements_multiline_text_framed(canvas, 42, 30 + STATUS_BAR_Y_SHIFT, "Locked");
+    } else if(view_state == DesktopViewLockedStateLockedHintShown) {
+        canvas_set_font(canvas, FontSecondary);
+        elements_bold_rounded_frame(canvas, 14, 2 + STATUS_BAR_Y_SHIFT, 99, 48);
+        elements_multiline_text(canvas, 65, 20 + STATUS_BAR_Y_SHIFT, "To unlock\npress:");
+        canvas_draw_icon(canvas, 65, 36 + STATUS_BAR_Y_SHIFT, &I_Back3_45x8);
+        canvas_draw_icon(canvas, 16, 7 + STATUS_BAR_Y_SHIFT, &I_WarningDolphin_45x42);
+        canvas_draw_dot(canvas, 17, 61);
+    } else if(view_state == DesktopViewLockedStateUnlockedHintShown) {
+        canvas_set_font(canvas, FontPrimary);
+        elements_multiline_text_framed(canvas, 42, 30 + STATUS_BAR_Y_SHIFT, "Unlocked");
     }
 }
 
@@ -134,43 +142,38 @@ View* desktop_view_locked_get_view(DesktopViewLocked* locked_view) {
 static bool desktop_view_locked_input(InputEvent* event, void* context) {
     furi_assert(event);
     furi_assert(context);
-    DesktopViewLocked* locked_view = context;
-    bool locked = false;
-    bool locked_with_pin = false;
-    bool doors_closing = false;
-    uint32_t press_time = xTaskGetTickCount();
-
-    {
-        DesktopViewLockedModel* model = view_get_model(locked_view->view);
-        bool changed = false;
-        locked = model->locked;
-        locked_with_pin = model->pin_locked;
-        doors_closing = model->doors_closing;
-        if(!locked && model->unlocked_hint && event->type == InputTypePress) {
-            model->unlocked_hint = false;
-            changed = true;
-        }
-        view_commit_model(locked_view->view, changed);
-    }
 
-    if(!locked || doors_closing || (event->type != InputTypeShort)) {
-        return locked;
+    bool is_changed = false;
+    const uint32_t press_time = xTaskGetTickCount();
+    DesktopViewLocked* locked_view = context;
+    DesktopViewLockedModel* model = view_get_model(locked_view->view);
+    if(model->view_state == DesktopViewLockedStateUnlockedHintShown &&
+       event->type == InputTypePress) {
+        model->view_state = DesktopViewLockedStateUnlocked;
+        is_changed = true;
     }
+    const DesktopViewLockedState view_state = model->view_state;
+    const bool pin_locked = model->pin_locked;
+    view_commit_model(locked_view->view, is_changed);
 
-    if(locked_with_pin) {
+    if(view_state == DesktopViewLockedStateUnlocked || event->type != InputTypeShort) {
+        return view_state != DesktopViewLockedStateUnlocked;
+    } else if(view_state == DesktopViewLockedStateLocked && pin_locked) {
         locked_view->callback(DesktopLockedEventShowPinInput, locked_view->context);
-    } else {
+    } else if(
+        view_state == DesktopViewLockedStateLocked ||
+        view_state == DesktopViewLockedStateLockedHintShown) {
         if(press_time - locked_view->lock_lastpress > UNLOCK_RST_TIMEOUT) {
             locked_view->lock_lastpress = press_time;
             locked_view->lock_count = 0;
         }
 
         desktop_view_locked_update_hint_icon_timeout(locked_view);
+
         if(event->key == InputKeyBack) {
             locked_view->lock_lastpress = press_time;
             locked_view->lock_count++;
             if(locked_view->lock_count == UNLOCK_CNT) {
-                desktop_view_locked_unlock(locked_view);
                 locked_view->callback(DesktopLockedEventUnlocked, locked_view->context);
             }
         } else {
@@ -180,7 +183,7 @@ static bool desktop_view_locked_input(InputEvent* event, void* context) {
         locked_view->lock_lastpress = press_time;
     }
 
-    return locked;
+    return true;
 }
 
 DesktopViewLocked* desktop_view_locked_alloc() {
@@ -189,7 +192,6 @@ DesktopViewLocked* desktop_view_locked_alloc() {
     locked_view->timer =
         xTimerCreate(NULL, 1000 / 16, pdTRUE, locked_view, locked_view_timer_callback);
 
-    locked_view->view = view_alloc();
     view_allocate_model(locked_view->view, ViewModelTypeLocking, sizeof(DesktopViewLockedModel));
     view_set_context(locked_view->view, locked_view);
     view_set_draw_callback(locked_view->view, desktop_view_locked_draw);
@@ -207,7 +209,8 @@ void desktop_view_locked_free(DesktopViewLocked* locked_view) {
 
 void desktop_view_locked_close_doors(DesktopViewLocked* locked_view) {
     DesktopViewLockedModel* model = view_get_model(locked_view->view);
-    model->doors_closing = true;
+    furi_assert(model->view_state == DesktopViewLockedStateLocked);
+    model->view_state = DesktopViewLockedStateDoorsClosing;
     model->door_offset = DOOR_OFFSET_START;
     view_commit_model(locked_view->view, true);
     xTimerChangePeriod(locked_view->timer, pdMS_TO_TICKS(DOOR_MOVING_INTERVAL_MS), portMAX_DELAY);
@@ -215,19 +218,24 @@ void desktop_view_locked_close_doors(DesktopViewLocked* locked_view) {
 
 void desktop_view_locked_lock(DesktopViewLocked* locked_view, bool pin_locked) {
     DesktopViewLockedModel* model = view_get_model(locked_view->view);
-    model->locked = true;
+    furi_assert(model->view_state == DesktopViewLockedStateUnlocked);
+    model->view_state = DesktopViewLockedStateLocked;
     model->pin_locked = pin_locked;
     view_commit_model(locked_view->view, true);
 }
 
 void desktop_view_locked_unlock(DesktopViewLocked* locked_view) {
-    furi_assert(locked_view);
-
     locked_view->lock_count = 0;
     DesktopViewLockedModel* model = view_get_model(locked_view->view);
-    model->locked = false;
+    model->view_state = DesktopViewLockedStateUnlockedHintShown;
     model->pin_locked = false;
-    model->unlocked_hint = true;
     view_commit_model(locked_view->view, true);
     xTimerChangePeriod(locked_view->timer, pdMS_TO_TICKS(UNLOCKED_HINT_TIMEOUT_MS), portMAX_DELAY);
 }
+
+bool desktop_view_locked_is_locked_hint_visible(DesktopViewLocked* locked_view) {
+    DesktopViewLockedModel* model = view_get_model(locked_view->view);
+    const DesktopViewLockedState view_state = model->view_state;
+    view_commit_model(locked_view->view, false);
+    return view_state == DesktopViewLockedStateLockedHintShown;
+}

+ 1 - 0
applications/desktop/views/desktop_view_locked.h

@@ -19,3 +19,4 @@ void desktop_view_locked_free(DesktopViewLocked* locked_view);
 void desktop_view_locked_lock(DesktopViewLocked* locked_view, bool pin_locked);
 void desktop_view_locked_unlock(DesktopViewLocked* locked_view);
 void desktop_view_locked_close_doors(DesktopViewLocked* locked_view);
+bool desktop_view_locked_is_locked_hint_visible(DesktopViewLocked* locked_view);

+ 7 - 1
applications/notification/notification_app.c

@@ -1,6 +1,7 @@
 #include <furi.h>
 #include <furi_hal.h>
 #include <storage/storage.h>
+#include <input/input.h>
 #include "notification.h"
 #include "notification_messages.h"
 #include "notification_app.h"
@@ -416,8 +417,13 @@ static bool notification_save_settings(NotificationApp* app) {
 };
 
 static void input_event_callback(const void* value, void* context) {
+    furi_assert(value);
+    furi_assert(context);
+    const InputEvent* event = value;
     NotificationApp* app = context;
-    notification_message(app, &sequence_display_on);
+    if(event->type == InputTypePress) {
+        notification_message(app, &sequence_display_on);
+    }
 }
 
 // App alloc

+ 8 - 2
applications/notification/notification_messages.c

@@ -21,7 +21,7 @@ const NotificationMessage message_display_lock = {
 };
 
 const NotificationMessage message_display_unlock = {
-    .type = NotificationMessageTypeLedDisplayLock,
+    .type = NotificationMessageTypeLedDisplayUnlock,
     .data.led.value = 0x00,
 };
 
@@ -208,6 +208,12 @@ const NotificationSequence sequence_display_unlock = {
     NULL,
 };
 
+const NotificationSequence sequence_display_off_delay_1000 = {
+    &message_delay_1000,
+    &message_display_off,
+    NULL,
+};
+
 // Charging
 const NotificationSequence sequence_charging = {
     &message_red_255,
@@ -436,4 +442,4 @@ const NotificationSequence sequence_audiovisual_alert = {
     &message_sound_off,
     &message_vibro_off,
     NULL,
-};
+};

+ 2 - 0
applications/notification/notification_messages.h

@@ -78,6 +78,8 @@ extern const NotificationSequence sequence_display_off;
 extern const NotificationSequence sequence_display_lock;
 /** Display: backlight always on unlock */
 extern const NotificationSequence sequence_display_unlock;
+/** Display: backlight force off after a delay of 1000ms */
+extern const NotificationSequence sequence_display_off_delay_1000;
 
 // Charging
 extern const NotificationSequence sequence_charging;

+ 6 - 43
applications/notification/notification_settings_app.c

@@ -2,6 +2,7 @@
 #include "notification_app.h"
 #include <gui/modules/variable_item_list.h>
 #include <gui/view_dispatcher.h>
+#include <lib/toolbox/value_index.h>
 
 #define MAX_NOTIFICATION_SETTINGS 4
 
@@ -63,44 +64,6 @@ const char* const vibro_text[VIBRO_COUNT] = {
 };
 const bool vibro_value[VIBRO_COUNT] = {false, true};
 
-uint8_t float_value_index(const float value, const float values[], uint8_t values_count) {
-    const float epsilon = 0.01f;
-    float last_value = values[0];
-    uint8_t index = 0;
-    for(uint8_t i = 0; i < values_count; i++) {
-        if((value >= last_value - epsilon) && (value <= values[i] + epsilon)) {
-            index = i;
-            break;
-        }
-        last_value = values[i];
-    }
-    return index;
-}
-
-uint8_t uint32_value_index(const uint32_t value, const uint32_t values[], uint8_t values_count) {
-    int64_t last_value = INT64_MIN;
-    uint8_t index = 0;
-    for(uint8_t i = 0; i < values_count; i++) {
-        if((value >= last_value) && (value <= values[i])) {
-            index = i;
-            break;
-        }
-        last_value = values[i];
-    }
-    return index;
-}
-
-uint8_t bool_value_index(const bool value, const bool values[], uint8_t values_count) {
-    uint8_t index = 0;
-    for(uint8_t i = 0; i < values_count; i++) {
-        if(value == values[i]) {
-            index = i;
-            break;
-        }
-    }
-    return index;
-}
-
 static void backlight_changed(VariableItem* item) {
     NotificationAppSettings* app = variable_item_get_context(item);
     uint8_t index = variable_item_get_current_value_index(item);
@@ -164,21 +127,21 @@ static NotificationAppSettings* alloc_settings() {
 
     item = variable_item_list_add(
         app->variable_item_list, "LCD Backlight", BACKLIGHT_COUNT, backlight_changed, app);
-    value_index = float_value_index(
+    value_index = value_index_float(
         app->notification->settings.display_brightness, backlight_value, BACKLIGHT_COUNT);
     variable_item_set_current_value_index(item, value_index);
     variable_item_set_current_value_text(item, backlight_text[value_index]);
 
     item = variable_item_list_add(
         app->variable_item_list, "Backlight Time", DELAY_COUNT, screen_changed, app);
-    value_index = uint32_value_index(
+    value_index = value_index_uint32(
         app->notification->settings.display_off_delay_ms, delay_value, DELAY_COUNT);
     variable_item_set_current_value_index(item, value_index);
     variable_item_set_current_value_text(item, delay_text[value_index]);
 
     item = variable_item_list_add(
         app->variable_item_list, "LED Brightness", BACKLIGHT_COUNT, led_changed, app);
-    value_index = float_value_index(
+    value_index = value_index_float(
         app->notification->settings.led_brightness, backlight_value, BACKLIGHT_COUNT);
     variable_item_set_current_value_index(item, value_index);
     variable_item_set_current_value_text(item, backlight_text[value_index]);
@@ -186,13 +149,13 @@ static NotificationAppSettings* alloc_settings() {
     item = variable_item_list_add(
         app->variable_item_list, "Volume", VOLUME_COUNT, volume_changed, app);
     value_index =
-        float_value_index(app->notification->settings.speaker_volume, volume_value, VOLUME_COUNT);
+        value_index_float(app->notification->settings.speaker_volume, volume_value, VOLUME_COUNT);
     variable_item_set_current_value_index(item, value_index);
     variable_item_set_current_value_text(item, volume_text[value_index]);
 
     item =
         variable_item_list_add(app->variable_item_list, "Vibro", VIBRO_COUNT, vibro_changed, app);
-    value_index = bool_value_index(app->notification->settings.vibro_on, vibro_value, VIBRO_COUNT);
+    value_index = value_index_bool(app->notification->settings.vibro_on, vibro_value, VIBRO_COUNT);
     variable_item_set_current_value_index(item, value_index);
     variable_item_set_current_value_text(item, vibro_text[value_index]);
 

+ 2 - 15
applications/system/system_settings.c

@@ -1,19 +1,6 @@
 #include "system_settings.h"
 #include <loader/loader.h>
-
-static uint8_t
-    uint32_value_index(const uint32_t value, const uint32_t values[], uint8_t values_count) {
-    int64_t last_value = INT64_MIN;
-    uint8_t index = 0;
-    for(uint8_t i = 0; i < values_count; i++) {
-        if((value >= last_value) && (value <= values[i])) {
-            index = i;
-            break;
-        }
-        last_value = values[i];
-    }
-    return index;
-}
+#include <lib/toolbox/value_index.h>
 
 const char* const log_level_text[] = {
     "Default",
@@ -80,7 +67,7 @@ SystemSettings* system_settings_alloc() {
 
     item = variable_item_list_add(
         app->var_item_list, "Log Level", COUNT_OF(log_level_text), log_level_changed, app);
-    value_index = uint32_value_index(
+    value_index = value_index_uint32(
         furi_hal_rtc_get_log_level(), log_level_value, COUNT_OF(log_level_text));
     variable_item_set_current_value_index(item, value_index);
     variable_item_set_current_value_text(item, log_level_text[value_index]);

+ 4 - 0
firmware/targets/f7/furi_hal/furi_hal_delay.c

@@ -26,6 +26,10 @@ uint32_t furi_hal_get_tick(void) {
     return tick_cnt;
 }
 
+uint32_t furi_hal_ms_to_ticks(float milliseconds) {
+    return milliseconds / (1000.0f / osKernelGetTickFreq());
+}
+
 void furi_hal_delay_us(float microseconds) {
     uint32_t start = DWT->CYCCNT;
     uint32_t time_ticks = microseconds * furi_hal_delay_instructions_per_microsecond();

+ 7 - 0
firmware/targets/furi_hal_include/furi_hal_delay.h

@@ -31,6 +31,13 @@ void furi_hal_tick(void);
  */
 uint32_t furi_hal_get_tick(void);
 
+/** Convert milliseconds to ticks
+ *
+ * @param[in]   milliseconds    time in milliseconds
+ * @return      time in ticks
+ */
+uint32_t furi_hal_ms_to_ticks(float milliseconds);
+
 /** Delay in milliseconds
  * @warning    Cannot be used from ISR
  *

+ 39 - 0
lib/toolbox/value_index.c

@@ -0,0 +1,39 @@
+#include "value_index.h"
+
+uint8_t value_index_uint32(const uint32_t value, const uint32_t values[], uint8_t values_count) {
+    int64_t last_value = INT64_MIN;
+    uint8_t index = 0;
+    for(uint8_t i = 0; i < values_count; i++) {
+        if((value >= last_value) && (value <= values[i])) {
+            index = i;
+            break;
+        }
+        last_value = values[i];
+    }
+    return index;
+}
+
+uint8_t value_index_float(const float value, const float values[], uint8_t values_count) {
+    const float epsilon = 0.01f;
+    float last_value = values[0];
+    uint8_t index = 0;
+    for(uint8_t i = 0; i < values_count; i++) {
+        if((value >= last_value - epsilon) && (value <= values[i] + epsilon)) {
+            index = i;
+            break;
+        }
+        last_value = values[i];
+    }
+    return index;
+}
+
+uint8_t value_index_bool(const bool value, const bool values[], uint8_t values_count) {
+    uint8_t index = 0;
+    for(uint8_t i = 0; i < values_count; i++) {
+        if(value == values[i]) {
+            index = i;
+            break;
+        }
+    }
+    return index;
+}

+ 51 - 0
lib/toolbox/value_index.h

@@ -0,0 +1,51 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Get the index of a uint32_t array element which is closest to the given value.
+ *
+ * Returned index corresponds to the first element found.
+ * If no suitable elements were found, the function returns 0.
+ *
+ * @param   value           value to be searched.
+ * @param   values          pointer to the array to perform the search in.
+ * @param   values_count    array size.
+ *
+ * @return value's index.
+ */
+uint8_t value_index_uint32(const uint32_t value, const uint32_t values[], uint8_t values_count);
+
+/** Get the index of a float array element which is closest to the given value.
+ *
+ * Returned index corresponds to the first element found.
+ * If no suitable elements were found, the function returns 0.
+ *
+ * @param   value           value to be searched.
+ * @param   values          pointer to the array to perform the search in.
+ * @param   values_count    array size.
+ *
+ * @return value's index.
+ */
+uint8_t value_index_float(const float value, const float values[], uint8_t values_count);
+
+/** Get the index of a bool array element which is equal to the given value.
+ *
+ * Returned index corresponds to the first element found.
+ * If no suitable elements were found, the function returns 0.
+ *
+ * @param   value           value to be searched.
+ * @param   values          pointer to the array to perform the search in.
+ * @param   values_count    array size.
+ *
+ * @return value's index.
+ */
+uint8_t value_index_bool(const bool value, const bool values[], uint8_t values_count);
+
+#ifdef __cplusplus
+}
+#endif