Alexander Kopachov hace 2 años
padre
commit
bc5daa2ed9

+ 9 - 0
.ofwcatalog/CHANGELOG.md

@@ -1,5 +1,14 @@
 # Changelog
 
+## v3.1.0 - 31 Jul 2023
+
+* Fixed [#171](https://github.com/akopachov/flipper-zero_authenticator/issues/171)
+
+## v3.0.2 - 28 Jul 2023
+
+* Fixed [#169](https://github.com/akopachov/flipper-zero_authenticator/issues/169)
+* Fixed [#172](https://github.com/akopachov/flipper-zero_authenticator/issues/172)
+
 ## v3.0.0 - 26 Jul 2023
 
 * Implemented better encryption [#167](https://github.com/akopachov/flipper-zero_authenticator/issues/167)

+ 1 - 1
application.fam

@@ -15,7 +15,7 @@ App(
     ],
     stack_size=2 * 1024,
     order=20,
-    fap_version="3.2",
+    fap_version="3.10",
     fap_author="Alexander Kopachov (@akopachov)",
     fap_description="Software-based TOTP authenticator for Flipper Zero device",
     fap_weburl="https://github.com/akopachov/flipper-zero_authenticator",

+ 66 - 8
cli/commands/automation/automation.c

@@ -10,14 +10,20 @@
 #ifdef TOTP_BADBT_TYPE_ENABLED
 #define TOTP_CLI_COMMAND_AUTOMATION_METHOD_BT "bt"
 #endif
+#define TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_QWERTY "QWERTY"
+#define TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_AZERTY "AZERTY"
+#define TOTP_CLI_COMMAND_AUTOMATION_ARG_KB_LAYOUT_PREFIX "-k"
+#define TOTP_CLI_COMMAND_AUTOMATION_ARG_KB_LAYOUT "layout"
 
 void totp_cli_command_automation_docopt_commands() {
-    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_AUTOMATION "       Get or set automation method\r\n");
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_AUTOMATION "       Get or set automation settings\r\n");
 }
 
 void totp_cli_command_automation_docopt_usage() {
-    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_NAME " " TOTP_CLI_COMMAND_AUTOMATION " " DOCOPT_OPTIONAL(
-        DOCOPT_MULTIPLE(DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_AUTOMATION_ARG_METHOD))) "\r\n");
+    TOTP_CLI_PRINTF("  " TOTP_CLI_COMMAND_NAME " " TOTP_CLI_COMMAND_AUTOMATION " " DOCOPT_OPTIONAL(DOCOPT_OPTION(
+        TOTP_CLI_COMMAND_AUTOMATION_ARG_KB_LAYOUT_PREFIX,
+        DOCOPT_ARGUMENT(
+            TOTP_CLI_COMMAND_AUTOMATION_ARG_KB_LAYOUT))) " " DOCOPT_OPTIONAL(DOCOPT_MULTIPLE(DOCOPT_ARGUMENT(TOTP_CLI_COMMAND_AUTOMATION_ARG_METHOD))) "\r\n");
 }
 
 void totp_cli_command_automation_docopt_arguments() {
@@ -31,7 +37,16 @@ void totp_cli_command_automation_docopt_arguments() {
         "\r\n");
 }
 
-static void totp_cli_command_automation_print_method(AutomationMethod method, const char* color) {
+void totp_cli_command_automation_docopt_options() {
+    TOTP_CLI_PRINTF("  " DOCOPT_OPTION(
+        TOTP_CLI_COMMAND_AUTOMATION_ARG_KB_LAYOUT_PREFIX,
+        DOCOPT_ARGUMENT(
+            TOTP_CLI_COMMAND_AUTOMATION_ARG_KB_LAYOUT)) "    Automation keyboard layout. Must be one of: " TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_QWERTY
+                                                        ", " TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_AZERTY
+                                                        "\r\n");
+}
+
+static void print_method(AutomationMethod method, const char* color) {
 #ifdef TOTP_BADBT_TYPE_ENABLED
     bool has_previous_method = false;
 #endif
@@ -57,6 +72,36 @@ static void totp_cli_command_automation_print_method(AutomationMethod method, co
     }
 }
 
+static void print_kb_layout(AutomationKeyboardLayout layout, const char* color) {
+    char* layoutToPrint;
+    switch(layout) {
+    case AutomationKeyboardLayoutQWERTY:
+        layoutToPrint = TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_QWERTY;
+        break;
+    case AutomationKeyboardLayoutAZERTY:
+        layoutToPrint = TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_AZERTY;
+        break;
+    default:
+        furi_crash("Unknown automation keyboard layout");
+        break;
+    }
+
+    TOTP_CLI_PRINTF_COLORFUL(color, "%s", layoutToPrint);
+}
+
+static bool parse_automation_keyboard_layout(const FuriString* str, AutomationKeyboardLayout* out) {
+    bool result = true;
+    if(furi_string_cmpi_str(str, TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_QWERTY) == 0) {
+        *out = AutomationKeyboardLayoutQWERTY;
+    } else if(furi_string_cmpi_str(str, TOTP_CLI_COMMAND_AUTOMATION_LAYOUT_AZERTY) == 0) {
+        *out = AutomationKeyboardLayoutAZERTY;
+    } else {
+        result = false;
+    }
+
+    return result;
+}
+
 void totp_cli_command_automation_handle(PluginState* plugin_state, FuriString* args, Cli* cli) {
     if(!totp_cli_ensure_authenticated(plugin_state, cli)) {
         return;
@@ -65,6 +110,7 @@ void totp_cli_command_automation_handle(PluginState* plugin_state, FuriString* a
     FuriString* temp_str = furi_string_alloc();
     bool new_method_provided = false;
     AutomationMethod new_method = AutomationMethodNone;
+    AutomationKeyboardLayout new_kb_layout = plugin_state->automation_kb_layout;
     bool args_valid = true;
     while(args_read_string_and_trim(args, temp_str)) {
         if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_AUTOMATION_METHOD_NONE) == 0) {
@@ -80,7 +126,13 @@ void totp_cli_command_automation_handle(PluginState* plugin_state, FuriString* a
             new_method |= AutomationMethodBadBt;
         }
 #endif
-        else {
+        else if(furi_string_cmpi_str(temp_str, TOTP_CLI_COMMAND_AUTOMATION_ARG_KB_LAYOUT_PREFIX) == 0) {
+            if(!args_read_string_and_trim(args, temp_str) ||
+               !parse_automation_keyboard_layout(temp_str, &new_kb_layout)) {
+                args_valid = false;
+                break;
+            }
+        } else {
             args_valid = false;
             break;
         }
@@ -96,9 +148,13 @@ void totp_cli_command_automation_handle(PluginState* plugin_state, FuriString* a
             TOTP_CLI_LOCK_UI(plugin_state);
 
             plugin_state->automation_method = new_method;
+            plugin_state->automation_kb_layout = new_kb_layout;
             if(totp_config_file_update_automation_method(plugin_state)) {
                 TOTP_CLI_PRINTF_SUCCESS("Automation method is set to ");
-                totp_cli_command_automation_print_method(new_method, TOTP_CLI_COLOR_SUCCESS);
+                print_method(new_method, TOTP_CLI_COLOR_SUCCESS);
+                TOTP_CLI_PRINTF_SUCCESS(" (");
+                print_kb_layout(plugin_state->automation_kb_layout, TOTP_CLI_COLOR_SUCCESS);
+                TOTP_CLI_PRINTF_SUCCESS(")");
                 cli_nl();
             } else {
                 totp_cli_print_error_updating_config_file();
@@ -115,8 +171,10 @@ void totp_cli_command_automation_handle(PluginState* plugin_state, FuriString* a
             TOTP_CLI_UNLOCK_UI(plugin_state);
         } else {
             TOTP_CLI_PRINTF_INFO("Current automation method is ");
-            totp_cli_command_automation_print_method(
-                plugin_state->automation_method, TOTP_CLI_COLOR_INFO);
+            print_method(plugin_state->automation_method, TOTP_CLI_COLOR_INFO);
+            TOTP_CLI_PRINTF_INFO(" (");
+            print_kb_layout(plugin_state->automation_kb_layout, TOTP_CLI_COLOR_INFO);
+            TOTP_CLI_PRINTF_INFO(")");
             cli_nl();
         }
     } while(false);

+ 2 - 1
cli/commands/automation/automation.h

@@ -8,4 +8,5 @@
 void totp_cli_command_automation_handle(PluginState* plugin_state, FuriString* args, Cli* cli);
 void totp_cli_command_automation_docopt_commands();
 void totp_cli_command_automation_docopt_usage();
-void totp_cli_command_automation_docopt_arguments();
+void totp_cli_command_automation_docopt_arguments();
+void totp_cli_command_automation_docopt_options();

+ 1 - 0
cli/commands/help/help.c

@@ -65,4 +65,5 @@ void totp_cli_command_help_handle() {
     totp_cli_command_update_docopt_options();
     totp_cli_command_delete_docopt_options();
     totp_cli_command_pin_docopt_options();
+    totp_cli_command_automation_docopt_options();
 }

BIN
images/totp_arrow_bottom_10x5.png


+ 28 - 1
services/config/config.c

@@ -162,7 +162,11 @@ static bool totp_open_config_file(Storage* storage, FlipperFormat** file) {
         flipper_format_write_uint32(
             fff_data_file, TOTP_CONFIG_KEY_AUTOMATION_METHOD, &tmp_uint32, 1);
 
-        tmp_uint32 = 0;
+        tmp_uint32 = AutomationKeyboardLayoutQWERTY;
+        flipper_format_write_uint32(
+            fff_data_file, TOTP_CONFIG_KEY_AUTOMATION_KB_LAYOUT, &tmp_uint32, 1);
+
+        tmp_uint32 = 0; //-V1048
         flipper_format_write_uint32(fff_data_file, TOTP_CONFIG_KEY_FONT, &tmp_uint32, 1);
 
         if(!flipper_format_rewind(fff_data_file)) {
@@ -241,6 +245,12 @@ bool totp_config_file_update_automation_method(const PluginState* plugin_state)
             break;
         }
 
+        tmp_uint32 = plugin_state->automation_kb_layout;
+        if(!flipper_format_insert_or_update_uint32(
+               file, TOTP_CONFIG_KEY_AUTOMATION_KB_LAYOUT, &tmp_uint32, 1)) {
+            break;
+        }
+
         update_result = true;
     } while(false);
 
@@ -273,6 +283,12 @@ bool totp_config_file_update_user_settings(const PluginState* plugin_state) {
             break;
         }
 
+        tmp_uint32 = plugin_state->automation_kb_layout;
+        if(!flipper_format_insert_or_update_uint32(
+               file, TOTP_CONFIG_KEY_AUTOMATION_KB_LAYOUT, &tmp_uint32, 1)) {
+            break;
+        }
+
         update_result = true;
     } while(false);
 
@@ -458,6 +474,17 @@ bool totp_config_file_load(PluginState* const plugin_state) {
             break;
         }
 
+        if(!flipper_format_read_uint32(
+               fff_data_file, TOTP_CONFIG_KEY_AUTOMATION_KB_LAYOUT, &tmp_uint32, 1)) {
+            tmp_uint32 = AutomationKeyboardLayoutQWERTY;
+        }
+
+        plugin_state->automation_kb_layout = tmp_uint32;
+
+        if(!flipper_format_rewind(fff_data_file)) {
+            break;
+        }
+
         if(!flipper_format_read_uint32(fff_data_file, TOTP_CONFIG_KEY_FONT, &tmp_uint32, 1)) {
             tmp_uint32 = 0;
         }

+ 2 - 1
services/config/constants.h

@@ -4,7 +4,7 @@
 
 #define CONFIG_FILE_DIRECTORY_PATH EXT_PATH("authenticator")
 #define CONFIG_FILE_HEADER "Flipper TOTP plugin config file"
-#define CONFIG_FILE_ACTUAL_VERSION (7)
+#define CONFIG_FILE_ACTUAL_VERSION (8)
 
 #define TOTP_CONFIG_KEY_TIMEZONE "Timezone"
 #define TOTP_CONFIG_KEY_TOKEN_NAME "TokenName"
@@ -18,6 +18,7 @@
 #define TOTP_CONFIG_KEY_PINSET "PinIsSet"
 #define TOTP_CONFIG_KEY_NOTIFICATION_METHOD "NotificationMethod"
 #define TOTP_CONFIG_KEY_AUTOMATION_METHOD "AutomationMethod"
+#define TOTP_CONFIG_KEY_AUTOMATION_KB_LAYOUT "AutomationKbLayout"
 #define TOTP_CONFIG_KEY_FONT "Font"
 #define TOTP_CONFIG_KEY_CRYPTO_VERSION "CryptoVersion"
 #define TOTP_CONFIG_KEY_CRYPTO_KEY_SLOT "CryptoKeySlot"

+ 16 - 0
services/config/migrations/common_migration.c

@@ -1,6 +1,7 @@
 #include "common_migration.h"
 #include "../constants.h"
 #include "../../../types/token_info.h"
+#include "../../../types/automation_kb_layout.h"
 #include <flipper_format/flipper_format_i.h>
 
 bool totp_config_migrate_to_latest(
@@ -90,6 +91,21 @@ bool totp_config_migrate_to_latest(
 
         flipper_format_rewind(fff_backup_data_file);
 
+        if(flipper_format_read_string(
+               fff_backup_data_file, TOTP_CONFIG_KEY_AUTOMATION_KB_LAYOUT, temp_str)) {
+            flipper_format_write_string(
+                fff_data_file, TOTP_CONFIG_KEY_AUTOMATION_KB_LAYOUT, temp_str);
+        } else {
+            uint32_t default_automation_kb_layout = AutomationKeyboardLayoutQWERTY;
+            flipper_format_write_uint32(
+                fff_data_file,
+                TOTP_CONFIG_KEY_AUTOMATION_KB_LAYOUT,
+                &default_automation_kb_layout,
+                1);
+        }
+
+        flipper_format_rewind(fff_backup_data_file);
+
         while(true) {
             if(!flipper_format_read_string(
                    fff_backup_data_file, TOTP_CONFIG_KEY_TOKEN_NAME, temp_str)) {

+ 8 - 0
types/automation_kb_layout.h

@@ -0,0 +1,8 @@
+#pragma once
+
+typedef uint8_t AutomationKeyboardLayout;
+
+enum AutomationKeyboardLayouts {
+    AutomationKeyboardLayoutQWERTY = 0,
+    AutomationKeyboardLayoutAZERTY = 1
+};

+ 6 - 0
types/plugin_state.h

@@ -9,6 +9,7 @@
 #include "../services/idle_timeout/idle_timeout.h"
 #include "notification_method.h"
 #include "automation_method.h"
+#include "automation_kb_layout.h"
 #ifdef TOTP_BADBT_TYPE_ENABLED
 #include "../workers/bt_type_code/bt_type_code.h"
 #endif
@@ -83,6 +84,11 @@ typedef struct {
      */
     AutomationMethod automation_method;
 
+    /**
+     * @brief Automation keyboard layout to be used
+     */
+    AutomationKeyboardLayout automation_kb_layout;
+
 #ifdef TOTP_BADBT_TYPE_ENABLED
     /**
      * @brief Bad-Bluetooth worker context

+ 163 - 156
ui/scenes/app_settings/totp_app_settings.c

@@ -17,7 +17,23 @@
 #endif
 
 static const char* YES_NO_LIST[] = {"NO", "YES"};
-static const char* ON_OFF_LIST[] = {"OFF", "ON"};
+static const char* AUTOMATION_LIST[] = {
+    "None",
+    "USB"
+#ifdef TOTP_BADBT_TYPE_ENABLED
+    ,
+    "Bluetooth",
+    "BT and USB"
+#endif
+};
+
+#ifdef TOTP_BADBT_TYPE_ENABLED
+#define AUTOMATION_LIST_MAX_INDEX (3)
+#else
+#define AUTOMATION_LIST_MAX_INDEX (1)
+#endif
+
+static const char* BAD_KB_LAYOUT_LIST[] = {"QWERTY", "AZERTY"};
 static const char* FONT_TEST_STR = "0123BCD";
 static const uint8_t FONT_TEST_STR_LENGTH = 7;
 
@@ -27,10 +43,8 @@ typedef enum {
     FontSelect,
     SoundSwitch,
     VibroSwitch,
-    BadUsbSwitch,
-#ifdef TOTP_BADBT_TYPE_ENABLED
-    BadBtSwitch,
-#endif
+    AutomationSwitch,
+    BadKeyboardLayoutSelect,
     ConfirmButton
 } Control;
 
@@ -39,11 +53,9 @@ typedef struct {
     uint8_t tz_offset_minutes;
     bool notification_sound;
     bool notification_vibro;
-    bool badusb_enabled;
-#ifdef TOTP_BADBT_TYPE_ENABLED
-    bool badbt_enabled;
-#endif
-    uint8_t y_offset;
+    AutomationMethod automation_method;
+    uint16_t y_offset;
+    AutomationKeyboardLayout automation_kb_layout;
     Control selected_control;
     uint8_t active_font;
 } SceneState;
@@ -59,10 +71,9 @@ void totp_scene_app_settings_activate(PluginState* plugin_state) {
     scene_state->tz_offset_minutes = 60.0f * off_dec;
     scene_state->notification_sound = plugin_state->notification_method & NotificationMethodSound;
     scene_state->notification_vibro = plugin_state->notification_method & NotificationMethodVibro;
-    scene_state->badusb_enabled = plugin_state->automation_method & AutomationMethodBadUsb;
-#ifdef TOTP_BADBT_TYPE_ENABLED
-    scene_state->badbt_enabled = plugin_state->automation_method & AutomationMethodBadBt;
-#endif
+    scene_state->automation_method = plugin_state->automation_method;
+    scene_state->automation_kb_layout = plugin_state->automation_kb_layout;
+
     scene_state->active_font = plugin_state->active_font_index;
 }
 
@@ -82,127 +93,121 @@ static void two_digit_to_str(int8_t num, char* str) {
 
 void totp_scene_app_settings_render(Canvas* const canvas, const PluginState* plugin_state) {
     const SceneState* scene_state = plugin_state->current_scene_state;
+    if(scene_state->selected_control < FontSelect) {
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str_aligned(
+            canvas, 0, 0 - scene_state->y_offset, AlignLeft, AlignTop, "Timezone offset");
+        canvas_set_font(canvas, FontSecondary);
+
+        char tmp_str[4];
+        two_digit_to_str(scene_state->tz_offset_hours, &tmp_str[0]);
+        canvas_draw_str_aligned(
+            canvas, 0, 17 - scene_state->y_offset, AlignLeft, AlignTop, "Hours:");
+        ui_control_select_render(
+            canvas,
+            36,
+            10 - scene_state->y_offset,
+            SCREEN_WIDTH - 36 - UI_CONTROL_VSCROLL_WIDTH,
+            &tmp_str[0],
+            scene_state->selected_control == HoursInput);
+
+        two_digit_to_str(scene_state->tz_offset_minutes, &tmp_str[0]);
+        canvas_draw_str_aligned(
+            canvas, 0, 35 - scene_state->y_offset, AlignLeft, AlignTop, "Minutes:");
+        ui_control_select_render(
+            canvas,
+            36,
+            28 - scene_state->y_offset,
+            SCREEN_WIDTH - 36 - UI_CONTROL_VSCROLL_WIDTH,
+            &tmp_str[0],
+            scene_state->selected_control == MinutesInput);
+
+    } else if(scene_state->selected_control < SoundSwitch) {
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str_aligned(
+            canvas, 0, 64 - scene_state->y_offset, AlignLeft, AlignTop, "Font");
+        canvas_set_font(canvas, FontSecondary);
+
+        const FONT_INFO* const font = available_fonts[scene_state->active_font];
+        ui_control_select_render(
+            canvas,
+            0,
+            74 - scene_state->y_offset,
+            SCREEN_WIDTH - UI_CONTROL_VSCROLL_WIDTH,
+            font->name,
+            scene_state->selected_control == FontSelect);
+
+        uint8_t font_x_offset =
+            SCREEN_WIDTH_CENTER -
+            (((font->charInfo[0].width + font->spacePixels) * FONT_TEST_STR_LENGTH) >> 1);
+        uint8_t font_y_offset = 108 - scene_state->y_offset - (font->height >> 1);
+        canvas_draw_str_ex(
+            canvas, font_x_offset, font_y_offset, FONT_TEST_STR, FONT_TEST_STR_LENGTH, font);
+
+    } else if(scene_state->selected_control < AutomationSwitch) {
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str_aligned(
+            canvas, 0, 128 - scene_state->y_offset, AlignLeft, AlignTop, "Notifications");
+        canvas_set_font(canvas, FontSecondary);
+
+        canvas_draw_str_aligned(
+            canvas, 0, 145 - scene_state->y_offset, AlignLeft, AlignTop, "Sound:");
+        ui_control_select_render(
+            canvas,
+            36,
+            138 - scene_state->y_offset,
+            SCREEN_WIDTH - 36 - UI_CONTROL_VSCROLL_WIDTH,
+            YES_NO_LIST[scene_state->notification_sound],
+            scene_state->selected_control == SoundSwitch);
+
+        canvas_draw_str_aligned(
+            canvas, 0, 163 - scene_state->y_offset, AlignLeft, AlignTop, "Vibro:");
+        ui_control_select_render(
+            canvas,
+            36,
+            156 - scene_state->y_offset,
+            SCREEN_WIDTH - 36 - UI_CONTROL_VSCROLL_WIDTH,
+            YES_NO_LIST[scene_state->notification_vibro],
+            scene_state->selected_control == VibroSwitch);
+    } else {
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str_aligned(
+            canvas, 0, 192 - scene_state->y_offset, AlignLeft, AlignTop, "Automation");
+        canvas_set_font(canvas, FontSecondary);
+
+        canvas_draw_str_aligned(
+            canvas, 0, 209 - scene_state->y_offset, AlignLeft, AlignTop, "Method:");
+        ui_control_select_render(
+            canvas,
+            36,
+            202 - scene_state->y_offset,
+            SCREEN_WIDTH - 36 - UI_CONTROL_VSCROLL_WIDTH,
+            AUTOMATION_LIST[scene_state->automation_method],
+            scene_state->selected_control == AutomationSwitch);
+
+        canvas_draw_str_aligned(
+            canvas, 0, 227 - scene_state->y_offset, AlignLeft, AlignTop, "Layout:");
+
+        ui_control_select_render(
+            canvas,
+            36,
+            220 - scene_state->y_offset,
+            SCREEN_WIDTH - 36 - UI_CONTROL_VSCROLL_WIDTH,
+            BAD_KB_LAYOUT_LIST[scene_state->automation_kb_layout],
+            scene_state->selected_control == BadKeyboardLayoutSelect);
+
+        ui_control_button_render(
+            canvas,
+            SCREEN_WIDTH_CENTER - 24,
+            242 - scene_state->y_offset,
+            48,
+            13,
+            "Confirm",
+            scene_state->selected_control == ConfirmButton);
+    }
 
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str_aligned(
-        canvas, 0, 0 - scene_state->y_offset, AlignLeft, AlignTop, "Timezone offset");
-    canvas_set_font(canvas, FontSecondary);
-
-    char tmp_str[4];
-    two_digit_to_str(scene_state->tz_offset_hours, &tmp_str[0]);
-    canvas_draw_str_aligned(canvas, 0, 17 - scene_state->y_offset, AlignLeft, AlignTop, "Hours:");
-    ui_control_select_render(
-        canvas,
-        36,
-        10 - scene_state->y_offset,
-        SCREEN_WIDTH - 36,
-        &tmp_str[0],
-        scene_state->selected_control == HoursInput);
-
-    two_digit_to_str(scene_state->tz_offset_minutes, &tmp_str[0]);
-    canvas_draw_str_aligned(
-        canvas, 0, 35 - scene_state->y_offset, AlignLeft, AlignTop, "Minutes:");
-    ui_control_select_render(
-        canvas,
-        36,
-        28 - scene_state->y_offset,
-        SCREEN_WIDTH - 36,
-        &tmp_str[0],
-        scene_state->selected_control == MinutesInput);
-
-    canvas_draw_icon(
-        canvas,
-        SCREEN_WIDTH_CENTER - 5,
-        SCREEN_HEIGHT - 5 - scene_state->y_offset,
-        &I_totp_arrow_bottom_10x5);
-
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str_aligned(canvas, 0, 64 - scene_state->y_offset, AlignLeft, AlignTop, "Font");
-    canvas_set_font(canvas, FontSecondary);
-
-    const FONT_INFO* const font = available_fonts[scene_state->active_font];
-    ui_control_select_render(
-        canvas,
-        0,
-        74 - scene_state->y_offset,
-        SCREEN_WIDTH,
-        font->name,
-        scene_state->selected_control == FontSelect);
-
-    uint8_t font_x_offset =
-        SCREEN_WIDTH_CENTER -
-        (((font->charInfo[0].width + font->spacePixels) * FONT_TEST_STR_LENGTH) >> 1);
-    uint8_t font_y_offset = 108 - scene_state->y_offset - (font->height >> 1);
-    canvas_draw_str_ex(
-        canvas, font_x_offset, font_y_offset, FONT_TEST_STR, FONT_TEST_STR_LENGTH, font);
-
-    canvas_draw_icon(
-        canvas, SCREEN_WIDTH_CENTER - 5, 123 - scene_state->y_offset, &I_totp_arrow_bottom_10x5);
-
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str_aligned(
-        canvas, 0, 128 - scene_state->y_offset, AlignLeft, AlignTop, "Notifications");
-    canvas_set_font(canvas, FontSecondary);
-
-    canvas_draw_str_aligned(canvas, 0, 145 - scene_state->y_offset, AlignLeft, AlignTop, "Sound:");
-    ui_control_select_render(
-        canvas,
-        36,
-        138 - scene_state->y_offset,
-        SCREEN_WIDTH - 36,
-        YES_NO_LIST[scene_state->notification_sound],
-        scene_state->selected_control == SoundSwitch);
-
-    canvas_draw_str_aligned(canvas, 0, 163 - scene_state->y_offset, AlignLeft, AlignTop, "Vibro:");
-    ui_control_select_render(
-        canvas,
-        36,
-        156 - scene_state->y_offset,
-        SCREEN_WIDTH - 36,
-        YES_NO_LIST[scene_state->notification_vibro],
-        scene_state->selected_control == VibroSwitch);
-
-    canvas_draw_icon(
-        canvas, SCREEN_WIDTH_CENTER - 5, 187 - scene_state->y_offset, &I_totp_arrow_bottom_10x5);
-
-    canvas_set_font(canvas, FontPrimary);
-    canvas_draw_str_aligned(
-        canvas, 0, 192 - scene_state->y_offset, AlignLeft, AlignTop, "Automation");
-    canvas_set_font(canvas, FontSecondary);
-
-    canvas_draw_str_aligned(
-        canvas, 0, 209 - scene_state->y_offset, AlignLeft, AlignTop, "BadUSB:");
-    ui_control_select_render(
-        canvas,
-        36,
-        202 - scene_state->y_offset,
-        SCREEN_WIDTH - 36,
-        ON_OFF_LIST[scene_state->badusb_enabled],
-        scene_state->selected_control == BadUsbSwitch);
-
-#ifdef TOTP_BADBT_TYPE_ENABLED
-    canvas_draw_str_aligned(canvas, 0, 227 - scene_state->y_offset, AlignLeft, AlignTop, "BadBT:");
-    ui_control_select_render(
-        canvas,
-        36,
-        220 - scene_state->y_offset,
-        SCREEN_WIDTH - 36,
-        ON_OFF_LIST[scene_state->badbt_enabled],
-        scene_state->selected_control == BadBtSwitch);
-#endif
-
-    ui_control_button_render(
-        canvas,
-        SCREEN_WIDTH_CENTER - 24,
-#ifdef TOTP_BADBT_TYPE_ENABLED
-        242 - scene_state->y_offset,
-#else
-        229 - scene_state->y_offset,
-#endif
-        48,
-        13,
-        "Confirm",
-        scene_state->selected_control == ConfirmButton);
+    ui_control_vscroll_render(
+        canvas, SCREEN_WIDTH - 3, 0, SCREEN_HEIGHT, scene_state->selected_control, ConfirmButton);
 }
 
 bool totp_scene_app_settings_handle_event(
@@ -267,14 +272,17 @@ bool totp_scene_app_settings_handle_event(
                 scene_state->notification_sound = !scene_state->notification_sound;
             } else if(scene_state->selected_control == VibroSwitch) {
                 scene_state->notification_vibro = !scene_state->notification_vibro;
-            } else if(scene_state->selected_control == BadUsbSwitch) {
-                scene_state->badusb_enabled = !scene_state->badusb_enabled;
-            }
-#ifdef TOTP_BADBT_TYPE_ENABLED
-            else if(scene_state->selected_control == BadBtSwitch) {
-                scene_state->badbt_enabled = !scene_state->badbt_enabled;
+            } else if(scene_state->selected_control == AutomationSwitch) {
+                totp_roll_value_uint8_t(
+                    &scene_state->automation_method,
+                    1,
+                    0,
+                    AUTOMATION_LIST_MAX_INDEX,
+                    RollOverflowBehaviorRoll);
+            } else if(scene_state->selected_control == BadKeyboardLayoutSelect) {
+                totp_roll_value_uint8_t(
+                    &scene_state->automation_kb_layout, 1, 0, 1, RollOverflowBehaviorRoll);
             }
-#endif
             break;
         case InputKeyLeft:
             if(scene_state->selected_control == HoursInput) {
@@ -294,14 +302,17 @@ bool totp_scene_app_settings_handle_event(
                 scene_state->notification_sound = !scene_state->notification_sound;
             } else if(scene_state->selected_control == VibroSwitch) {
                 scene_state->notification_vibro = !scene_state->notification_vibro;
-            } else if(scene_state->selected_control == BadUsbSwitch) {
-                scene_state->badusb_enabled = !scene_state->badusb_enabled;
-            }
-#ifdef TOTP_BADBT_TYPE_ENABLED
-            else if(scene_state->selected_control == BadBtSwitch) {
-                scene_state->badbt_enabled = !scene_state->badbt_enabled;
+            } else if(scene_state->selected_control == AutomationSwitch) {
+                totp_roll_value_uint8_t(
+                    &scene_state->automation_method,
+                    -1,
+                    0,
+                    AUTOMATION_LIST_MAX_INDEX,
+                    RollOverflowBehaviorRoll);
+            } else if(scene_state->selected_control == BadKeyboardLayoutSelect) {
+                totp_roll_value_uint8_t(
+                    &scene_state->automation_kb_layout, -1, 0, 1, RollOverflowBehaviorRoll);
             }
-#endif
             break;
         case InputKeyOk:
             break;
@@ -322,14 +333,9 @@ bool totp_scene_app_settings_handle_event(
             (scene_state->notification_sound ? NotificationMethodSound : NotificationMethodNone) |
             (scene_state->notification_vibro ? NotificationMethodVibro : NotificationMethodNone);
 
-        plugin_state->automation_method = scene_state->badusb_enabled ? AutomationMethodBadUsb :
-                                                                        AutomationMethodNone;
-#ifdef TOTP_BADBT_TYPE_ENABLED
-        plugin_state->automation_method |= scene_state->badbt_enabled ? AutomationMethodBadBt :
-                                                                        AutomationMethodNone;
-#endif
-
+        plugin_state->automation_method = scene_state->automation_method;
         plugin_state->active_font_index = scene_state->active_font;
+        plugin_state->automation_kb_layout = scene_state->automation_kb_layout;
 
         if(!totp_config_file_update_user_settings(plugin_state)) {
             totp_dialogs_config_updating_error(plugin_state);
@@ -337,7 +343,8 @@ bool totp_scene_app_settings_handle_event(
         }
 
 #ifdef TOTP_BADBT_TYPE_ENABLED
-        if(!scene_state->badbt_enabled && plugin_state->bt_type_code_worker_context != NULL) {
+        if((scene_state->automation_method & AutomationMethodBadBt) == 0 &&
+           plugin_state->bt_type_code_worker_context != NULL) {
             totp_bt_type_code_worker_free(plugin_state->bt_type_code_worker_context);
             plugin_state->bt_type_code_worker_context = NULL;
         }

+ 6 - 2
ui/scenes/generate_token/totp_scene_generate_token.c

@@ -205,7 +205,10 @@ void totp_scene_generate_token_activate(PluginState* plugin_state) {
     scene_state->last_code_update_sync = furi_mutex_alloc(FuriMutexTypeNormal);
     if(plugin_state->automation_method & AutomationMethodBadUsb) {
         scene_state->usb_type_code_worker_context = totp_usb_type_code_worker_start(
-            scene_state->last_code, TokenDigitsCountMax + 1, scene_state->last_code_update_sync);
+            scene_state->last_code,
+            TokenDigitsCountMax + 1,
+            scene_state->last_code_update_sync,
+            plugin_state->automation_kb_layout);
     }
 
     scene_state->active_font = available_fonts[plugin_state->active_font_index];
@@ -221,7 +224,8 @@ void totp_scene_generate_token_activate(PluginState* plugin_state) {
             plugin_state->bt_type_code_worker_context,
             scene_state->last_code,
             TokenDigitsCountMax + 1,
-            scene_state->last_code_update_sync);
+            scene_state->last_code_update_sync,
+            plugin_state->automation_kb_layout);
     }
 #endif
     const TokenInfoIteratorContext* iterator_context =

+ 24 - 0
ui/ui_controls.c

@@ -112,3 +112,27 @@ void ui_control_button_render(
         canvas_set_color(canvas, ColorBlack);
     }
 }
+
+void ui_control_vscroll_render(
+    Canvas* const canvas,
+    uint8_t x,
+    uint8_t y,
+    uint8_t height,
+    uint8_t position,
+    uint8_t max_position) {
+    canvas_draw_line(canvas, x, y, x, y + height);
+    uint8_t block_height = height / MIN(10, max_position);
+    uint8_t block_position_y =
+        height * ((float)position / (float)max_position) - (block_height >> 1);
+    uint8_t block_position_y_abs = y + block_position_y;
+    if(block_position_y_abs + block_height > height) {
+        block_position_y_abs = height - block_height;
+    }
+
+    canvas_draw_box(
+        canvas,
+        x - (UI_CONTROL_VSCROLL_WIDTH >> 1),
+        block_position_y_abs,
+        UI_CONTROL_VSCROLL_WIDTH,
+        block_height);
+}

+ 19 - 0
ui/ui_controls.h

@@ -3,6 +3,8 @@
 #include <inttypes.h>
 #include <gui/gui.h>
 
+#define UI_CONTROL_VSCROLL_WIDTH (3)
+
 /**
  * @brief Renders TextBox control
  * @param canvas canvas to render control at
@@ -51,3 +53,20 @@ void ui_control_select_render(
     uint8_t width,
     const char* text,
     bool is_selected);
+
+/**
+ * @brief Renders vertical scroll bar
+ * @param canvas canvas to render control at
+ * @param x horizontal position of a control to be rendered at
+ * @param y vertical position of a control to be rendered at
+ * @param height control height
+ * @param position current position
+ * @param max_position maximal position
+ */
+void ui_control_vscroll_render(
+    Canvas* const canvas,
+    uint8_t x,
+    uint8_t y,
+    uint8_t height,
+    uint8_t position,
+    uint8_t max_position);

+ 6 - 2
workers/bt_type_code/bt_type_code.c

@@ -33,6 +33,7 @@ struct TotpBtTypeCodeWorkerContext {
     char previous_bt_name[TOTP_BT_WORKER_BT_ADV_NAME_MAX_LEN];
     uint8_t previous_bt_mac[TOTP_BT_WORKER_BT_MAC_ADDRESS_LEN];
 #endif
+    AutomationKeyboardLayout keyboard_layout;
 };
 
 static inline bool totp_type_code_worker_stop_requested() {
@@ -73,7 +74,8 @@ static void totp_type_code_worker_type_code(TotpBtTypeCodeWorkerContext* context
             &furi_hal_bt_hid_kb_release,
             context->code_buffer,
             context->code_buffer_size,
-            context->flags);
+            context->flags,
+            context->keyboard_layout);
         furi_mutex_release(context->code_buffer_sync);
     }
 }
@@ -119,11 +121,13 @@ void totp_bt_type_code_worker_start(
     TotpBtTypeCodeWorkerContext* context,
     char* code_buffer,
     uint8_t code_buffer_size,
-    FuriMutex* code_buffer_sync) {
+    FuriMutex* code_buffer_sync,
+    AutomationKeyboardLayout keyboard_layout) {
     furi_check(context != NULL);
     context->code_buffer = code_buffer;
     context->code_buffer_size = code_buffer_size;
     context->code_buffer_sync = code_buffer_sync;
+    context->keyboard_layout = keyboard_layout;
     context->thread = furi_thread_alloc();
     furi_thread_set_name(context->thread, "TOTPBtHidWorker");
     furi_thread_set_stack_size(context->thread, 1024);

+ 4 - 1
workers/bt_type_code/bt_type_code.h

@@ -3,6 +3,7 @@
 #include <stdint.h>
 #include <stdbool.h>
 #include <furi/core/mutex.h>
+#include "../../types/automation_kb_layout.h"
 
 typedef uint8_t TotpBtTypeCodeWorkerEvent;
 
@@ -47,12 +48,14 @@ void totp_bt_type_code_worker_free(TotpBtTypeCodeWorkerContext* context);
  * @param code_buffer code buffer to be used to automate
  * @param code_buffer_size code buffer size
  * @param code_buffer_sync code buffer synchronization primitive
+ * @param keyboard_layout keyboard layout to be used
  */
 void totp_bt_type_code_worker_start(
     TotpBtTypeCodeWorkerContext* context,
     char* code_buffer,
     uint8_t code_buffer_size,
-    FuriMutex* code_buffer_sync);
+    FuriMutex* code_buffer_sync,
+    AutomationKeyboardLayout keyboard_layout);
 
 /**
  * @brief Stops bluetooth token input automation worker

+ 34 - 5
workers/type_code_common.c

@@ -3,7 +3,9 @@
 #include <furi/core/kernel.h>
 #include "../../services/convert/convert.h"
 
-static const uint8_t hid_number_keys[] = {
+#define HID_KEYS_MAP_LENGTH (36)
+
+static const uint8_t hid_qwerty_keys_map[HID_KEYS_MAP_LENGTH] = {
     HID_KEYBOARD_0, HID_KEYBOARD_1, HID_KEYBOARD_2, HID_KEYBOARD_3, HID_KEYBOARD_4,
     HID_KEYBOARD_5, HID_KEYBOARD_6, HID_KEYBOARD_7, HID_KEYBOARD_8, HID_KEYBOARD_9,
     HID_KEYBOARD_A, HID_KEYBOARD_B, HID_KEYBOARD_C, HID_KEYBOARD_D, HID_KEYBOARD_E,
@@ -13,6 +15,16 @@ static const uint8_t hid_number_keys[] = {
     HID_KEYBOARD_U, HID_KEYBOARD_V, HID_KEYBOARD_W, HID_KEYBOARD_X, HID_KEYBOARD_Y,
     HID_KEYBOARD_Z};
 
+static const uint8_t hid_azerty_keys_map[HID_KEYS_MAP_LENGTH] = {
+    HID_KEYBOARD_0, HID_KEYBOARD_1, HID_KEYBOARD_2,         HID_KEYBOARD_3, HID_KEYBOARD_4,
+    HID_KEYBOARD_5, HID_KEYBOARD_6, HID_KEYBOARD_7,         HID_KEYBOARD_8, HID_KEYBOARD_9,
+    HID_KEYBOARD_Q, HID_KEYBOARD_B, HID_KEYBOARD_C,         HID_KEYBOARD_D, HID_KEYBOARD_E,
+    HID_KEYBOARD_F, HID_KEYBOARD_G, HID_KEYBOARD_H,         HID_KEYBOARD_I, HID_KEYBOARD_J,
+    HID_KEYBOARD_K, HID_KEYBOARD_L, HID_KEYBOARD_SEMICOLON, HID_KEYBOARD_N, HID_KEYBOARD_O,
+    HID_KEYBOARD_P, HID_KEYBOARD_A, HID_KEYBOARD_R,         HID_KEYBOARD_S, HID_KEYBOARD_T,
+    HID_KEYBOARD_U, HID_KEYBOARD_V, HID_KEYBOARD_Z,         HID_KEYBOARD_X, HID_KEYBOARD_Y,
+    HID_KEYBOARD_W};
+
 static uint32_t get_keystroke_delay(TokenAutomationFeature features) {
     if(features & TokenAutomationFeatureTypeSlower) {
         return 100;
@@ -44,21 +56,38 @@ void totp_type_code_worker_execute_automation(
     TOTP_AUTOMATION_KEY_HANDLER key_release_fn,
     const char* code_buffer,
     uint8_t code_buffer_size,
-    TokenAutomationFeature features) {
+    TokenAutomationFeature features,
+    AutomationKeyboardLayout keyboard_layout) {
     furi_delay_ms(500);
     uint8_t i = 0;
     char cb_char;
 
+    const uint8_t* keyboard_layout_dict;
+    switch(keyboard_layout) {
+    case AutomationKeyboardLayoutQWERTY:
+        keyboard_layout_dict = &hid_qwerty_keys_map[0];
+        break;
+    case AutomationKeyboardLayoutAZERTY:
+        keyboard_layout_dict = &hid_azerty_keys_map[0];
+        break;
+
+    default:
+        return;
+    }
+
     while(i < code_buffer_size && (cb_char = code_buffer[i]) != 0) {
         uint8_t char_index = CONVERT_CHAR_TO_DIGIT(cb_char);
         if(char_index > 9) {
             char_index = cb_char - 'A' + 10;
         }
 
-        if(char_index >= sizeof(hid_number_keys)) break;
+        if(char_index >= HID_KEYS_MAP_LENGTH) break;
 
-        uint16_t hid_kb_key = hid_number_keys[char_index];
-        if(char_index > 9) {
+        uint16_t hid_kb_key = keyboard_layout_dict[char_index];
+
+        // For non-AZERTY press shift for all non-digit chars
+        // For AZERTY press shift for all characters
+        if(char_index > 9 || keyboard_layout == AutomationKeyboardLayoutAZERTY) {
             hid_kb_key |= KEY_MOD_LEFT_SHIFT;
         }
 

+ 4 - 1
workers/type_code_common.h

@@ -1,6 +1,7 @@
 #pragma once
 #include <stdint.h>
 #include "../types/token_info.h"
+#include "../types/automation_kb_layout.h"
 
 typedef bool (*TOTP_AUTOMATION_KEY_HANDLER)(uint16_t key);
 
@@ -11,10 +12,12 @@ typedef bool (*TOTP_AUTOMATION_KEY_HANDLER)(uint16_t key);
  * @param code_buffer code buffer to be typed
  * @param code_buffer_size code buffer size
  * @param features automation features
+ * @param keyboard_layout keyboard layout to be used
  */
 void totp_type_code_worker_execute_automation(
     TOTP_AUTOMATION_KEY_HANDLER key_press_fn,
     TOTP_AUTOMATION_KEY_HANDLER key_release_fn,
     const char* code_buffer,
     uint8_t code_buffer_size,
-    TokenAutomationFeature features);
+    TokenAutomationFeature features,
+    AutomationKeyboardLayout keyboard_layout);

+ 6 - 2
workers/usb_type_code/usb_type_code.c

@@ -15,6 +15,7 @@ struct TotpUsbTypeCodeWorkerContext {
     FuriThread* thread;
     FuriMutex* code_buffer_sync;
     FuriHalUsbInterface* usb_mode_prev;
+    AutomationKeyboardLayout keyboard_layout;
 };
 
 static void totp_type_code_worker_restore_usb_mode(TotpUsbTypeCodeWorkerContext* context) {
@@ -45,7 +46,8 @@ static void totp_type_code_worker_type_code(TotpUsbTypeCodeWorkerContext* contex
             &furi_hal_hid_kb_release,
             context->code_buffer,
             context->code_buffer_size,
-            context->flags);
+            context->flags,
+            context->keyboard_layout);
         furi_mutex_release(context->code_buffer_sync);
 
         furi_delay_ms(100);
@@ -83,7 +85,8 @@ static int32_t totp_type_code_worker_callback(void* context) {
 TotpUsbTypeCodeWorkerContext* totp_usb_type_code_worker_start(
     char* code_buffer,
     uint8_t code_buffer_size,
-    FuriMutex* code_buffer_sync) {
+    FuriMutex* code_buffer_sync,
+    AutomationKeyboardLayout keyboard_layout) {
     TotpUsbTypeCodeWorkerContext* context = malloc(sizeof(TotpUsbTypeCodeWorkerContext));
     furi_check(context != NULL);
     context->code_buffer = code_buffer;
@@ -91,6 +94,7 @@ TotpUsbTypeCodeWorkerContext* totp_usb_type_code_worker_start(
     context->code_buffer_sync = code_buffer_sync;
     context->thread = furi_thread_alloc();
     context->usb_mode_prev = NULL;
+    context->keyboard_layout = keyboard_layout;
     furi_thread_set_name(context->thread, "TOTPUsbHidWorker");
     furi_thread_set_stack_size(context->thread, 1024);
     furi_thread_set_context(context->thread, context);

+ 4 - 1
workers/usb_type_code/usb_type_code.h

@@ -3,6 +3,7 @@
 #include <stdint.h>
 #include <stdbool.h>
 #include <furi/core/mutex.h>
+#include "../../types/automation_kb_layout.h"
 
 typedef uint8_t TotpUsbTypeCodeWorkerEvent;
 
@@ -34,12 +35,14 @@ enum TotpUsbTypeCodeWorkerEvents {
  * @param code_buffer code buffer to be used to automate
  * @param code_buffer_size code buffer size
  * @param code_buffer_sync code buffer synchronization primitive
+ * @param keyboard_layout keyboard layout to be used
  * @return worker context
  */
 TotpUsbTypeCodeWorkerContext* totp_usb_type_code_worker_start(
     char* code_buffer,
     uint8_t code_buffer_size,
-    FuriMutex* code_buffer_sync);
+    FuriMutex* code_buffer_sync,
+    AutomationKeyboardLayout keyboard_layout);
 
 /**
  * @brief Stops USB token input automation worker