Просмотр исходного кода

[FL-1968] Pin code locking (#788)

* Gui: code input module
* Gui: fix size to fit frame
* Desktop: PIN config and lock option
* Gui: code input: cleanup, offset input fields if no header present
* Desktop: move code unlock to desktop_locked scene
* Desktop: fix unlock with back key
* Desktop: bump settings version
* Desktop: correct scene usage.

Co-authored-by: あく <alleteam@gmail.com>
its your bedtime 4 лет назад
Родитель
Сommit
fae8d8f23c
23 измененных файлов с 948 добавлено и 43 удалено
  1. 5 1
      applications/desktop/desktop.c
  2. 5 0
      applications/desktop/desktop_i.h
  3. 10 1
      applications/desktop/desktop_settings/desktop_settings.h
  4. 8 4
      applications/desktop/desktop_settings/desktop_settings_app.c
  5. 14 2
      applications/desktop/desktop_settings/desktop_settings_app.h
  6. 2 0
      applications/desktop/desktop_settings/scenes/desktop_settings_scene_config.h
  7. 1 1
      applications/desktop/desktop_settings/scenes/desktop_settings_scene_favorite.c
  8. 62 0
      applications/desktop/desktop_settings/scenes/desktop_settings_scene_pincode_input.c
  9. 78 0
      applications/desktop/desktop_settings/scenes/desktop_settings_scene_pincode_menu.c
  10. 6 2
      applications/desktop/desktop_settings/scenes/desktop_settings_scene_start.c
  11. 1 0
      applications/desktop/scenes/desktop_scene_config.h
  12. 18 0
      applications/desktop/scenes/desktop_scene_lock_menu.c
  13. 37 0
      applications/desktop/scenes/desktop_scene_locked.c
  14. 2 0
      applications/desktop/scenes/desktop_scene_main.c
  15. 50 0
      applications/desktop/scenes/desktop_scene_pinsetup.c
  16. 20 8
      applications/desktop/views/desktop_lock_menu.c
  17. 3 0
      applications/desktop/views/desktop_lock_menu.h
  18. 44 14
      applications/desktop/views/desktop_locked.c
  19. 10 4
      applications/desktop/views/desktop_locked.h
  20. 1 0
      applications/desktop/views/desktop_main.c
  21. 5 6
      applications/desktop/views/desktop_main.h
  22. 475 0
      applications/gui/modules/code_input.c
  23. 91 0
      applications/gui/modules/code_input.h

+ 5 - 1
applications/desktop/desktop.c

@@ -41,6 +41,7 @@ Desktop* desktop_alloc() {
     desktop->debug_view = desktop_debug_alloc();
     desktop->first_start_view = desktop_first_start_alloc();
     desktop->hw_mismatch_popup = popup_alloc();
+    desktop->code_input = code_input_alloc();
 
     view_dispatcher_add_view(
         desktop->view_dispatcher, DesktopViewMain, desktop_main_get_view(desktop->main_view));
@@ -62,7 +63,8 @@ Desktop* desktop_alloc() {
         desktop->view_dispatcher,
         DesktopViewHwMismatch,
         popup_get_view(desktop->hw_mismatch_popup));
-
+    view_dispatcher_add_view(
+        desktop->view_dispatcher, DesktopViewPinSetup, code_input_get_view(desktop->code_input));
     // Lock icon
     desktop->lock_viewport = view_port_alloc();
     view_port_set_width(desktop->lock_viewport, icon_get_width(&I_Lock_8x8));
@@ -82,6 +84,7 @@ void desktop_free(Desktop* desktop) {
     view_dispatcher_remove_view(desktop->view_dispatcher, DesktopViewDebug);
     view_dispatcher_remove_view(desktop->view_dispatcher, DesktopViewFirstStart);
     view_dispatcher_remove_view(desktop->view_dispatcher, DesktopViewHwMismatch);
+    view_dispatcher_remove_view(desktop->view_dispatcher, DesktopViewPinSetup);
 
     view_dispatcher_free(desktop->view_dispatcher);
     scene_manager_free(desktop->scene_manager);
@@ -92,6 +95,7 @@ void desktop_free(Desktop* desktop) {
     desktop_debug_free(desktop->debug_view);
     desktop_first_start_free(desktop->first_start_view);
     popup_free(desktop->hw_mismatch_popup);
+    code_input_free(desktop->code_input);
 
     furi_record_close("gui");
     desktop->gui = NULL;

+ 5 - 0
applications/desktop/desktop_i.h

@@ -8,6 +8,7 @@
 #include <gui/gui.h>
 #include <gui/view_dispatcher.h>
 #include <gui/modules/popup.h>
+#include <gui/modules/code_input.h>
 #include <gui/scene_manager.h>
 #include <assets_icons.h>
 #include <storage/storage.h>
@@ -29,6 +30,7 @@ typedef enum {
     DesktopViewDebug,
     DesktopViewFirstStart,
     DesktopViewHwMismatch,
+    DesktopViewPinSetup,
     DesktopViewTotal,
 } DesktopViewEnum;
 
@@ -46,7 +48,10 @@ struct Desktop {
     DesktopLockMenuView* lock_menu;
     DesktopLockedView* locked_view;
     DesktopDebugView* debug_view;
+    CodeInput* code_input;
+
     DesktopSettings settings;
+    PinCode pincode_buffer;
 
     ViewPort* lock_viewport;
 };

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

@@ -3,11 +3,20 @@
 #include <stdint.h>
 #include <stdbool.h>
 
-#define DESKTOP_SETTINGS_VER (0)
+#define DESKTOP_SETTINGS_VER (1)
+#define PIN_MAX_LENGTH 12
+
+typedef struct {
+    uint8_t length;
+    uint8_t data[PIN_MAX_LENGTH];
+} PinCode;
 
 typedef struct {
     uint8_t version;
     uint16_t favorite;
+
+    PinCode pincode;
+    bool locked;
 } DesktopSettings;
 
 bool desktop_settings_load(DesktopSettings* desktop_settings);

+ 8 - 4
applications/desktop/desktop_settings/desktop_settings_app.c

@@ -33,10 +33,13 @@ DesktopSettingsApp* desktop_settings_app_alloc() {
 
     app->submenu = submenu_alloc();
     view_dispatcher_add_view(
-        app->view_dispatcher, DesktopSettingsAppViewMain, submenu_get_view(app->submenu));
+        app->view_dispatcher, DesktopSettingsAppViewMenu, submenu_get_view(app->submenu));
 
+    app->code_input = code_input_alloc();
     view_dispatcher_add_view(
-        app->view_dispatcher, DesktopSettingsAppViewFavorite, submenu_get_view(app->submenu));
+        app->view_dispatcher,
+        DesktopSettingsAppViewPincodeInput,
+        code_input_get_view(app->code_input));
 
     scene_manager_next_scene(app->scene_manager, DesktopSettingsAppSceneStart);
     return app;
@@ -45,9 +48,10 @@ DesktopSettingsApp* desktop_settings_app_alloc() {
 void desktop_settings_app_free(DesktopSettingsApp* app) {
     furi_assert(app);
     // Variable item list
-    view_dispatcher_remove_view(app->view_dispatcher, DesktopSettingsAppViewMain);
-    view_dispatcher_remove_view(app->view_dispatcher, DesktopSettingsAppViewFavorite);
+    view_dispatcher_remove_view(app->view_dispatcher, DesktopSettingsAppViewMenu);
     submenu_free(app->submenu);
+    view_dispatcher_remove_view(app->view_dispatcher, DesktopSettingsAppViewPincodeInput);
+    code_input_free(app->code_input);
     // View dispatcher
     view_dispatcher_free(app->view_dispatcher);
     scene_manager_free(app->scene_manager);

+ 14 - 2
applications/desktop/desktop_settings/desktop_settings_app.h

@@ -6,20 +6,32 @@
 #include <gui/view_dispatcher.h>
 #include <gui/scene_manager.h>
 #include <gui/modules/submenu.h>
+#include <gui/modules/code_input.h>
 
 #include "desktop_settings.h"
+
 #include "scenes/desktop_settings_scene.h"
 
 typedef enum {
-    DesktopSettingsAppViewMain,
-    DesktopSettingsAppViewFavorite,
+    CodeEventsSetPin,
+    CodeEventsChangePin,
+    CodeEventsDisablePin,
+} CodeEventsEnum;
+
+typedef enum {
+    DesktopSettingsAppViewMenu,
+    DesktopSettingsAppViewPincodeInput,
 } DesktopSettingsAppView;
 
 typedef struct {
     DesktopSettings settings;
+
     Gui* gui;
     SceneManager* scene_manager;
     ViewDispatcher* view_dispatcher;
     Submenu* submenu;
+    CodeInput* code_input;
+
+    uint8_t menu_idx;
 
 } DesktopSettingsApp;

+ 2 - 0
applications/desktop/desktop_settings/scenes/desktop_settings_scene_config.h

@@ -1,2 +1,4 @@
 ADD_SCENE(desktop_settings, start, Start)
 ADD_SCENE(desktop_settings, favorite, Favorite)
+ADD_SCENE(desktop_settings, pincode_menu, PinCodeMenu)
+ADD_SCENE(desktop_settings, pincode_input, PinCodeInput)

+ 1 - 1
applications/desktop/desktop_settings/scenes/desktop_settings_scene_favorite.c

@@ -22,7 +22,7 @@ void desktop_settings_scene_favorite_on_enter(void* context) {
 
     submenu_set_header(app->submenu, "Quick access app:");
     submenu_set_selected_item(app->submenu, app->settings.favorite);
-    view_dispatcher_switch_to_view(app->view_dispatcher, DesktopSettingsAppViewFavorite);
+    view_dispatcher_switch_to_view(app->view_dispatcher, DesktopSettingsAppViewMenu);
 }
 
 bool desktop_settings_scene_favorite_on_event(void* context, SceneManagerEvent event) {

+ 62 - 0
applications/desktop/desktop_settings/scenes/desktop_settings_scene_pincode_input.c

@@ -0,0 +1,62 @@
+#include "../desktop_settings_app.h"
+
+#define SCENE_EXIT_EVENT (0U)
+
+void desktop_settings_scene_ok_callback(void* context) {
+    DesktopSettingsApp* app = context;
+    uint32_t state =
+        scene_manager_get_scene_state(app->scene_manager, DesktopSettingsAppScenePinCodeInput);
+
+    if(state == CodeEventsDisablePin) {
+        memset(app->settings.pincode.data, 0, app->settings.pincode.length * sizeof(uint8_t));
+        app->settings.pincode.length = 0;
+    }
+
+    view_dispatcher_send_custom_event(app->view_dispatcher, SCENE_EXIT_EVENT);
+}
+
+void desktop_settings_scene_pincode_input_on_enter(void* context) {
+    DesktopSettingsApp* app = context;
+    CodeInput* code_input = app->code_input;
+
+    uint32_t state =
+        scene_manager_get_scene_state(app->scene_manager, DesktopSettingsAppScenePinCodeInput);
+    bool update = state != CodeEventsDisablePin;
+
+    code_input_set_header_text(code_input, "PIN Code Setup");
+    code_input_set_result_callback(
+        code_input,
+        desktop_settings_scene_ok_callback,
+        NULL,
+        app,
+        app->settings.pincode.data,
+        &app->settings.pincode.length,
+        update);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, DesktopSettingsAppViewPincodeInput);
+}
+
+bool desktop_settings_scene_pincode_input_on_event(void* context, SceneManagerEvent event) {
+    DesktopSettingsApp* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case SCENE_EXIT_EVENT:
+            scene_manager_previous_scene(app->scene_manager);
+            consumed = true;
+            break;
+
+        default:
+            consumed = true;
+            break;
+        }
+    }
+    return consumed;
+}
+
+void desktop_settings_scene_pincode_input_on_exit(void* context) {
+    DesktopSettingsApp* app = context;
+    code_input_set_result_callback(app->code_input, NULL, NULL, NULL, NULL, NULL, 0);
+    code_input_set_header_text(app->code_input, "");
+}

+ 78 - 0
applications/desktop/desktop_settings/scenes/desktop_settings_scene_pincode_menu.c

@@ -0,0 +1,78 @@
+#include "../desktop_settings_app.h"
+#include "applications.h"
+
+static void desktop_settings_scene_pincode_menu_submenu_callback(void* context, uint32_t index) {
+    DesktopSettingsApp* app = context;
+    view_dispatcher_send_custom_event(app->view_dispatcher, index);
+}
+
+void desktop_settings_scene_pincode_menu_on_enter(void* context) {
+    DesktopSettingsApp* app = context;
+    Submenu* submenu = app->submenu;
+    submenu_clean(submenu);
+
+    if(!app->settings.pincode.length) {
+        submenu_add_item(
+            submenu,
+            "Set Pin",
+            CodeEventsSetPin,
+            desktop_settings_scene_pincode_menu_submenu_callback,
+            app);
+
+    } else {
+        submenu_add_item(
+            submenu,
+            "Change Pin",
+            CodeEventsChangePin,
+            desktop_settings_scene_pincode_menu_submenu_callback,
+            app);
+
+        submenu_add_item(
+            submenu,
+            "Disable",
+            CodeEventsDisablePin,
+            desktop_settings_scene_pincode_menu_submenu_callback,
+            app);
+    }
+
+    submenu_set_header(app->submenu, "Pin code settings:");
+    submenu_set_selected_item(app->submenu, app->menu_idx);
+    view_dispatcher_switch_to_view(app->view_dispatcher, DesktopSettingsAppViewMenu);
+}
+
+bool desktop_settings_scene_pincode_menu_on_event(void* context, SceneManagerEvent event) {
+    DesktopSettingsApp* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case CodeEventsSetPin:
+            scene_manager_set_scene_state(
+                app->scene_manager, DesktopSettingsAppScenePinCodeInput, event.event);
+            scene_manager_next_scene(app->scene_manager, DesktopSettingsAppScenePinCodeInput);
+            consumed = true;
+            break;
+        case CodeEventsChangePin:
+            scene_manager_set_scene_state(
+                app->scene_manager, DesktopSettingsAppScenePinCodeInput, event.event);
+            scene_manager_next_scene(app->scene_manager, DesktopSettingsAppScenePinCodeInput);
+            consumed = true;
+            break;
+        case CodeEventsDisablePin:
+            scene_manager_set_scene_state(
+                app->scene_manager, DesktopSettingsAppScenePinCodeInput, event.event);
+            scene_manager_next_scene(app->scene_manager, DesktopSettingsAppScenePinCodeInput);
+            consumed = true;
+            break;
+        default:
+            consumed = true;
+            break;
+        }
+    }
+    return consumed;
+}
+
+void desktop_settings_scene_pincode_menu_on_exit(void* context) {
+    DesktopSettingsApp* app = context;
+    submenu_clean(app->submenu);
+}

+ 6 - 2
applications/desktop/desktop_settings/scenes/desktop_settings_scene_start.c

@@ -29,7 +29,7 @@ void desktop_settings_scene_start_on_enter(void* context) {
         desktop_settings_scene_start_submenu_callback,
         app);
 
-    view_dispatcher_switch_to_view(app->view_dispatcher, DesktopSettingsAppViewMain);
+    view_dispatcher_switch_to_view(app->view_dispatcher, DesktopSettingsAppViewMenu);
 }
 
 bool desktop_settings_scene_start_on_event(void* context, SceneManagerEvent event) {
@@ -39,7 +39,11 @@ bool desktop_settings_scene_start_on_event(void* context, SceneManagerEvent even
     if(event.type == SceneManagerEventTypeCustom) {
         switch(event.event) {
         case DesktopSettingsStartSubmenuIndexFavorite:
-            scene_manager_next_scene(app->scene_manager, DesktopSettingsAppViewFavorite);
+            scene_manager_next_scene(app->scene_manager, DesktopSettingsAppSceneFavorite);
+            consumed = true;
+            break;
+        case DesktopSettingsStartSubmenuIndexPinSetup:
+            scene_manager_next_scene(app->scene_manager, DesktopSettingsAppScenePinCodeMenu);
             consumed = true;
             break;
         }

+ 1 - 0
applications/desktop/scenes/desktop_scene_config.h

@@ -4,3 +4,4 @@ ADD_SCENE(desktop, locked, Locked)
 ADD_SCENE(desktop, debug, Debug)
 ADD_SCENE(desktop, first_start, FirstStart)
 ADD_SCENE(desktop, hw_mismatch, HwMismatch)
+ADD_SCENE(desktop, pinsetup, PinSetup)

+ 18 - 0
applications/desktop/scenes/desktop_scene_lock_menu.c

@@ -9,7 +9,10 @@ void desktop_scene_lock_menu_callback(DesktopLockMenuEvent event, void* context)
 void desktop_scene_lock_menu_on_enter(void* context) {
     Desktop* desktop = (Desktop*)context;
 
+    desktop_settings_load(&desktop->settings);
+
     desktop_lock_menu_set_callback(desktop->lock_menu, desktop_scene_lock_menu_callback, desktop);
+    desktop_lock_menu_pin_set(desktop->lock_menu, desktop->settings.pincode.length > 0);
     view_dispatcher_switch_to_view(desktop->view_dispatcher, DesktopViewLockMenu);
 }
 
@@ -20,10 +23,25 @@ bool desktop_scene_lock_menu_on_event(void* context, SceneManagerEvent event) {
     if(event.type == SceneManagerEventTypeCustom) {
         switch(event.event) {
         case DesktopLockMenuEventLock:
+            scene_manager_set_scene_state(
+                desktop->scene_manager, DesktopSceneLocked, DesktopLockedNoPin);
             scene_manager_next_scene(desktop->scene_manager, DesktopSceneLocked);
             consumed = true;
             break;
+        case DesktopLockMenuEventPinLock:
+
+            if(desktop->settings.pincode.length > 0) {
+                desktop->settings.locked = true;
+                desktop_settings_save(&desktop->settings);
+                scene_manager_set_scene_state(
+                    desktop->scene_manager, DesktopSceneLocked, DesktopLockedWithPin);
+                scene_manager_next_scene(desktop->scene_manager, DesktopSceneLocked);
+            } else {
+                scene_manager_next_scene(desktop->scene_manager, DesktopScenePinSetup);
+            }
 
+            consumed = true;
+            break;
         case DesktopLockMenuEventExit:
             scene_manager_next_scene(desktop->scene_manager, DesktopSceneMain);
             consumed = true;

+ 37 - 0
applications/desktop/scenes/desktop_scene_locked.c

@@ -15,12 +15,39 @@ void desktop_scene_locked_on_enter(void* context) {
     desktop_locked_update_hint_timeout(locked_view);
     desktop_locked_set_dolphin_animation(locked_view);
 
+    uint32_t state = scene_manager_get_scene_state(desktop->scene_manager, DesktopViewLocked);
+
+    desktop_locked_with_pin(desktop->locked_view, state == DesktopLockedWithPin);
+
     view_port_enabled_set(desktop->lock_viewport, true);
     osTimerStart(locked_view->timer, 63);
 
     view_dispatcher_switch_to_view(desktop->view_dispatcher, DesktopViewLocked);
 }
 
+static bool desktop_scene_locked_check_pin(Desktop* desktop, DesktopMainEvent event) {
+    bool match = false;
+
+    size_t length = desktop->pincode_buffer.length;
+    length = code_input_push(desktop->pincode_buffer.data, length, event);
+    desktop->pincode_buffer.length = length;
+
+    match = code_input_compare(
+        desktop->pincode_buffer.data,
+        length,
+        desktop->settings.pincode.data,
+        desktop->settings.pincode.length);
+
+    if(match) {
+        desktop->pincode_buffer.length = 0;
+        desktop->settings.locked = false;
+        desktop_settings_save(&desktop->settings);
+        desktop_main_unlocked(desktop->main_view);
+    }
+
+    return match;
+}
+
 bool desktop_scene_locked_on_event(void* context, SceneManagerEvent event) {
     Desktop* desktop = (Desktop*)context;
 
@@ -36,7 +63,17 @@ bool desktop_scene_locked_on_event(void* context, SceneManagerEvent event) {
         case DesktopLockedEventUpdate:
             desktop_locked_manage_redraw(desktop->locked_view);
             consumed = true;
+            break;
+        case DesktopLockedEventInputReset:
+            desktop->pincode_buffer.length = 0;
+            break;
         default:
+            if(desktop_scene_locked_check_pin(desktop, event.event)) {
+                scene_manager_set_scene_state(
+                    desktop->scene_manager, DesktopSceneMain, DesktopMainEventUnlocked);
+                scene_manager_next_scene(desktop->scene_manager, DesktopSceneMain);
+                consumed = true;
+            }
             break;
         }
     }

+ 2 - 0
applications/desktop/scenes/desktop_scene_main.c

@@ -34,6 +34,8 @@ void desktop_scene_main_on_enter(void* context) {
     desktop_main_set_callback(main_view, desktop_scene_main_callback, desktop);
     view_port_enabled_set(desktop->lock_viewport, false);
 
+    desktop_settings_load(&desktop->settings);
+
     if(scene_manager_get_scene_state(desktop->scene_manager, DesktopSceneMain) ==
        DesktopMainEventUnlocked) {
         desktop_main_unlocked(desktop->main_view);

+ 50 - 0
applications/desktop/scenes/desktop_scene_pinsetup.c

@@ -0,0 +1,50 @@
+#include "../desktop_i.h"
+
+#define SCENE_EXIT_EVENT (0U)
+
+void desktop_scene_ok_callback(void* context) {
+    Desktop* app = context;
+    desktop_settings_save(&app->settings);
+    view_dispatcher_send_custom_event(app->view_dispatcher, SCENE_EXIT_EVENT);
+}
+
+void desktop_scene_pinsetup_on_enter(void* context) {
+    Desktop* app = context;
+    CodeInput* code_input = app->code_input;
+
+    code_input_set_result_callback(
+        code_input,
+        desktop_scene_ok_callback,
+        NULL,
+        app,
+        app->settings.pincode.data,
+        &app->settings.pincode.length,
+        true);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, DesktopViewPinSetup);
+}
+
+bool desktop_scene_pinsetup_on_event(void* context, SceneManagerEvent event) {
+    Desktop* app = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        switch(event.event) {
+        case SCENE_EXIT_EVENT:
+            scene_manager_previous_scene(app->scene_manager);
+            consumed = true;
+            break;
+
+        default:
+            consumed = true;
+            break;
+        }
+    }
+    return consumed;
+}
+
+void desktop_scene_pinsetup_on_exit(void* context) {
+    Desktop* app = context;
+    code_input_set_result_callback(app->code_input, NULL, NULL, NULL, NULL, NULL, 0);
+    code_input_set_header_text(app->code_input, "");
+}

+ 20 - 8
applications/desktop/views/desktop_lock_menu.c

@@ -12,6 +12,14 @@ void desktop_lock_menu_set_callback(
     lock_menu->context = context;
 }
 
+void desktop_lock_menu_pin_set(DesktopLockMenuView* lock_menu, bool pin_is_set) {
+    with_view_model(
+        lock_menu->view, (DesktopLockMenuViewModel * model) {
+            model->pin_set = pin_is_set;
+            return true;
+        });
+}
+
 void desktop_lock_menu_reset_idx(DesktopLockMenuView* lock_menu) {
     with_view_model(
         lock_menu->view, (DesktopLockMenuViewModel * model) {
@@ -26,6 +34,10 @@ static void lock_menu_callback(void* context, uint8_t index) {
     switch(index) {
     case 0: // lock
         lock_menu->callback(DesktopLockMenuEventLock, lock_menu->context);
+        break;
+    case 1: // lock
+        lock_menu->callback(DesktopLockMenuEventPinLock, lock_menu->context);
+        break;
     default: // wip message
         with_view_model(
             lock_menu->view, (DesktopLockMenuViewModel * model) {
@@ -37,7 +49,7 @@ static void lock_menu_callback(void* context, uint8_t index) {
 }
 
 void desktop_lock_menu_render(Canvas* canvas, void* model) {
-    const char* Lockmenu_Items[3] = {"Lock", "Set PIN", "DUMB mode"};
+    const char* Lockmenu_Items[3] = {"Lock", "Lock with PIN", "DUMB mode"};
 
     DesktopLockMenuViewModel* m = model;
     canvas_clear(canvas);
@@ -47,13 +59,13 @@ void desktop_lock_menu_render(Canvas* canvas, void* model) {
     canvas_set_font(canvas, FontSecondary);
 
     for(uint8_t i = 0; i < 3; ++i) {
-        canvas_draw_str_aligned(
-            canvas,
-            64,
-            13 + (i * 17),
-            AlignCenter,
-            AlignCenter,
-            (m->hint_timeout && m->idx == i && m->idx) ? "Not implemented" : Lockmenu_Items[i]);
+        const char* str = Lockmenu_Items[i];
+
+        if(i == 1 && !m->pin_set) str = "Set PIN";
+        if(m->hint_timeout && m->idx == 2 && m->idx == i) str = "Not implemented";
+
+        canvas_draw_str_aligned(canvas, 64, 13 + (i * 17), AlignCenter, AlignCenter, str);
+
         if(m->idx == i) elements_frame(canvas, 15, 5 + (i * 17), 98, 15);
     }
 }

+ 3 - 0
applications/desktop/views/desktop_lock_menu.h

@@ -11,6 +11,7 @@
 typedef enum {
     DesktopLockMenuEventLock,
     DesktopLockMenuEventUnlock,
+    DesktopLockMenuEventPinLock,
     DesktopLockMenuEventExit,
 } DesktopLockMenuEvent;
 
@@ -27,6 +28,7 @@ struct DesktopLockMenuView {
 typedef struct {
     uint8_t idx;
     uint8_t hint_timeout;
+    bool pin_set;
 } DesktopLockMenuViewModel;
 
 void desktop_lock_menu_set_callback(
@@ -35,6 +37,7 @@ void desktop_lock_menu_set_callback(
     void* context);
 
 View* desktop_lock_menu_get_view(DesktopLockMenuView* lock_menu);
+void desktop_lock_menu_pin_set(DesktopLockMenuView* lock_menu, bool pin_is_set);
 void desktop_lock_menu_reset_idx(DesktopLockMenuView* lock_menu);
 DesktopLockMenuView* desktop_lock_menu_alloc();
 void desktop_lock_menu_free(DesktopLockMenuView* lock_menu);

+ 44 - 14
applications/desktop/views/desktop_locked.c

@@ -80,6 +80,14 @@ void desktop_locked_reset_counter(DesktopLockedView* locked_view) {
         });
 }
 
+void desktop_locked_with_pin(DesktopLockedView* locked_view, bool locked) {
+    with_view_model(
+        locked_view->view, (DesktopLockedViewModel * model) {
+            model->pin_lock = locked;
+            return true;
+        });
+}
+
 void desktop_locked_render(Canvas* canvas, void* model) {
     DesktopLockedViewModel* m = model;
     uint32_t now = osKernelGetTickCount();
@@ -100,7 +108,7 @@ void desktop_locked_render(Canvas* canvas, void* model) {
             canvas_set_font(canvas, FontPrimary);
             elements_multiline_text_framed(canvas, 42, 30, "Locked");
 
-        } else {
+        } else if(!m->pin_lock) {
             canvas_set_font(canvas, FontSecondary);
             canvas_draw_icon(canvas, 13, 5, &I_LockPopup_100x49);
             elements_multiline_text(canvas, 65, 20, "To unlock\npress:");
@@ -116,27 +124,49 @@ View* desktop_locked_get_view(DesktopLockedView* locked_view) {
 bool desktop_locked_input(InputEvent* event, void* context) {
     furi_assert(event);
     furi_assert(context);
-
     DesktopLockedView* locked_view = context;
+
+    uint32_t press_time = 0;
+    bool locked_with_pin = false;
+
+    with_view_model(
+        locked_view->view, (DesktopLockedViewModel * model) {
+            locked_with_pin = model->pin_lock;
+            return false;
+        });
+
     if(event->type == InputTypeShort) {
-        desktop_locked_update_hint_timeout(locked_view);
+        if(locked_with_pin) {
+            press_time = osKernelGetTickCount();
 
-        if(event->key == InputKeyBack) {
-            uint32_t press_time = osKernelGetTickCount();
-            // check if pressed sequentially
-            if(press_time - locked_view->lock_lastpress > UNLOCK_RST_TIMEOUT) {
+            if(press_time - locked_view->lock_lastpress > UNLOCK_RST_TIMEOUT * 3) {
                 locked_view->lock_lastpress = press_time;
-                locked_view->lock_count = 0;
-            } else if(press_time - locked_view->lock_lastpress < UNLOCK_RST_TIMEOUT) {
-                locked_view->lock_lastpress = press_time;
-                locked_view->lock_count++;
+                locked_view->callback(DesktopLockedEventInputReset, locked_view->context);
             }
 
-            if(locked_view->lock_count == UNLOCK_CNT) {
-                locked_view->lock_count = 0;
-                locked_view->callback(DesktopLockedEventUnlock, locked_view->context);
+            locked_view->callback(event->key, locked_view->context);
+        } else {
+            desktop_locked_update_hint_timeout(locked_view);
+
+            if(event->key == InputKeyBack) {
+                press_time = osKernelGetTickCount();
+                // check if pressed sequentially
+                if(press_time - locked_view->lock_lastpress < UNLOCK_RST_TIMEOUT) {
+                    locked_view->lock_lastpress = press_time;
+                    locked_view->lock_count++;
+                }
+
+                if(locked_view->lock_count == UNLOCK_CNT) {
+                    locked_view->lock_count = 0;
+                    locked_view->callback(DesktopLockedEventUnlock, locked_view->context);
+                }
             }
         }
+
+        if(press_time - locked_view->lock_lastpress > UNLOCK_RST_TIMEOUT) {
+            locked_view->lock_lastpress = press_time;
+            locked_view->lock_count = 0;
+        }
     }
     // All events consumed
     return true;

+ 10 - 4
applications/desktop/views/desktop_locked.h

@@ -15,10 +15,16 @@
 #define DOOR_R_POS_MIN 60
 
 typedef enum {
-    DesktopLockedEventUnlock,
-    DesktopLockedEventUpdate,
+    DesktopLockedEventUnlock = 10U,
+    DesktopLockedEventUpdate = 11U,
+    DesktopLockedEventInputReset = 12U,
 } DesktopLockedEvent;
 
+typedef enum {
+    DesktopLockedWithPin,
+    DesktopLockedNoPin,
+} DesktopLockedSceneState;
+
 typedef struct DesktopLockedView DesktopLockedView;
 
 typedef void (*DesktopLockedViewCallback)(DesktopLockedEvent event, void* context);
@@ -42,6 +48,7 @@ typedef struct {
     int8_t door_right_x;
     bool animation_seq_end;
 
+    bool pin_lock;
 } DesktopLockedViewModel;
 
 void desktop_locked_set_callback(
@@ -58,5 +65,4 @@ void desktop_locked_manage_redraw(DesktopLockedView* locked_view);
 View* desktop_locked_get_view(DesktopLockedView* locked_view);
 DesktopLockedView* desktop_locked_alloc();
 void desktop_locked_free(DesktopLockedView* locked_view);
-void desktop_main_unlocked(DesktopMainView* main_view);
-void desktop_main_reset_hint(DesktopMainView* main_view);
+void desktop_locked_with_pin(DesktopLockedView* lock_menu, bool locked);

+ 1 - 0
applications/desktop/views/desktop_main.c

@@ -67,6 +67,7 @@ bool desktop_main_input(InputEvent* event, void* context) {
     } else if(event->key == InputKeyLeft && event->type == InputTypeShort) {
         main_view->callback(DesktopMainEventOpenFavorite, main_view->context);
     }
+
     desktop_main_reset_hint(main_view);
 
     return true;

+ 5 - 6
applications/desktop/views/desktop_main.h

@@ -7,12 +7,12 @@
 #include <furi.h>
 
 typedef enum {
-    DesktopMainEventOpenMenu,
     DesktopMainEventOpenLockMenu,
-    DesktopMainEventOpenDebug,
-    DesktopMainEventUnlocked,
     DesktopMainEventOpenArchive,
     DesktopMainEventOpenFavorite,
+    DesktopMainEventOpenMenu,
+    DesktopMainEventOpenDebug,
+    DesktopMainEventUnlocked,
 } DesktopMainEvent;
 
 typedef struct DesktopMainView DesktopMainView;
@@ -37,9 +37,8 @@ void desktop_main_set_callback(
     void* context);
 
 View* desktop_main_get_view(DesktopMainView* main_view);
-
 DesktopMainView* desktop_main_alloc();
-
 void desktop_main_free(DesktopMainView* main_view);
-
 void desktop_main_switch_dolphin_animation(DesktopMainView* main_view);
+void desktop_main_unlocked(DesktopMainView* main_view);
+void desktop_main_reset_hint(DesktopMainView* main_view);

+ 475 - 0
applications/gui/modules/code_input.c

@@ -0,0 +1,475 @@
+#include "code_input.h"
+#include <gui/elements.h>
+#include <furi.h>
+
+#define MAX_CODE_LEN 10
+
+struct CodeInput {
+    View* view;
+};
+
+typedef enum {
+    CodeInputStateVerify,
+    CodeInputStateUpdate,
+    CodeInputStateTotal,
+} CodeInputStateEnum;
+
+typedef enum {
+    CodeInputFirst,
+    CodeInputSecond,
+    CodeInputTotal,
+} CodeInputsEnum;
+
+typedef struct {
+    uint8_t state;
+    uint8_t current;
+    bool ext_update;
+
+    uint8_t input_length[CodeInputTotal];
+    uint8_t local_buffer[CodeInputTotal][MAX_CODE_LEN];
+
+    CodeInputOkCallback ok_callback;
+    CodeInputFailCallback fail_callback;
+    void* callback_context;
+
+    const char* header;
+
+    uint8_t* ext_buffer;
+    uint8_t* ext_buffer_length;
+} CodeInputModel;
+
+static const Icon* keys_assets[] = {
+    [InputKeyUp] = &I_ButtonUp_7x4,
+    [InputKeyDown] = &I_ButtonDown_7x4,
+    [InputKeyRight] = &I_ButtonRight_4x7,
+    [InputKeyLeft] = &I_ButtonLeft_4x7,
+};
+
+/**
+ * @brief Compare buffers
+ * 
+ * @param in Input buffer pointer
+ * @param len_in Input array length
+ * @param src Source buffer pointer
+ * @param len_src Source array length
+ */
+
+bool code_input_compare(uint8_t* in, size_t len_in, uint8_t* src, size_t len_src) {
+    bool result = false;
+    do {
+        result = (len_in && len_src);
+        if(!result) {
+            break;
+        }
+        result = (len_in == len_src);
+        if(!result) {
+            break;
+        }
+        for(size_t i = 0; i < len_in; i++) {
+            result = (in[i] == src[i]);
+            if(!result) {
+                break;
+            }
+        }
+    } while(false);
+
+    return result;
+}
+
+/**
+ * @brief Compare local buffers
+ * 
+ * @param model 
+ */
+static bool code_input_compare_local(CodeInputModel* model) {
+    uint8_t* source = model->local_buffer[CodeInputFirst];
+    size_t source_length = model->input_length[CodeInputFirst];
+
+    uint8_t* input = model->local_buffer[CodeInputSecond];
+    size_t input_length = model->input_length[CodeInputSecond];
+
+    return code_input_compare(input, input_length, source, source_length);
+}
+
+/**
+ * @brief Compare ext with local
+ * 
+ * @param model 
+ */
+static bool code_input_compare_ext(CodeInputModel* model) {
+    uint8_t* input = model->local_buffer[CodeInputFirst];
+    size_t input_length = model->input_length[CodeInputFirst];
+
+    uint8_t* source = model->ext_buffer;
+    size_t source_length = *model->ext_buffer_length;
+
+    return code_input_compare(input, input_length, source, source_length);
+}
+
+/**
+ * @brief Set ext buffer
+ * 
+ * @param model 
+ */
+static void code_input_set_ext(CodeInputModel* model) {
+    *model->ext_buffer_length = model->input_length[CodeInputFirst];
+    for(size_t i = 0; i <= model->input_length[CodeInputFirst]; i++) {
+        model->ext_buffer[i] = model->local_buffer[CodeInputFirst][i];
+    }
+}
+
+/**
+ * @brief Draw input sequence
+ * 
+ * @param canvas 
+ * @param buffer 
+ * @param length 
+ * @param x 
+ * @param y 
+ * @param active
+ */
+static void code_input_draw_sequence(
+    Canvas* canvas,
+    uint8_t* buffer,
+    uint8_t length,
+    uint8_t x,
+    uint8_t y,
+    bool active) {
+    uint8_t pos_x = x + 6;
+    uint8_t pos_y = y + 3;
+
+    if(active) canvas_draw_icon(canvas, x - 4, y + 5, &I_ButtonRightSmall_3x5);
+
+    elements_slightly_rounded_frame(canvas, x, y, 116, 15);
+
+    for(size_t i = 0; i < length; i++) {
+        // maybe symmetrical assets? :-/
+        uint8_t offset_y = buffer[i] < 2 ? 2 + (buffer[i] * 2) : 1;
+        canvas_draw_icon(canvas, pos_x, pos_y + offset_y, keys_assets[buffer[i]]);
+        pos_x += buffer[i] > 1 ? 9 : 11;
+    }
+}
+
+/**
+ * @brief Reset input count
+ * 
+ * @param model 
+ */
+static void code_input_reset_count(CodeInputModel* model) {
+    model->input_length[model->current] = 0;
+}
+
+/**
+ * @brief Call input callback
+ * 
+ * @param model 
+ */
+static void code_input_call_ok_callback(CodeInputModel* model) {
+    if(model->ok_callback != NULL) {
+        model->ok_callback(model->callback_context);
+    }
+}
+
+/**
+ * @brief Call changed callback
+ * 
+ * @param model 
+ */
+static void code_input_call_fail_callback(CodeInputModel* model) {
+    if(model->fail_callback != NULL) {
+        model->fail_callback(model->callback_context);
+    }
+}
+
+/**
+ * @brief Handle Back button
+ * 
+ * @param model 
+ */
+static bool code_input_handle_back(CodeInputModel* model) {
+    if(model->current && !model->input_length[model->current]) {
+        --model->current;
+        return true;
+    }
+
+    if(model->input_length[model->current]) {
+        code_input_reset_count(model);
+        return true;
+    }
+
+    code_input_call_fail_callback(model);
+    return false;
+}
+
+/**
+ * @brief Handle OK button
+ * 
+ * @param model 
+ */
+static void code_input_handle_ok(CodeInputModel* model) {
+    switch(model->state) {
+    case CodeInputStateVerify:
+
+        if(code_input_compare_ext(model)) {
+            if(model->ext_update) {
+                model->state = CodeInputStateUpdate;
+            } else {
+                code_input_call_ok_callback(model);
+            }
+        }
+        code_input_reset_count(model);
+        break;
+
+    case CodeInputStateUpdate:
+
+        if(!model->current && model->input_length[model->current]) {
+            model->current++;
+        } else {
+            if(code_input_compare_local(model)) {
+                if(model->ext_update) {
+                    code_input_set_ext(model);
+                }
+                code_input_call_ok_callback(model);
+            } else {
+                code_input_reset_count(model);
+            }
+        }
+
+        break;
+    default:
+        break;
+    }
+}
+
+/**
+ * @brief Handle input
+ * 
+ * @param model 
+ * @param key 
+ */
+
+size_t code_input_push(uint8_t* buffer, size_t length, InputKey key) {
+    buffer[length] = key;
+    length = CLAMP(length + 1, MAX_CODE_LEN, 0);
+    return length;
+}
+
+/**
+ * @brief Handle D-pad keys
+ * 
+ * @param model 
+ * @param key 
+ */
+static void code_input_handle_dpad(CodeInputModel* model, InputKey key) {
+    uint8_t at = model->current;
+    size_t new_length = code_input_push(model->local_buffer[at], model->input_length[at], key);
+    model->input_length[at] = new_length;
+}
+
+/**
+ * @brief Draw callback
+ * 
+ * @param canvas 
+ * @param _model 
+ */
+static void code_input_view_draw_callback(Canvas* canvas, void* _model) {
+    CodeInputModel* model = _model;
+    uint8_t y_offset = 0;
+    if(!strlen(model->header)) y_offset = 5;
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+
+    canvas_draw_str(canvas, 2, 9, model->header);
+
+    canvas_set_font(canvas, FontSecondary);
+
+    switch(model->state) {
+    case CodeInputStateVerify:
+        code_input_draw_sequence(
+            canvas,
+            model->local_buffer[CodeInputFirst],
+            model->input_length[CodeInputFirst],
+            6,
+            30 - y_offset,
+            true);
+        break;
+    case CodeInputStateUpdate:
+        code_input_draw_sequence(
+            canvas,
+            model->local_buffer[CodeInputFirst],
+            model->input_length[CodeInputFirst],
+            6,
+            14 - y_offset,
+            !model->current);
+        code_input_draw_sequence(
+            canvas,
+            model->local_buffer[CodeInputSecond],
+            model->input_length[CodeInputSecond],
+            6,
+            44 - y_offset,
+            model->current);
+
+        if(model->current) canvas_draw_str(canvas, 2, 39 - y_offset, "Repeat code");
+
+        break;
+    default:
+        break;
+    }
+}
+
+/**
+ * @brief Input callback
+ * 
+ * @param event 
+ * @param context 
+ * @return true 
+ * @return false 
+ */
+static bool code_input_view_input_callback(InputEvent* event, void* context) {
+    CodeInput* code_input = context;
+    furi_assert(code_input);
+    bool consumed = false;
+
+    if(event->type == InputTypeShort || event->type == InputTypeRepeat) {
+        switch(event->key) {
+        case InputKeyBack:
+            with_view_model(
+                code_input->view, (CodeInputModel * model) {
+                    consumed = code_input_handle_back(model);
+                    return true;
+                });
+            break;
+
+        case InputKeyOk:
+            with_view_model(
+                code_input->view, (CodeInputModel * model) {
+                    code_input_handle_ok(model);
+                    return true;
+                });
+            consumed = true;
+            break;
+        default:
+
+            with_view_model(
+                code_input->view, (CodeInputModel * model) {
+                    code_input_handle_dpad(model, event->key);
+                    return true;
+                });
+            consumed = true;
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+/**
+ * @brief Reset all input-related data in model
+ * 
+ * @param model CodeInputModel
+ */
+static void code_input_reset_model_input_data(CodeInputModel* model) {
+    model->current = 0;
+    model->input_length[CodeInputFirst] = 0;
+    model->input_length[CodeInputSecond] = 0;
+    model->ext_buffer = NULL;
+    model->ext_update = false;
+    model->state = 0;
+}
+
+/** 
+ * @brief Allocate and initialize code input. This code input is used to enter codes.
+ * 
+ * @return CodeInput instance pointer
+ */
+CodeInput* code_input_alloc() {
+    CodeInput* code_input = furi_alloc(sizeof(CodeInput));
+    code_input->view = view_alloc();
+    view_set_context(code_input->view, code_input);
+    view_allocate_model(code_input->view, ViewModelTypeLocking, sizeof(CodeInputModel));
+    view_set_draw_callback(code_input->view, code_input_view_draw_callback);
+    view_set_input_callback(code_input->view, code_input_view_input_callback);
+
+    with_view_model(
+        code_input->view, (CodeInputModel * model) {
+            model->header = "";
+            model->ok_callback = NULL;
+            model->fail_callback = NULL;
+            model->callback_context = NULL;
+            code_input_reset_model_input_data(model);
+            return true;
+        });
+
+    return code_input;
+}
+
+/** 
+ * @brief Deinitialize and free code input
+ * 
+ * @param code_input Code input instance
+ */
+void code_input_free(CodeInput* code_input) {
+    furi_assert(code_input);
+    view_free(code_input->view);
+    free(code_input);
+}
+
+/** 
+ * @brief Get code input view
+ * 
+ * @param code_input code input instance
+ * @return View instance that can be used for embedding
+ */
+View* code_input_get_view(CodeInput* code_input) {
+    furi_assert(code_input);
+    return code_input->view;
+}
+
+/** 
+ * @brief Set code input callbacks
+ * 
+ * @param code_input code input instance
+ * @param ok_callback input callback fn
+ * @param fail_callback code match callback fn
+ * @param callback_context callback context
+ * @param buffer buffer 
+ * @param buffer_length ptr to buffer length uint
+ * @param ext_update  true to update buffer 
+ */
+void code_input_set_result_callback(
+    CodeInput* code_input,
+    CodeInputOkCallback ok_callback,
+    CodeInputFailCallback fail_callback,
+    void* callback_context,
+    uint8_t* buffer,
+    uint8_t* buffer_length,
+    bool ext_update) {
+    with_view_model(
+        code_input->view, (CodeInputModel * model) {
+            code_input_reset_model_input_data(model);
+            model->ok_callback = ok_callback;
+            model->fail_callback = fail_callback;
+            model->callback_context = callback_context;
+
+            model->ext_buffer = buffer;
+            model->ext_buffer_length = buffer_length;
+            model->state = (*buffer_length == 0) ? 1 : 0;
+            model->ext_update = ext_update;
+
+            return true;
+        });
+}
+
+/**
+ * @brief Set code input header text
+ * 
+ * @param code_input code input instance
+ * @param text text to be shown
+ */
+void code_input_set_header_text(CodeInput* code_input, const char* text) {
+    with_view_model(
+        code_input->view, (CodeInputModel * model) {
+            model->header = text;
+            return true;
+        });
+}

+ 91 - 0
applications/gui/modules/code_input.h

@@ -0,0 +1,91 @@
+/**
+ * @file code_input.h
+ * GUI: CodeInput keyboard view module API
+ */
+
+#pragma once
+
+#include <gui/view.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Code input anonymous structure  */
+typedef struct CodeInput CodeInput;
+
+/** callback that is executed when entered code matches ext buffer */
+typedef void (*CodeInputOkCallback)(void* context);
+
+/** callback that is executed when entered code does not matches ext buffer */
+typedef void (*CodeInputFailCallback)(void* context);
+
+/** Allocate and initialize code input. This code input is used to enter codes.
+ *
+ * @return     CodeInput instance pointer
+ */
+CodeInput* code_input_alloc();
+
+/** Deinitialize and free code input
+ *
+ * @param      code_input  Code input instance
+ */
+void code_input_free(CodeInput* code_input);
+
+/** Get code input view
+ *
+ * @param      code_input  code input instance
+ *
+ * @return     View instance that can be used for embedding
+ */
+View* code_input_get_view(CodeInput* code_input);
+
+/** Set code input result callback
+ *
+ * @param      code_input        code input instance
+ * @param      ok_callback    ok callback fn
+ * @param      fail_callback  fail callback fn
+ * @param      callback_context  callback context
+ * @param      buffer       buffer to use
+ * @param      buffer_length       buffer length
+ * @param      update  set true to update buffer 
+ */
+void code_input_set_result_callback(
+    CodeInput* code_input,
+    CodeInputOkCallback ok_callback,
+    CodeInputFailCallback fail_callback,
+    void* callback_context,
+    uint8_t* buffer,
+    uint8_t* buffer_length,
+    bool update);
+
+/** Set code input header text
+ *
+ * @param      code_input  code input instance
+ * @param      text        text to be shown
+ */
+void code_input_set_header_text(CodeInput* code_input, const char* text);
+
+/** Compare two buffers
+ *
+ * @param      in       buffer to compare to source
+ * @param      len_in   length of input buffer
+ * @param      src      source buffer
+ * @param      len_src  length of insourceput buffer
+ * @return     true if buffers match
+ */
+
+bool code_input_compare(uint8_t* in, size_t len_in, uint8_t* src, size_t len_src);
+
+/** Push input into the end of array
+ *
+ * @param      buffer   buffer
+ * @param      length   length of buffer
+ * @param      key      input key
+ * @return     new length of input buffer
+ */
+size_t code_input_push(uint8_t* buffer, size_t length, InputKey key);
+
+#ifdef __cplusplus
+}
+#endif