Explorar o código

[FL-1329] Settings (#563)

* Menu: secondary menu rendering
* Manu: reset window position on enter to new menu
* App-loader: settings menu
* Applications: add settings app list
* App backlight-control: all work related to turning off the display is now in the notification app
* App notification: settings save and load
* Gui: variable item list module
* App: new notification settings app
* Display: backlight is now fully serviced in the notification app
* Gui: update variable item list module documentation
SG %!s(int64=4) %!d(string=hai) anos
pai
achega
29da0e360c

+ 21 - 0
applications/app-loader/app-loader.c

@@ -221,6 +221,27 @@ int32_t app_loader(void* p) {
             menu_item_add(menu, menu_debug);
         });
 
+    // Settings
+    FURI_LOG_I(APP_LOADER_TAG, "Building settings menu");
+    with_value_mutex(
+        menu_mutex, (Menu * menu) {
+            MenuItem* menu_debug =
+                menu_item_alloc_menu("Settings", assets_icons_get(A_Settings_14));
+
+            for(size_t i = 0; i < FLIPPER_SETTINGS_APPS_COUNT; i++) {
+                // Add menu item
+                menu_item_subitem_add(
+                    menu_debug,
+                    menu_item_alloc_function(
+                        FLIPPER_SETTINGS_APPS[i].name,
+                        assets_icons_get(FLIPPER_SETTINGS_APPS[i].icon),
+                        app_loader_menu_callback,
+                        (void*)&FLIPPER_SETTINGS_APPS[i]));
+            }
+
+            menu_item_add(menu, menu_debug);
+        });
+
     // Call on start hooks
     for(size_t i = 0; i < FLIPPER_ON_SYSTEM_START_COUNT; i++) {
         (*FLIPPER_ON_SYSTEM_START[i])();

+ 16 - 5
applications/applications.c

@@ -13,7 +13,6 @@ int32_t menu_task(void* p);
 int32_t coreglitch_demo_0(void* p);
 int32_t u8g2_qrcode(void* p);
 int32_t gui_task(void* p);
-int32_t backlight_control(void* p);
 int32_t irda(void* p);
 int32_t app_loader(void* p);
 int32_t nfc_task(void* p);
@@ -49,6 +48,9 @@ void bt_cli_init();
 void lfrfid_cli_init();
 void ibutton_cli_init();
 
+// Settings
+int32_t notification_app_settings(void* p);
+
 const FlipperApplication FLIPPER_SERVICES[] = {
 #ifdef SRV_CLI
     {.app = cli_task, .name = "cli_task", .stack_size = 4096, .icon = A_Plugins_14},
@@ -67,10 +69,6 @@ const FlipperApplication FLIPPER_SERVICES[] = {
 #endif
 
 #ifdef SRV_GUI
-    {.app = backlight_control,
-     .name = "backlight_control",
-     .stack_size = 1024,
-     .icon = A_Plugins_14},
     // TODO: fix stack size when sd api will be in separate thread
     {.app = gui_task, .name = "gui_task", .stack_size = 8192, .icon = A_Plugins_14},
 #endif
@@ -322,3 +320,16 @@ const FlipperApplication FLIPPER_SCENE_APPS[] = {
 const size_t FLIPPER_SCENE_APPS_COUNT = sizeof(FLIPPER_SCENE_APPS) / sizeof(FlipperApplication);
 
 #endif
+
+// Settings menu
+const FlipperApplication FLIPPER_SETTINGS_APPS[] = {
+#ifdef SRV_NOTIFICATION
+    {.app = notification_app_settings,
+     .name = "Notification",
+     .stack_size = 1024,
+     .icon = A_Plugins_14},
+#endif
+};
+
+const size_t FLIPPER_SETTINGS_APPS_COUNT =
+    sizeof(FLIPPER_SETTINGS_APPS) / sizeof(FlipperApplication);

+ 7 - 1
applications/applications.h

@@ -49,4 +49,10 @@ extern const FlipperApplication FLIPPER_SCENE;
 extern const FlipperApplication FLIPPER_SCENE_APPS[];
 extern const size_t FLIPPER_SCENE_APPS_COUNT;
 
-extern const FlipperApplication FLIPPER_ARCHIVE;
+extern const FlipperApplication FLIPPER_ARCHIVE;
+
+/* Settings list
+ * Spawned by app-loader
+ */
+extern const FlipperApplication FLIPPER_SETTINGS_APPS[];
+extern const size_t FLIPPER_SETTINGS_APPS_COUNT;

+ 0 - 31
applications/backlight-control/backlight-control.c

@@ -1,31 +0,0 @@
-#include <furi.h>
-#include <api-hal.h>
-#include <notification/notification-messages.h>
-
-#define BACKLIGHT_TIME 30000
-#define BACKLIGHT_FLAG_ACTIVITY 0x00000001U
-
-static void event_cb(const void* value, void* ctx) {
-    osThreadFlagsSet((osThreadId_t)ctx, BACKLIGHT_FLAG_ACTIVITY);
-}
-
-int32_t backlight_control(void* p) {
-    // open record
-    NotificationApp* notifications = furi_record_open("notification");
-    PubSub* event_record = furi_record_open("input_events");
-    subscribe_pubsub(event_record, event_cb, (void*)osThreadGetId());
-
-    notification_internal_message(notifications, &sequence_display_on);
-
-    while(1) {
-        // wait for event
-        if(osThreadFlagsWait(BACKLIGHT_FLAG_ACTIVITY, osFlagsWaitAny, BACKLIGHT_TIME) ==
-           BACKLIGHT_FLAG_ACTIVITY) {
-            notification_internal_message(notifications, &sequence_display_on);
-        } else {
-            notification_internal_message(notifications, &sequence_display_off);
-        }
-    }
-
-    return 0;
-}

+ 282 - 0
applications/gui/modules/variable-item-list.c

@@ -0,0 +1,282 @@
+#include "variable-item-list.h"
+#include "gui/canvas.h"
+#include <m-array.h>
+#include <furi.h>
+#include <gui/elements.h>
+#include <stdint.h>
+
+struct VariableItem {
+    const char* label;
+    uint8_t current_value_index;
+    string_t current_value_text;
+    uint8_t values_count;
+    VariableItemChangeCallback change_callback;
+    void* context;
+};
+
+ARRAY_DEF(VariableItemArray, VariableItem, M_POD_OPLIST);
+
+struct VariableItemList {
+    View* view;
+};
+
+typedef struct {
+    VariableItemArray_t items;
+    uint8_t position;
+    uint8_t window_position;
+} VariableItemListModel;
+
+static void variable_item_list_process_up(VariableItemList* variable_item_list);
+static void variable_item_list_process_down(VariableItemList* variable_item_list);
+static void variable_item_list_process_left(VariableItemList* variable_item_list);
+static void variable_item_list_process_right(VariableItemList* variable_item_list);
+
+static void variable_item_list_draw_callback(Canvas* canvas, void* _model) {
+    VariableItemListModel* model = _model;
+
+    const uint8_t item_height = 16;
+    const uint8_t item_width = 123;
+
+    canvas_clear(canvas);
+
+    uint8_t position = 0;
+    VariableItemArray_it_t it;
+
+    canvas_set_font(canvas, FontSecondary);
+    for(VariableItemArray_it(it, model->items); !VariableItemArray_end_p(it);
+        VariableItemArray_next(it)) {
+        uint8_t item_position = position - model->window_position;
+        uint8_t items_on_screen = 4;
+        uint8_t y_offset = 0;
+
+        if(item_position < items_on_screen) {
+            const VariableItem* item = VariableItemArray_cref(it);
+            uint8_t item_y = y_offset + (item_position * item_height);
+            uint8_t item_text_y = item_y + item_height - 4;
+
+            if(position == model->position) {
+                canvas_set_color(canvas, ColorBlack);
+                elements_slightly_rounded_box(canvas, 0, item_y + 1, item_width, item_height - 2);
+                canvas_set_color(canvas, ColorWhite);
+            } else {
+                canvas_set_color(canvas, ColorBlack);
+            }
+
+            canvas_draw_str(canvas, 6, item_text_y, item->label);
+
+            if(item->current_value_index > 0) {
+                canvas_draw_str(canvas, 73, item_text_y, "<");
+            }
+
+            canvas_draw_str(canvas, 84, item_text_y, string_get_cstr(item->current_value_text));
+
+            if(item->current_value_index < (item->values_count - 1)) {
+                canvas_draw_str(canvas, 113, item_text_y, ">");
+            }
+        }
+
+        position++;
+    }
+
+    elements_scrollbar(canvas, model->position, VariableItemArray_size(model->items));
+}
+
+static bool variable_item_list_input_callback(InputEvent* event, void* context) {
+    VariableItemList* variable_item_list = context;
+    furi_assert(variable_item_list);
+    bool consumed = false;
+
+    if(event->type == InputTypeShort) {
+        switch(event->key) {
+        case InputKeyUp:
+            consumed = true;
+            variable_item_list_process_up(variable_item_list);
+            break;
+        case InputKeyDown:
+            consumed = true;
+            variable_item_list_process_down(variable_item_list);
+            break;
+        case InputKeyLeft:
+            consumed = true;
+            variable_item_list_process_left(variable_item_list);
+            break;
+        case InputKeyRight:
+            consumed = true;
+            variable_item_list_process_right(variable_item_list);
+            break;
+        default:
+            break;
+        }
+    }
+
+    return consumed;
+}
+
+void variable_item_list_process_up(VariableItemList* variable_item_list) {
+    with_view_model(
+        variable_item_list->view, (VariableItemListModel * model) {
+            uint8_t items_on_screen = 4;
+            if(model->position > 0) {
+                model->position--;
+                if(((model->position - model->window_position) < 1) &&
+                   model->window_position > 0) {
+                    model->window_position--;
+                }
+            } else {
+                model->position = VariableItemArray_size(model->items) - 1;
+                if(model->position > (items_on_screen - 1)) {
+                    model->window_position = model->position - (items_on_screen - 1);
+                }
+            }
+            return true;
+        });
+}
+
+void variable_item_list_process_down(VariableItemList* variable_item_list) {
+    with_view_model(
+        variable_item_list->view, (VariableItemListModel * model) {
+            uint8_t items_on_screen = 4;
+            if(model->position < (VariableItemArray_size(model->items) - 1)) {
+                model->position++;
+                if((model->position - model->window_position) > (items_on_screen - 2) &&
+                   model->window_position <
+                       (VariableItemArray_size(model->items) - items_on_screen)) {
+                    model->window_position++;
+                }
+            } else {
+                model->position = 0;
+                model->window_position = 0;
+            }
+            return true;
+        });
+}
+
+VariableItem* variable_item_list_get_selected_item(VariableItemListModel* model) {
+    VariableItem* item = NULL;
+
+    VariableItemArray_it_t it;
+    uint8_t position = 0;
+    for(VariableItemArray_it(it, model->items); !VariableItemArray_end_p(it);
+        VariableItemArray_next(it)) {
+        if(position == model->position) {
+            break;
+        }
+        position++;
+    }
+
+    item = VariableItemArray_ref(it);
+
+    furi_assert(item);
+    return item;
+}
+
+void variable_item_list_process_left(VariableItemList* variable_item_list) {
+    with_view_model(
+        variable_item_list->view, (VariableItemListModel * model) {
+            VariableItem* item = variable_item_list_get_selected_item(model);
+            if(item->current_value_index > 0) {
+                item->current_value_index--;
+                if(item->change_callback) {
+                    item->change_callback(item);
+                }
+            }
+            return true;
+        });
+}
+
+void variable_item_list_process_right(VariableItemList* variable_item_list) {
+    with_view_model(
+        variable_item_list->view, (VariableItemListModel * model) {
+            VariableItem* item = variable_item_list_get_selected_item(model);
+            if(item->current_value_index < (item->values_count - 1)) {
+                item->current_value_index++;
+                if(item->change_callback) {
+                    item->change_callback(item);
+                }
+            }
+            return true;
+        });
+}
+
+VariableItemList* variable_item_list_alloc() {
+    VariableItemList* variable_item_list = furi_alloc(sizeof(VariableItemList));
+    variable_item_list->view = view_alloc();
+    view_set_context(variable_item_list->view, variable_item_list);
+    view_allocate_model(
+        variable_item_list->view, ViewModelTypeLocking, sizeof(VariableItemListModel));
+    view_set_draw_callback(variable_item_list->view, variable_item_list_draw_callback);
+    view_set_input_callback(variable_item_list->view, variable_item_list_input_callback);
+
+    with_view_model(
+        variable_item_list->view, (VariableItemListModel * model) {
+            VariableItemArray_init(model->items);
+            model->position = 0;
+            model->window_position = 0;
+            return true;
+        });
+
+    return variable_item_list;
+}
+
+void variable_item_list_free(VariableItemList* variable_item_list) {
+    furi_assert(variable_item_list);
+
+    with_view_model(
+        variable_item_list->view, (VariableItemListModel * model) {
+            VariableItemArray_it_t it;
+            for(VariableItemArray_it(it, model->items); !VariableItemArray_end_p(it);
+                VariableItemArray_next(it)) {
+                string_clear(VariableItemArray_ref(it)->current_value_text);
+            }
+            VariableItemArray_clear(model->items);
+            return false;
+        });
+    view_free(variable_item_list->view);
+    free(variable_item_list);
+}
+
+View* variable_item_list_get_view(VariableItemList* variable_item_list) {
+    furi_assert(variable_item_list);
+    return variable_item_list->view;
+}
+
+VariableItem* variable_item_list_add(
+    VariableItemList* variable_item_list,
+    const char* label,
+    uint8_t values_count,
+    VariableItemChangeCallback change_callback,
+    void* context) {
+    VariableItem* item = NULL;
+    furi_assert(label);
+    furi_assert(variable_item_list);
+
+    with_view_model(
+        variable_item_list->view, (VariableItemListModel * model) {
+            item = VariableItemArray_push_new(model->items);
+            item->label = label;
+            item->values_count = values_count;
+            item->change_callback = change_callback;
+            item->context = context;
+            item->current_value_index = 0;
+            string_init(item->current_value_text);
+            return true;
+        });
+
+    return item;
+}
+
+void variable_item_set_current_value_index(VariableItem* item, uint8_t current_value_index) {
+    item->current_value_index = current_value_index;
+}
+
+void variable_item_set_current_value_text(VariableItem* item, const char* current_value_text) {
+    string_set_str(item->current_value_text, current_value_text);
+}
+
+uint8_t variable_item_get_current_value_index(VariableItem* item) {
+    return item->current_value_index;
+}
+
+void* variable_item_get_context(VariableItem* item) {
+    return item->context;
+}

+ 64 - 0
applications/gui/modules/variable-item-list.h

@@ -0,0 +1,64 @@
+#pragma once
+#include <gui/view.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct VariableItemList VariableItemList;
+typedef struct VariableItem VariableItem;
+typedef void (*VariableItemChangeCallback)(VariableItem* item);
+
+/** Allocate and initialize VariableItemList
+ * @return VariableItemList* 
+ */
+VariableItemList* variable_item_list_alloc();
+
+/** Deinitialize and free VariableItemList
+ * @param variable_item_list VariableItemList instance
+ */
+void variable_item_list_free(VariableItemList* variable_item_list);
+View* variable_item_list_get_view(VariableItemList* variable_item_list);
+
+/** Add item to VariableItemList
+ * @param variable_item_list VariableItemList instance
+ * @param label item name
+ * @param values_count item values count
+ * @param change_callback called on value change in gui
+ * @param context item context
+ * @return VariableItem* item instance
+ */
+VariableItem* variable_item_list_add(
+    VariableItemList* variable_item_list,
+    const char* label,
+    uint8_t values_count,
+    VariableItemChangeCallback change_callback,
+    void* context);
+
+/** Set item current selected index
+ * @param item VariableItem* instance
+ * @param current_value_index 
+ */
+void variable_item_set_current_value_index(VariableItem* item, uint8_t current_value_index);
+
+/** Set item current selected text
+ * @param item VariableItem* instance
+ * @param current_value_text 
+ */
+void variable_item_set_current_value_text(VariableItem* item, const char* current_value_text);
+
+/** Get item current selected index
+ * @param item VariableItem* instance
+ * @return uint8_t current selected index
+ */
+uint8_t variable_item_get_current_value_index(VariableItem* item);
+
+/** Get item context
+ * @param item VariableItem* instance
+ * @return void* item context
+ */
+void* variable_item_get_context(VariableItem* item);
+
+#ifdef __cplusplus
+}
+#endif

+ 91 - 22
applications/menu/menu.c

@@ -69,23 +69,6 @@ void menu_settings_item_add(Menu* menu, MenuItem* item) {
 }
 
 void menu_draw_primary(Menu* menu, Canvas* canvas) {
-}
-
-void menu_draw_secondary(Menu* menu, Canvas* canvas) {
-}
-
-void menu_view_port_callback(Canvas* canvas, void* context) {
-    furi_assert(canvas);
-    furi_assert(context);
-
-    Menu* menu = acquire_mutex((ValueMutex*)context, 100); // wait 10 ms to get mutex
-    if(menu == NULL) return; // redraw fail
-
-    furi_assert(menu->current);
-
-    canvas_clear(canvas);
-    canvas_set_color(canvas, ColorBlack);
-
     size_t position = menu_item_get_position(menu->current);
     MenuItemArray_t* items = menu_item_get_subitems(menu->current);
     size_t items_count = MenuItemArray_size(*items);
@@ -119,6 +102,63 @@ void menu_view_port_callback(Canvas* canvas, void* context) {
         canvas_draw_str(canvas, 2, 32, "Empty");
         elements_scrollbar(canvas, 0, 0);
     }
+}
+
+void menu_draw_secondary(Menu* menu, Canvas* canvas) {
+    size_t position = 0;
+    size_t selected_position = menu_item_get_position(menu->current);
+    size_t window_position = menu_item_get_window_position(menu->current);
+    MenuItemArray_t* items = menu_item_get_subitems(menu->current);
+    const uint8_t items_on_screen = 4;
+    const uint8_t item_height = 16;
+    const uint8_t item_width = 123;
+    size_t items_count = MenuItemArray_size(*items);
+    MenuItemArray_it_t it;
+
+    canvas_set_font(canvas, FontSecondary);
+    for(MenuItemArray_it(it, *items); !MenuItemArray_end_p(it); MenuItemArray_next(it)) {
+        size_t item_position = position - window_position;
+
+        if(item_position < items_on_screen) {
+            if(position == selected_position) {
+                canvas_set_color(canvas, ColorBlack);
+                elements_slightly_rounded_box(
+                    canvas, 0, (item_position * item_height) + 1, item_width, item_height - 2);
+                canvas_set_color(canvas, ColorWhite);
+            } else {
+                canvas_set_color(canvas, ColorBlack);
+            }
+            canvas_draw_str(
+                canvas,
+                6,
+                (item_position * item_height) + item_height - 4,
+                menu_item_get_label(*MenuItemArray_ref(it)));
+        }
+
+        position++;
+    }
+
+    elements_scrollbar(canvas, selected_position, items_count);
+}
+
+void menu_view_port_callback(Canvas* canvas, void* context) {
+    furi_assert(canvas);
+    furi_assert(context);
+
+    Menu* menu = acquire_mutex((ValueMutex*)context, 100); // wait 10 ms to get mutex
+    if(menu == NULL) return; // redraw fail
+
+    furi_assert(menu->current);
+
+    canvas_clear(canvas);
+    canvas_set_color(canvas, ColorBlack);
+
+    // if top level
+    if(menu_item_get_parent(menu->current) == NULL) {
+        menu_draw_primary(menu, canvas);
+    } else {
+        menu_draw_secondary(menu, canvas);
+    }
 
     release_mutex((ValueMutex*)context, menu);
 }
@@ -156,22 +196,49 @@ void menu_update(Menu* menu) {
 
 void menu_up(Menu* menu) {
     furi_assert(menu);
-
     size_t position = menu_item_get_position(menu->current);
+    size_t window_position = menu_item_get_window_position(menu->current);
     MenuItemArray_t* items = menu_item_get_subitems(menu->current);
-    if(position == 0) position = MenuItemArray_size(*items);
-    position--;
+
+    const uint8_t items_on_screen = 4;
+
+    if(position > 0) {
+        position--;
+        if(((position - window_position) < 1) && window_position > 0) {
+            window_position--;
+        }
+    } else {
+        position = MenuItemArray_size(*items) - 1;
+        if(position > (items_on_screen - 1)) {
+            window_position = position - (items_on_screen - 1);
+        }
+    }
+
     menu_item_set_position(menu->current, position);
+    menu_item_set_window_position(menu->current, window_position);
     menu_update(menu);
 }
 
 void menu_down(Menu* menu) {
     furi_assert(menu);
     size_t position = menu_item_get_position(menu->current);
+    size_t window_position = menu_item_get_window_position(menu->current);
     MenuItemArray_t* items = menu_item_get_subitems(menu->current);
-    position++;
-    position = position % MenuItemArray_size(*items);
+
+    const uint8_t items_on_screen = 4;
+    if(position < (MenuItemArray_size(*items) - 1)) {
+        position++;
+        if((position - window_position) > (items_on_screen - 2) &&
+           window_position < (MenuItemArray_size(*items) - items_on_screen)) {
+            window_position++;
+        }
+    } else {
+        position = 0;
+        window_position = 0;
+    }
+
     menu_item_set_position(menu->current, position);
+    menu_item_set_window_position(menu->current, window_position);
     menu_update(menu);
 }
 
@@ -182,6 +249,7 @@ void menu_ok(Menu* menu) {
         view_port_enabled_set(menu->view_port, true);
         menu->current = menu->root;
         menu_item_set_position(menu->current, 0);
+        menu_item_set_window_position(menu->current, 0);
         menu_update(menu);
         return;
     }
@@ -198,6 +266,7 @@ void menu_ok(Menu* menu) {
     if(type == MenuItemTypeMenu) {
         menu->current = item;
         menu_item_set_position(menu->current, 0);
+        menu_item_set_window_position(menu->current, 0);
         menu_update(menu);
     } else if(type == MenuItemTypeFunction) {
         menu_item_function_call(item);

+ 12 - 0
applications/menu/menu_item.c

@@ -10,6 +10,7 @@ struct MenuItem {
     Icon* icon;
 
     size_t position;
+    size_t window_position;
     MenuItem* parent;
     void* data;
 
@@ -49,6 +50,7 @@ MenuItem* menu_item_alloc_function(
     menu_item->icon = icon;
     menu_item->callback = callback;
     menu_item->callback_context = context;
+    menu_item->parent = NULL;
 
     return menu_item;
 }
@@ -90,6 +92,16 @@ size_t menu_item_get_position(MenuItem* menu_item) {
     return menu_item->position;
 }
 
+void menu_item_set_window_position(MenuItem* menu_item, size_t window_position) {
+    furi_assert(menu_item);
+    menu_item->window_position = window_position;
+}
+
+size_t menu_item_get_window_position(MenuItem* menu_item) {
+    furi_assert(menu_item);
+    return menu_item->window_position;
+}
+
 void menu_item_set_label(MenuItem* menu_item, const char* label) {
     furi_assert(menu_item);
     menu_item->label = label;

+ 3 - 0
applications/menu/menu_item.h

@@ -33,6 +33,9 @@ MenuItemType menu_item_get_type(MenuItem* menu_item);
 void menu_item_set_position(MenuItem* menu_item, size_t position);
 size_t menu_item_get_position(MenuItem* menu_item);
 
+void menu_item_set_window_position(MenuItem* menu_item, size_t window_position);
+size_t menu_item_get_window_position(MenuItem* menu_item);
+
 void menu_item_set_label(MenuItem* menu_item, const char* label);
 const char* menu_item_get_label(MenuItem* menu_item);
 

+ 231 - 0
applications/notification/notification-app-settings.c

@@ -0,0 +1,231 @@
+#include <furi.h>
+#include "notification-app.h"
+#include <gui/modules/variable-item-list.h>
+#include <gui/view_dispatcher.h>
+
+#define MAX_NOTIFICATION_SETTINGS 4
+
+typedef struct {
+    NotificationApp* notification;
+    Gui* gui;
+    ViewDispatcher* view_dispatcher;
+    VariableItemList* variable_item_list;
+} NotificationAppSettings;
+
+static const NotificationSequence sequence_note_c = {
+    &message_note_c5,
+    &message_delay_100,
+    &message_sound_off,
+    NULL,
+};
+
+static const NotificationSequence sequence_vibro = {
+    &message_vibro_on,
+    &message_delay_100,
+    &message_vibro_off,
+    NULL,
+};
+
+#define BACKLIGHT_COUNT 5
+const char* const backlight_text[BACKLIGHT_COUNT] = {
+    "0%",
+    "25%",
+    "50%",
+    "75%",
+    "100%",
+};
+const float backlight_value[BACKLIGHT_COUNT] = {
+    0.0f,
+    0.25f,
+    0.5f,
+    0.75f,
+    1.0f,
+};
+
+#define VOLUME_COUNT 5
+const char* const volume_text[VOLUME_COUNT] = {
+    "0%",
+    "25%",
+    "50%",
+    "75%",
+    "100%",
+};
+const float volume_value[VOLUME_COUNT] = {0.0f, 0.04f, 0.1f, 0.2f, 1.0f};
+
+#define DELAY_COUNT 6
+const char* const delay_text[DELAY_COUNT] = {
+    "1s",
+    "5s",
+    "15s",
+    "30s",
+    "60s",
+    "120s",
+};
+const uint32_t delay_value[DELAY_COUNT] = {1000, 5000, 15000, 30000, 60000, 120000};
+
+#define VIBRO_COUNT 2
+const char* const vibro_text[VIBRO_COUNT] = {
+    "OFF",
+    "ON",
+};
+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 = 1; 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) {
+    float last_value = values[0];
+    uint8_t index = 0;
+    for(uint8_t i = 1; 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);
+
+    variable_item_set_current_value_text(item, backlight_text[index]);
+    app->notification->settings.display_brightness = backlight_value[index];
+    notification_message(app->notification, &sequence_display_on);
+}
+
+static void screen_changed(VariableItem* item) {
+    NotificationAppSettings* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    variable_item_set_current_value_text(item, delay_text[index]);
+    app->notification->settings.display_off_delay_ms = delay_value[index];
+    notification_message(app->notification, &sequence_display_on);
+}
+
+static void led_changed(VariableItem* item) {
+    NotificationAppSettings* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    variable_item_set_current_value_text(item, backlight_text[index]);
+    app->notification->settings.led_brightness = backlight_value[index];
+    notification_message(app->notification, &sequence_blink_white_100);
+}
+
+static void volume_changed(VariableItem* item) {
+    NotificationAppSettings* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    variable_item_set_current_value_text(item, volume_text[index]);
+    app->notification->settings.speaker_volume = volume_value[index];
+    notification_message(app->notification, &sequence_note_c);
+}
+
+static void vibro_changed(VariableItem* item) {
+    NotificationAppSettings* app = variable_item_get_context(item);
+    uint8_t index = variable_item_get_current_value_index(item);
+
+    variable_item_set_current_value_text(item, vibro_text[index]);
+    app->notification->settings.vibro_on = vibro_value[index];
+    notification_message(app->notification, &sequence_vibro);
+}
+
+static uint32_t notification_app_settings_exit(void* context) {
+    return VIEW_NONE;
+}
+
+static NotificationAppSettings* alloc_settings() {
+    NotificationAppSettings* app = malloc(sizeof(NotificationAppSettings));
+    app->notification = furi_record_open("notification");
+    app->gui = furi_record_open("gui");
+
+    app->variable_item_list = variable_item_list_alloc();
+    View* view = variable_item_list_get_view(app->variable_item_list);
+    view_set_previous_callback(view, notification_app_settings_exit);
+
+    VariableItem* item;
+    uint8_t value_index;
+
+    item = variable_item_list_add(
+        app->variable_item_list, "LCD backlight", BACKLIGHT_COUNT, backlight_changed, app);
+    value_index = float_value_index(
+        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(
+        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(
+        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]);
+
+    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);
+    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);
+    variable_item_set_current_value_index(item, value_index);
+    variable_item_set_current_value_text(item, vibro_text[value_index]);
+
+    app->view_dispatcher = view_dispatcher_alloc();
+    view_dispatcher_enable_queue(app->view_dispatcher);
+    view_dispatcher_attach_to_gui(app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
+    view_dispatcher_add_view(app->view_dispatcher, 0, view);
+    view_dispatcher_switch_to_view(app->view_dispatcher, 0);
+
+    return app;
+}
+
+static void free_settings(NotificationAppSettings* app) {
+    view_dispatcher_remove_view(app->view_dispatcher, 0);
+    variable_item_list_free(app->variable_item_list);
+    view_dispatcher_free(app->view_dispatcher);
+
+    furi_record_close("gui");
+    furi_record_close("notification");
+    free(app);
+}
+
+int32_t notification_app_settings(void* p) {
+    NotificationAppSettings* app = alloc_settings();
+    view_dispatcher_run(app->view_dispatcher);
+    notification_message_save_settings(app->notification);
+    free_settings(app);
+    return 0;
+}

+ 78 - 8
applications/notification/notification-app.c

@@ -1,5 +1,6 @@
 #include <furi.h>
 #include <api-hal.h>
+#include <internal-storage/internal-storage.h>
 #include "notification.h"
 #include "notification-messages.h"
 #include "notification-app.h"
@@ -23,6 +24,13 @@ uint8_t notification_settings_get_display_brightness(NotificationApp* app, uint8
 uint8_t notification_settings_get_rgb_led_brightness(NotificationApp* app, uint8_t value);
 uint32_t notification_settings_display_off_delay_ticks(NotificationApp* app);
 
+void notification_message_save_settings(NotificationApp* app) {
+    NotificationAppMessage m = {.type = SaveSettingsMessage, .back_event = osEventFlagsNew(NULL)};
+    furi_check(osMessageQueuePut(app->queue, &m, 0, osWaitForever) == osOK);
+    osEventFlagsWait(m.back_event, NOTIFICATION_EVENT_COMPLETE, osFlagsWaitAny, osWaitForever);
+    osEventFlagsDelete(m.back_event);
+};
+
 // internal layer
 void notification_apply_internal_led_layer(NotificationLedLayer* layer, uint8_t layer_value) {
     furi_assert(layer);
@@ -103,7 +111,7 @@ void notification_reset_notification_layer(NotificationApp* app, uint8_t reset_m
 static void notification_apply_notification_leds(NotificationApp* app, const uint8_t* values) {
     for(uint8_t i = 0; i < NOTIFICATION_LED_COUNT; i++) {
         notification_apply_notification_led_layer(
-            &app->led[i], notification_settings_get_display_brightness(app, values[i]));
+            &app->led[i], notification_settings_get_rgb_led_brightness(app, values[i]));
     }
 }
 
@@ -197,7 +205,7 @@ void notification_process_notification_message(
             break;
         case NotificationMessageTypeVibro:
             if(notification_message->data.vibro.on) {
-                notification_vibro_on();
+                if(app->settings.vibro_on) notification_vibro_on();
             } else {
                 notification_vibro_off();
             }
@@ -260,10 +268,6 @@ void notification_process_notification_message(
     if(reset_notifications) {
         notification_reset_notification_layer(app, reset_mask);
     }
-
-    if(message->back_event != NULL) {
-        osEventFlagsSet(message->back_event, NOTIFICATION_EVENT_COMPLETE);
-    }
 }
 
 void notification_process_internal_message(NotificationApp* app, NotificationAppMessage* message) {
@@ -303,10 +307,58 @@ void notification_process_internal_message(NotificationApp* app, NotificationApp
         notification_message_index++;
         notification_message = (*message->sequence)[notification_message_index];
     }
+}
+
+static void notification_load_settings(NotificationApp* app) {
+    NotificationSettings settings;
+    InternalStorage* internal_storage = furi_record_open("internal-storage");
+    const size_t settings_size = sizeof(NotificationSettings);
+
+    FURI_LOG_I("notification", "Loading state from internal-storage");
+    int ret = internal_storage_read_key(
+        internal_storage, NOTIFICATION_SETTINGS_PATH, (uint8_t*)&settings, settings_size);
+
+    if(ret != settings_size) {
+        FURI_LOG_E("notification", "Load failed. Storage returned: %d", ret);
+    } else {
+        FURI_LOG_I("notification", "Load success", ret);
+
+        if(settings.version != NOTIFICATION_SETTINGS_VERSION) {
+            FURI_LOG_E(
+                "notification",
+                "Version(%d != %d) mismatch",
+                app->settings.version,
+                NOTIFICATION_SETTINGS_VERSION);
+        } else {
+            osKernelLock();
+            memcpy(&app->settings, &settings, settings_size);
+            osKernelUnlock();
+        }
+    }
+
+    furi_record_close("internal-storage");
+};
+
+static void notification_save_settings(NotificationApp* app) {
+    InternalStorage* internal_storage = furi_record_open("internal-storage");
+    const size_t settings_size = sizeof(NotificationSettings);
 
-    if(message->back_event != NULL) {
-        osEventFlagsSet(message->back_event, NOTIFICATION_EVENT_COMPLETE);
+    FURI_LOG_I("notification", "Saving state to internal-storage");
+    int ret = internal_storage_write_key(
+        internal_storage, NOTIFICATION_SETTINGS_PATH, (uint8_t*)&app->settings, settings_size);
+
+    if(ret != settings_size) {
+        FURI_LOG_E("notification", "Save failed. Storage returned: %d", ret);
+    } else {
+        FURI_LOG_I("notification", "Saved");
     }
+
+    furi_record_close("internal-storage");
+};
+
+static void input_event_callback(const void* value, void* context) {
+    NotificationApp* app = context;
+    notification_message(app, &sequence_display_on);
 }
 
 // App alloc
@@ -319,6 +371,7 @@ static NotificationApp* notification_app_alloc() {
     app->settings.display_brightness = 1.0f;
     app->settings.led_brightness = 1.0f;
     app->settings.display_off_delay_ms = 30000;
+    app->settings.vibro_on = true;
 
     app->display.value[LayerInternal] = 0x00;
     app->display.value[LayerNotification] = 0x00;
@@ -340,6 +393,13 @@ static NotificationApp* notification_app_alloc() {
     app->led[2].index = LayerInternal;
     app->led[2].light = LightBlue;
 
+    app->settings.version = NOTIFICATION_SETTINGS_VERSION;
+
+    // display backlight control
+    app->event_record = furi_record_open("input_events");
+    subscribe_pubsub(app->event_record, input_event_callback, app);
+    notification_message(app, &sequence_display_on);
+
     return app;
 };
 
@@ -347,6 +407,8 @@ static NotificationApp* notification_app_alloc() {
 int32_t notification_app(void* p) {
     NotificationApp* app = notification_app_alloc();
 
+    notification_load_settings(app);
+
     notification_vibro_off();
     notification_sound_off();
     notification_apply_internal_led_layer(&app->display, 0x00);
@@ -366,6 +428,14 @@ int32_t notification_app(void* p) {
             break;
         case InternalLayerMessage:
             notification_process_internal_message(app, &message);
+            break;
+        case SaveSettingsMessage:
+            notification_save_settings(app);
+            break;
+        }
+
+        if(message.back_event != NULL) {
+            osEventFlagsSet(message.back_event, NOTIFICATION_EVENT_COMPLETE);
         }
     }
 

+ 10 - 1
applications/notification/notification-app.h

@@ -9,6 +9,7 @@
 typedef enum {
     NotificationLayerMessage,
     InternalLayerMessage,
+    SaveSettingsMessage,
 } NotificationAppMessageType;
 
 typedef struct {
@@ -29,19 +30,27 @@ typedef struct {
     Light light;
 } NotificationLedLayer;
 
+#define NOTIFICATION_SETTINGS_VERSION 0x01
+#define NOTIFICATION_SETTINGS_PATH "notification_settings"
+
 typedef struct {
+    uint8_t version;
     float display_brightness;
     float led_brightness;
     float speaker_volume;
     uint32_t display_off_delay_ms;
+    bool vibro_on;
 } NotificationSettings;
 
 struct NotificationApp {
     osMessageQueueId_t queue;
+    PubSub* event_record;
     osTimerId_t display_timer;
 
     NotificationLedLayer display;
     NotificationLedLayer led[NOTIFICATION_LED_COUNT];
 
     NotificationSettings settings;
-};
+};
+
+void notification_message_save_settings(NotificationApp* app);