From c17ec1d1aa867a6cb302d7f8c86f6929be6c5c40 Mon Sep 17 00:00:00 2001
From: Charles Lombardo <clombardo169@gmail.com>
Date: Wed, 22 Nov 2023 17:31:48 -0500
Subject: [PATCH] Android UI Overhaul Part 2 (#7147)

---
 .../java/org/citra/citra_emu/NativeLibrary.kt |    8 -
 .../activities/EmulationActivity.java         |    6 +-
 .../citra_emu/dialogs/MotionAlertDialog.java  |  140 ---
 .../settings/model/AbstractBooleanSetting.kt  |    9 +
 .../settings/model/AbstractFloatSetting.kt    |    9 +
 .../settings/model/AbstractIntSetting.kt      |    9 +
 .../settings/model/AbstractSetting.kt         |   13 +
 .../settings/model/AbstractStringSetting.kt   |    9 +
 .../settings/model/BooleanSetting.java        |   23 -
 .../features/settings/model/BooleanSetting.kt |   43 +
 .../features/settings/model/FloatSetting.java |   23 -
 .../features/settings/model/FloatSetting.kt   |   37 +
 .../features/settings/model/IntSetting.java   |   23 -
 .../features/settings/model/IntSetting.kt     |   75 ++
 .../settings/model/ScaledFloatSetting.kt      |   41 +
 .../features/settings/model/Setting.java      |   42 -
 .../settings/model/SettingSection.java        |   55 -
 .../features/settings/model/SettingSection.kt |   38 +
 .../features/settings/model/Settings.java     |  131 ---
 .../features/settings/model/Settings.kt       |  201 ++++
 .../settings/model/SettingsViewModel.kt       |   11 +
 .../settings/model/StringSetting.java         |   23 -
 .../features/settings/model/StringSetting.kt  |   50 +
 .../model/view/AbstractShortSetting.kt        |   11 +
 .../settings/model/view/CheckBoxSetting.java  |   80 --
 .../settings/model/view/DateTimeSetting.java  |   40 -
 .../settings/model/view/DateTimeSetting.kt    |   32 +
 .../settings/model/view/HeaderSetting.java    |   14 -
 .../settings/model/view/HeaderSetting.kt      |    9 +
 .../model/view/InputBindingSetting.java       |  382 ------
 .../model/view/InputBindingSetting.kt         |  299 +++++
 .../settings/model/view/RunnableSetting.kt    |   15 +
 .../settings/model/view/SettingsItem.java     |  100 --
 .../settings/model/view/SettingsItem.kt       |   42 +
 .../model/view/SingleChoiceSetting.java       |   60 -
 .../model/view/SingleChoiceSetting.kt         |   60 +
 .../settings/model/view/SliderSetting.java    |  101 --
 .../settings/model/view/SliderSetting.kt      |   70 ++
 .../settings/model/view/StringInputSetting.kt |   27 +
 .../model/view/StringSingleChoiceSetting.java |   82 --
 .../model/view/StringSingleChoiceSetting.kt   |   78 ++
 .../settings/model/view/SubmenuSetting.java   |   21 -
 .../settings/model/view/SubmenuSetting.kt     |   13 +
 .../settings/model/view/SwitchSetting.kt      |   63 +
 .../settings/ui/SettingsActivity.java         |  227 ----
 .../features/settings/ui/SettingsActivity.kt  |  292 +++++
 .../ui/SettingsActivityPresenter.java         |   91 --
 .../settings/ui/SettingsActivityPresenter.kt  |   78 ++
 .../settings/ui/SettingsActivityView.java     |   87 --
 .../settings/ui/SettingsActivityView.kt       |   58 +
 .../features/settings/ui/SettingsAdapter.java |  393 -------
 .../features/settings/ui/SettingsAdapter.kt   |  503 ++++++++
 .../settings/ui/SettingsFragment.java         |  151 ---
 .../features/settings/ui/SettingsFragment.kt  |  128 ++
 .../ui/SettingsFragmentPresenter.java         |  410 -------
 .../settings/ui/SettingsFragmentPresenter.kt  | 1040 +++++++++++++++++
 .../settings/ui/SettingsFragmentView.java     |   78 --
 .../settings/ui/SettingsFragmentView.kt       |   59 +
 .../viewholder/CheckBoxSettingViewHolder.java |   54 -
 .../ui/viewholder/DateTimeViewHolder.java     |   47 -
 .../ui/viewholder/DateTimeViewHolder.kt       |   77 ++
 .../ui/viewholder/HeaderViewHolder.java       |   32 -
 .../ui/viewholder/HeaderViewHolder.kt         |   31 +
 .../InputBindingSettingViewHolder.java        |   55 -
 .../InputBindingSettingViewHolder.kt          |   60 +
 .../ui/viewholder/RunnableViewHolder.kt       |   58 +
 .../ui/viewholder/SettingViewHolder.java      |   49 -
 .../ui/viewholder/SettingViewHolder.kt        |   37 +
 .../ui/viewholder/SingleChoiceViewHolder.java |   62 -
 .../ui/viewholder/SingleChoiceViewHolder.kt   |   94 ++
 .../ui/viewholder/SliderViewHolder.java       |   46 -
 .../ui/viewholder/SliderViewHolder.kt         |   65 ++
 .../ui/viewholder/StringInputViewHolder.kt    |   46 +
 .../ui/viewholder/SubmenuViewHolder.java      |   45 -
 .../ui/viewholder/SubmenuViewHolder.kt        |   36 +
 .../ui/viewholder/SwitchSettingViewHolder.kt  |   62 +
 .../features/settings/utils/SettingsFile.java |  344 ------
 .../features/settings/utils/SettingsFile.kt   |  258 ++++
 .../fragments/HomeSettingsFragment.kt         |    7 +
 .../MotionBottomSheetDialogFragment.kt        |  208 ++++
 .../fragments/ResetSettingsDialogFragment.kt  |   31 +
 .../fragments/SystemFilesFragment.kt          |   10 +-
 .../citra/citra_emu/ui/main/MainActivity.kt   |   18 +-
 .../citra/citra_emu/ui/main/ThemeProvider.kt  |   12 +
 .../citra/citra_emu/utils/SystemSaveGame.kt   |   67 ++
 .../org/citra/citra_emu/utils/ThemeUtil.kt    |   23 +
 src/android/app/src/main/jni/CMakeLists.txt   |    1 +
 src/android/app/src/main/jni/config.cpp       |   31 +-
 src/android/app/src/main/jni/config.h         |    1 -
 src/android/app/src/main/jni/native.cpp       |   26 -
 .../app/src/main/jni/system_save_game.cpp     |  122 ++
 .../app/src/main/res/drawable/ic_palette.xml  |    9 +
 .../src/main/res/layout/activity_settings.xml |   35 +-
 .../app/src/main/res/layout/dialog_input.xml  |   64 +
 .../res/layout/dialog_software_keyboard.xml   |   26 +
 .../src/main/res/layout/list_item_setting.xml |   67 +-
 .../res/layout/list_item_setting_switch.xml   |   53 +
 .../res/layout/list_item_settings_header.xml  |   29 +-
 .../res/layout/sysclock_datetime_picker.xml   |   22 -
 .../src/main/res/values-night-v31/themes.xml  |   29 +
 .../app/src/main/res/values-night/themes.xml  |    9 +
 .../app/src/main/res/values-v31/themes.xml    |   29 +
 .../app/src/main/res/values/arrays.xml        |  397 +++++--
 .../app/src/main/res/values/strings.xml       |  268 ++++-
 104 files changed, 5613 insertions(+), 3752 deletions(-)
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractBooleanSetting.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractFloatSetting.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractIntSetting.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractSetting.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractStringSetting.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/ScaledFloatSetting.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Setting.java
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/AbstractShortSetting.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/CheckBoxSetting.java
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringInputSetting.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SwitchSetting.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/fragments/ResetSettingsDialogFragment.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/ui/main/ThemeProvider.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/SystemSaveGame.kt
 create mode 100644 src/android/app/src/main/jni/system_save_game.cpp
 create mode 100644 src/android/app/src/main/res/drawable/ic_palette.xml
 create mode 100644 src/android/app/src/main/res/layout/dialog_input.xml
 create mode 100644 src/android/app/src/main/res/layout/dialog_software_keyboard.xml
 create mode 100644 src/android/app/src/main/res/layout/list_item_setting_switch.xml
 delete mode 100644 src/android/app/src/main/res/layout/sysclock_datetime_picker.xml
 create mode 100644 src/android/app/src/main/res/values-night-v31/themes.xml
 create mode 100644 src/android/app/src/main/res/values-night/themes.xml
 create mode 100644 src/android/app/src/main/res/values-v31/themes.xml

diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt
index ebcfa1933e..3a357ed319 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt
@@ -515,14 +515,6 @@ object NativeLibrary {
      */
     external fun logDeviceInfo()
 
-    external fun loadSystemConfig()
-
-    external fun saveSystemConfig()
-
-    external fun setSystemSetupNeeded(needed: Boolean)
-
-    external fun getIsSystemSetupNeeded(): Boolean
-
     @Keep
     @JvmStatic
     fun createFile(directory: String, filename: String): Boolean =
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java
index 6a6075782c..2f631b61ee 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java
+++ b/src/android/app/src/main/java/org/citra/citra_emu/activities/EmulationActivity.java
@@ -535,7 +535,7 @@ public final class EmulationActivity extends AppCompatActivity {
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
         int action;
-        int button = mPreferences.getInt(InputBindingSetting.getInputButtonKey(event.getKeyCode()), event.getKeyCode());
+        int button = mPreferences.getInt(InputBindingSetting.Companion.getInputButtonKey(event.getKeyCode()), event.getKeyCode());
 
         switch (event.getAction()) {
             case KeyEvent.ACTION_DOWN:
@@ -693,8 +693,8 @@ public final class EmulationActivity extends AppCompatActivity {
             int axis = range.getAxis();
             float origValue = event.getAxisValue(axis);
             float value = mControllerMappingHelper.scaleAxis(input, axis, origValue);
-            int nextMapping = mPreferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1);
-            int guestOrientation = mPreferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1);
+            int nextMapping = mPreferences.getInt(InputBindingSetting.Companion.getInputAxisButtonKey(axis), -1);
+            int guestOrientation = mPreferences.getInt(InputBindingSetting.Companion.getInputAxisOrientationKey(axis), -1);
 
             if (nextMapping == -1 || guestOrientation == -1) {
                 // Axis is unmapped
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java b/src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java
deleted file mode 100644
index 0f10f1858e..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/dialogs/MotionAlertDialog.java
+++ /dev/null
@@ -1,140 +0,0 @@
-package org.citra.citra_emu.dialogs;
-
-import android.content.Context;
-import android.view.InputDevice;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-
-import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
-import org.citra.citra_emu.utils.Log;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * {@link AlertDialog} derivative that listens for
- * motion events from controllers and joysticks.
- */
-public final class MotionAlertDialog extends AlertDialog {
-    // The selected input preference
-    private final InputBindingSetting setting;
-    private final ArrayList<Float> mPreviousValues = new ArrayList<>();
-    private int mPrevDeviceId = 0;
-    private boolean mWaitingForEvent = true;
-
-    /**
-     * Constructor
-     *
-     * @param context The current {@link Context}.
-     * @param setting The Preference to show this dialog for.
-     */
-    public MotionAlertDialog(Context context, InputBindingSetting setting) {
-        super(context);
-
-        this.setting = setting;
-    }
-
-    public boolean onKeyEvent(int keyCode, KeyEvent event) {
-        Log.debug("[MotionAlertDialog] Received key event: " + event.getAction());
-        switch (event.getAction()) {
-            case KeyEvent.ACTION_UP:
-                setting.onKeyInput(event);
-                dismiss();
-                // Even if we ignore the key, we still consume it. Thus return true regardless.
-                return true;
-
-            default:
-                return false;
-        }
-    }
-
-    @Override
-    public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) {
-        return super.onKeyLongPress(keyCode, event);
-    }
-
-    @Override
-    public boolean dispatchKeyEvent(KeyEvent event) {
-        // Handle this key if we care about it, otherwise pass it down the framework
-        return onKeyEvent(event.getKeyCode(), event) || super.dispatchKeyEvent(event);
-    }
-
-    @Override
-    public boolean dispatchGenericMotionEvent(@NonNull MotionEvent event) {
-        // Handle this event if we care about it, otherwise pass it down the framework
-        return onMotionEvent(event) || super.dispatchGenericMotionEvent(event);
-    }
-
-    private boolean onMotionEvent(MotionEvent event) {
-        if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)
-            return false;
-        if (event.getAction() != MotionEvent.ACTION_MOVE)
-            return false;
-
-        InputDevice input = event.getDevice();
-
-        List<InputDevice.MotionRange> motionRanges = input.getMotionRanges();
-
-        if (input.getId() != mPrevDeviceId) {
-            mPreviousValues.clear();
-        }
-        mPrevDeviceId = input.getId();
-        boolean firstEvent = mPreviousValues.isEmpty();
-
-        int numMovedAxis = 0;
-        float axisMoveValue = 0.0f;
-        InputDevice.MotionRange lastMovedRange = null;
-        char lastMovedDir = '?';
-        if (mWaitingForEvent) {
-            for (int i = 0; i < motionRanges.size(); i++) {
-                InputDevice.MotionRange range = motionRanges.get(i);
-                int axis = range.getAxis();
-                float origValue = event.getAxisValue(axis);
-                float value = origValue;//ControllerMappingHelper.scaleAxis(input, axis, origValue);
-                if (firstEvent) {
-                    mPreviousValues.add(value);
-                } else {
-                    float previousValue = mPreviousValues.get(i);
-
-                    // Only handle the axes that are not neutral (more than 0.5)
-                    // but ignore any axis that has a constant value (e.g. always 1)
-                    if (Math.abs(value) > 0.5f && value != previousValue) {
-                        // It is common to have multiple axes with the same physical input. For example,
-                        // shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE.
-                        // To handle this, we ignore an axis motion that's the exact same as a motion
-                        // we already saw. This way, we ignore axes with two names, but catch the case
-                        // where a joystick is moved in two directions.
-                        // ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html
-                        if (value != axisMoveValue) {
-                            axisMoveValue = value;
-                            numMovedAxis++;
-                            lastMovedRange = range;
-                            lastMovedDir = value < 0.0f ? '-' : '+';
-                        }
-                    }
-                    // Special case for d-pads (axis value jumps between 0 and 1 without any values
-                    // in between). Without this, the user would need to press the d-pad twice
-                    // due to the first press being caught by the "if (firstEvent)" case further up.
-                    else if (Math.abs(value) < 0.25f && Math.abs(previousValue) > 0.75f) {
-                        numMovedAxis++;
-                        lastMovedRange = range;
-                        lastMovedDir = previousValue < 0.0f ? '-' : '+';
-                    }
-                }
-
-                mPreviousValues.set(i, value);
-            }
-
-            // If only one axis moved, that's the winner.
-            if (numMovedAxis == 1) {
-                mWaitingForEvent = false;
-                setting.onMotionInput(input, lastMovedRange, lastMovedDir);
-                dismiss();
-            }
-        }
-        return true;
-    }
-}
\ No newline at end of file
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractBooleanSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractBooleanSetting.kt
new file mode 100644
index 0000000000..e60b1ca36b
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractBooleanSetting.kt
@@ -0,0 +1,9 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model
+
+interface AbstractBooleanSetting : AbstractSetting {
+    var boolean: Boolean
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractFloatSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractFloatSetting.kt
new file mode 100644
index 0000000000..c3b2c8e2e6
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractFloatSetting.kt
@@ -0,0 +1,9 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model
+
+interface AbstractFloatSetting : AbstractSetting {
+    var float: Float
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractIntSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractIntSetting.kt
new file mode 100644
index 0000000000..7c36608547
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractIntSetting.kt
@@ -0,0 +1,9 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model
+
+interface AbstractIntSetting : AbstractSetting {
+    var int: Int
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractSetting.kt
new file mode 100644
index 0000000000..54af79efb0
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractSetting.kt
@@ -0,0 +1,13 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model
+
+interface AbstractSetting {
+    val key: String?
+    val section: String?
+    val isRuntimeEditable: Boolean
+    val valueAsString: String
+    val defaultValue: Any
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractStringSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractStringSetting.kt
new file mode 100644
index 0000000000..41ecc50381
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractStringSetting.kt
@@ -0,0 +1,9 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model
+
+interface AbstractStringSetting : AbstractSetting {
+    var string: String
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.java
deleted file mode 100644
index 932dcf1d32..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.citra.citra_emu.features.settings.model;
-
-public final class BooleanSetting extends Setting {
-    private boolean mValue;
-
-    public BooleanSetting(String key, String section, boolean value) {
-        super(key, section);
-        mValue = value;
-    }
-
-    public boolean getValue() {
-        return mValue;
-    }
-
-    public void setValue(boolean value) {
-        mValue = value;
-    }
-
-    @Override
-    public String getValueAsString() {
-        return mValue ? "True" : "False";
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt
new file mode 100644
index 0000000000..28b026f918
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/BooleanSetting.kt
@@ -0,0 +1,43 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model
+
+enum class BooleanSetting(
+    override val key: String,
+    override val section: String,
+    override val defaultValue: Boolean
+) : AbstractBooleanSetting {
+    SPIRV_SHADER_GEN("spirv_shader_gen", Settings.SECTION_RENDERER, true),
+    ASYNC_SHADERS("async_shader_compilation", Settings.SECTION_RENDERER, false),
+    PLUGIN_LOADER("plugin_loader", Settings.SECTION_SYSTEM, false),
+    ALLOW_PLUGIN_LOADER("allow_plugin_loader", Settings.SECTION_SYSTEM, true);
+
+    override var boolean: Boolean = defaultValue
+
+    override val valueAsString: String
+        get() = boolean.toString()
+
+    override val isRuntimeEditable: Boolean
+        get() {
+            for (setting in NOT_RUNTIME_EDITABLE) {
+                if (setting == this) {
+                    return false
+                }
+            }
+            return true
+        }
+
+    companion object {
+        private val NOT_RUNTIME_EDITABLE = listOf(
+            PLUGIN_LOADER,
+            ALLOW_PLUGIN_LOADER
+        )
+
+        fun from(key: String): BooleanSetting? =
+            BooleanSetting.values().firstOrNull { it.key == key }
+
+        fun clear() = BooleanSetting.values().forEach { it.boolean = it.defaultValue }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.java
deleted file mode 100644
index 275f0eceae..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.citra.citra_emu.features.settings.model;
-
-public final class FloatSetting extends Setting {
-    private float mValue;
-
-    public FloatSetting(String key, String section, float value) {
-        super(key, section);
-        mValue = value;
-    }
-
-    public float getValue() {
-        return mValue;
-    }
-
-    public void setValue(float value) {
-        mValue = value;
-    }
-
-    @Override
-    public String getValueAsString() {
-        return Float.toString(mValue);
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.kt
new file mode 100644
index 0000000000..81c5dbbf84
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/FloatSetting.kt
@@ -0,0 +1,37 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model
+
+enum class FloatSetting(
+    override val key: String,
+    override val section: String,
+    override val defaultValue: Float
+) : AbstractFloatSetting {
+    // There are no float settings currently
+    EMPTY_SETTING("", "", 0.0f);
+
+    override var float: Float = defaultValue
+
+    override val valueAsString: String
+        get() = float.toString()
+
+    override val isRuntimeEditable: Boolean
+        get() {
+            for (setting in NOT_RUNTIME_EDITABLE) {
+                if (setting == this) {
+                    return false
+                }
+            }
+            return true
+        }
+
+    companion object {
+        private val NOT_RUNTIME_EDITABLE = emptyList<FloatSetting>()
+
+        fun from(key: String): FloatSetting? = FloatSetting.values().firstOrNull { it.key == key }
+
+        fun clear() = FloatSetting.values().forEach { it.float = it.defaultValue }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.java
deleted file mode 100644
index f712e5bfa4..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.citra.citra_emu.features.settings.model;
-
-public final class IntSetting extends Setting {
-    private int mValue;
-
-    public IntSetting(String key, String section, int value) {
-        super(key, section);
-        mValue = value;
-    }
-
-    public int getValue() {
-        return mValue;
-    }
-
-    public void setValue(int value) {
-        mValue = value;
-    }
-
-    @Override
-    public String getValueAsString() {
-        return Integer.toString(mValue);
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt
new file mode 100644
index 0000000000..12d0b4ae42
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt
@@ -0,0 +1,75 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model
+
+enum class IntSetting(
+    override val key: String,
+    override val section: String,
+    override val defaultValue: Int
+) : AbstractIntSetting {
+    FRAME_LIMIT("frame_limit", Settings.SECTION_RENDERER, 100),
+    EMULATED_REGION("region_value", Settings.SECTION_SYSTEM, -1),
+    INIT_CLOCK("init_clock", Settings.SECTION_SYSTEM, 0),
+    CAMERA_INNER_FLIP("camera_inner_flip", Settings.SECTION_CAMERA, 0),
+    CAMERA_OUTER_LEFT_FLIP("camera_outer_left_flip", Settings.SECTION_CAMERA, 0),
+    CAMERA_OUTER_RIGHT_FLIP("camera_outer_right_flip", Settings.SECTION_CAMERA, 0),
+    GRAPHICS_API("graphics_api", Settings.SECTION_RENDERER, 1),
+    RESOLUTION_FACTOR("resolution_factor", Settings.SECTION_RENDERER, 1),
+    STEREOSCOPIC_3D_MODE("render_3d", Settings.SECTION_RENDERER, 0),
+    STEREOSCOPIC_3D_DEPTH("factor_3d", Settings.SECTION_RENDERER, 0),
+    CARDBOARD_SCREEN_SIZE("cardboard_screen_size", Settings.SECTION_LAYOUT, 85),
+    CARDBOARD_X_SHIFT("cardboard_x_shift", Settings.SECTION_LAYOUT, 0),
+    CARDBOARD_Y_SHIFT("cardboard_y_shift", Settings.SECTION_LAYOUT, 0),
+    AUDIO_INPUT_TYPE("output_type", Settings.SECTION_AUDIO, 0),
+    NEW_3DS("is_new_3ds", Settings.SECTION_SYSTEM, 1),
+    CPU_CLOCK_SPEED("cpu_clock_percentage", Settings.SECTION_CORE, 100),
+    LINEAR_FILTERING("filter_mode", Settings.SECTION_RENDERER, 1),
+    SHADERS_ACCURATE_MUL("shaders_accurate_mul", Settings.SECTION_RENDERER, 0),
+    DISK_SHADER_CACHE("use_disk_shader_cache", Settings.SECTION_RENDERER, 1),
+    DUMP_TEXTURES("dump_textures", Settings.SECTION_UTILITY, 0),
+    CUSTOM_TEXTURES("custom_textures", Settings.SECTION_UTILITY, 0),
+    ASYNC_CUSTOM_LOADING("async_custom_loading", Settings.SECTION_UTILITY, 1),
+    PRELOAD_TEXTURES("preload_textures", Settings.SECTION_UTILITY, 0),
+    ENABLE_AUDIO_STRETCHING("enable_audio_stretching", Settings.SECTION_AUDIO, 1),
+    CPU_JIT("use_cpu_jit", Settings.SECTION_CORE, 1),
+    HW_SHADER("use_hw_shader", Settings.SECTION_RENDERER, 1),
+    VSYNC("use_vsync_new", Settings.SECTION_RENDERER, 1),
+    DEBUG_RENDERER("renderer_debug", Settings.SECTION_DEBUG, 0),
+    TEXTURE_FILTER("texture_filter", Settings.SECTION_RENDERER, 0),
+    USE_FRAME_LIMIT("use_frame_limit", Settings.SECTION_RENDERER, 1);
+
+    override var int: Int = defaultValue
+
+    override val valueAsString: String
+        get() = int.toString()
+
+    override val isRuntimeEditable: Boolean
+        get() {
+            for (setting in NOT_RUNTIME_EDITABLE) {
+                if (setting == this) {
+                    return false
+                }
+            }
+            return true
+        }
+
+    companion object {
+        private val NOT_RUNTIME_EDITABLE = listOf(
+            EMULATED_REGION,
+            INIT_CLOCK,
+            NEW_3DS,
+            GRAPHICS_API,
+            VSYNC,
+            DEBUG_RENDERER,
+            CPU_JIT,
+            ASYNC_CUSTOM_LOADING,
+            AUDIO_INPUT_TYPE
+        )
+
+        fun from(key: String): IntSetting? = IntSetting.values().firstOrNull { it.key == key }
+
+        fun clear() = IntSetting.values().forEach { it.int = it.defaultValue }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/ScaledFloatSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/ScaledFloatSetting.kt
new file mode 100644
index 0000000000..21629f7b0a
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/ScaledFloatSetting.kt
@@ -0,0 +1,41 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model
+
+enum class ScaledFloatSetting(
+    override val key: String,
+    override val section: String,
+    override val defaultValue: Float,
+    val scale: Int
+) : AbstractFloatSetting {
+    AUDIO_VOLUME("volume", Settings.SECTION_AUDIO, 1.0f, 100);
+
+    override var float: Float = defaultValue
+        get() = field * scale
+        set(value) {
+            field = value / scale
+        }
+
+    override val valueAsString: String get() = (float / scale).toString()
+
+    override val isRuntimeEditable: Boolean
+        get() {
+            for (setting in NOT_RUNTIME_EDITABLE) {
+                if (setting == this) {
+                    return false
+                }
+            }
+            return true
+        }
+
+    companion object {
+        private val NOT_RUNTIME_EDITABLE = emptyList<ScaledFloatSetting>()
+
+        fun from(key: String): ScaledFloatSetting? =
+            ScaledFloatSetting.values().firstOrNull { it.key == key }
+
+        fun clear() = ScaledFloatSetting.values().forEach { it.float = it.defaultValue * it.scale }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Setting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Setting.java
deleted file mode 100644
index b762847c94..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Setting.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package org.citra.citra_emu.features.settings.model;
-
-/**
- * Abstraction for a setting item as read from / written to Citra's configuration ini files.
- * These files generally consist of a key/value pair, though the type of value is ambiguous and
- * must be inferred at read-time. The type of value determines which child of this class is used
- * to represent the Setting.
- */
-public abstract class Setting {
-    private String mKey;
-    private String mSection;
-
-    /**
-     * Base constructor.
-     *
-     * @param key     Everything to the left of the = in a line from the ini file.
-     * @param section The corresponding recent section header; e.g. [Core] or [Enhancements] without the brackets.
-     */
-    public Setting(String key, String section) {
-        mKey = key;
-        mSection = section;
-    }
-
-    /**
-     * @return The identifier used to write this setting to the ini file.
-     */
-    public String getKey() {
-        return mKey;
-    }
-
-    /**
-     * @return The name of the header under which this Setting should be written in the ini file.
-     */
-    public String getSection() {
-        return mSection;
-    }
-
-    /**
-     * @return A representation of this Setting's backing value converted to a String (e.g. for serialization).
-     */
-    public abstract String getValueAsString();
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.java
deleted file mode 100644
index 0a291aa6bb..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package org.citra.citra_emu.features.settings.model;
-
-import java.util.HashMap;
-
-/**
- * A semantically-related group of Settings objects. These Settings are
- * internally stored as a HashMap.
- */
-public final class SettingSection {
-    private String mName;
-
-    private HashMap<String, Setting> mSettings = new HashMap<>();
-
-    /**
-     * Create a new SettingSection with no Settings in it.
-     *
-     * @param name The header of this section; e.g. [Core] or [Enhancements] without the brackets.
-     */
-    public SettingSection(String name) {
-        mName = name;
-    }
-
-    public String getName() {
-        return mName;
-    }
-
-    /**
-     * Convenience method; inserts a value directly into the backing HashMap.
-     *
-     * @param setting The Setting to be inserted.
-     */
-    public void putSetting(Setting setting) {
-        mSettings.put(setting.getKey(), setting);
-    }
-
-    /**
-     * Convenience method; gets a value directly from the backing HashMap.
-     *
-     * @param key Used to retrieve the Setting.
-     * @return A Setting object (you should probably cast this before using)
-     */
-    public Setting getSetting(String key) {
-        return mSettings.get(key);
-    }
-
-    public HashMap<String, Setting> getSettings() {
-        return mSettings;
-    }
-
-    public void mergeSection(SettingSection settingSection) {
-        for (Setting setting : settingSection.mSettings.values()) {
-            putSetting(setting);
-        }
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.kt
new file mode 100644
index 0000000000..02c5fa2d56
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingSection.kt
@@ -0,0 +1,38 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model
+
+/**
+ * A semantically-related group of Settings objects. These Settings are
+ * internally stored as a HashMap.
+ */
+class SettingSection(val name: String) {
+    val settings = HashMap<String, AbstractSetting>()
+
+    /**
+     * Convenience method; inserts a value directly into the backing HashMap.
+     *
+     * @param setting The Setting to be inserted.
+     */
+    fun putSetting(setting: AbstractSetting) {
+        settings[setting.key!!] = setting
+    }
+
+    /**
+     * Convenience method; gets a value directly from the backing HashMap.
+     *
+     * @param key Used to retrieve the Setting.
+     * @return A Setting object (you should probably cast this before using)
+     */
+    fun getSetting(key: String): AbstractSetting? {
+        return settings[key]
+    }
+
+    fun mergeSection(settingSection: SettingSection) {
+        for (setting in settingSection.settings.values) {
+            putSetting(setting)
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.java
deleted file mode 100644
index 997dd1e26c..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.java
+++ /dev/null
@@ -1,131 +0,0 @@
-package org.citra.citra_emu.features.settings.model;
-
-import android.text.TextUtils;
-
-import org.citra.citra_emu.CitraApplication;
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.settings.ui.SettingsActivityView;
-import org.citra.citra_emu.features.settings.utils.SettingsFile;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
-public class Settings {
-    public static final String PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch";
-    public static final String PREF_MATERIAL_YOU = "MaterialYouTheme";
-    public static final String PREF_THEME_MODE = "ThemeMode";
-    public static final String PREF_BLACK_BACKGROUNDS = "BlackBackgrounds";
-    public static final String PREF_SHOW_HOME_APPS = "ShowHomeApps";
-
-    public static final String SECTION_CORE = "Core";
-    public static final String SECTION_SYSTEM = "System";
-    public static final String SECTION_CAMERA = "Camera";
-    public static final String SECTION_CONTROLS = "Controls";
-    public static final String SECTION_RENDERER = "Renderer";
-    public static final String SECTION_LAYOUT = "Layout";
-    public static final String SECTION_UTILITY = "Utility";
-    public static final String SECTION_AUDIO = "Audio";
-    public static final String SECTION_DEBUG = "Debug";
-
-    private String gameId;
-
-    private static final Map<String, List<String>> configFileSectionsMap = new HashMap<>();
-
-    static {
-        configFileSectionsMap.put(SettingsFile.FILE_NAME_CONFIG, Arrays.asList(SECTION_CORE, SECTION_SYSTEM, SECTION_CAMERA, SECTION_CONTROLS, SECTION_RENDERER, SECTION_LAYOUT, SECTION_UTILITY, SECTION_AUDIO, SECTION_DEBUG));
-    }
-
-    /**
-     * A HashMap<String, SettingSection> that constructs a new SettingSection instead of returning null
-     * when getting a key not already in the map
-     */
-    public static final class SettingsSectionMap extends HashMap<String, SettingSection> {
-        @Override
-        public SettingSection get(Object key) {
-            if (!(key instanceof String)) {
-                return null;
-            }
-
-            String stringKey = (String) key;
-
-            if (!super.containsKey(stringKey)) {
-                SettingSection section = new SettingSection(stringKey);
-                super.put(stringKey, section);
-                return section;
-            }
-            return super.get(key);
-        }
-    }
-
-    private HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap();
-
-    public SettingSection getSection(String sectionName) {
-        return sections.get(sectionName);
-    }
-
-    public boolean isEmpty() {
-        return sections.isEmpty();
-    }
-
-    public HashMap<String, SettingSection> getSections() {
-        return sections;
-    }
-
-    public void loadSettings(SettingsActivityView view) {
-        sections = new Settings.SettingsSectionMap();
-        loadCitraSettings(view);
-
-        if (!TextUtils.isEmpty(gameId)) {
-            loadCustomGameSettings(gameId, view);
-        }
-    }
-
-    private void loadCitraSettings(SettingsActivityView view) {
-        for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) {
-            String fileName = entry.getKey();
-            sections.putAll(SettingsFile.readFile(fileName, view));
-        }
-    }
-
-    private void loadCustomGameSettings(String gameId, SettingsActivityView view) {
-        // custom game settings
-        mergeSections(SettingsFile.readCustomGameSettings(gameId, view));
-    }
-
-    private void mergeSections(HashMap<String, SettingSection> updatedSections) {
-        for (Map.Entry<String, SettingSection> entry : updatedSections.entrySet()) {
-            if (sections.containsKey(entry.getKey())) {
-                SettingSection originalSection = sections.get(entry.getKey());
-                SettingSection updatedSection = entry.getValue();
-                originalSection.mergeSection(updatedSection);
-            } else {
-                sections.put(entry.getKey(), entry.getValue());
-            }
-        }
-    }
-
-    public void loadSettings(String gameId, SettingsActivityView view) {
-        this.gameId = gameId;
-        loadSettings(view);
-    }
-
-    public void saveSettings(SettingsActivityView view) {
-        if (TextUtils.isEmpty(gameId)) {
-            view.showToastMessage(CitraApplication.Companion.getAppContext().getString(R.string.ini_saved), false);
-
-            for (Map.Entry<String, List<String>> entry : configFileSectionsMap.entrySet()) {
-                String fileName = entry.getKey();
-                List<String> sectionNames = entry.getValue();
-                TreeMap<String, SettingSection> iniSections = new TreeMap<>();
-                for (String section : sectionNames) {
-                    iniSections.put(section, sections.get(section));
-                }
-
-                SettingsFile.saveFile(fileName, iniSections, view);
-            }
-        }
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt
new file mode 100644
index 0000000000..926f669cc7
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/Settings.kt
@@ -0,0 +1,201 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model
+
+import android.text.TextUtils
+import org.citra.citra_emu.CitraApplication
+import org.citra.citra_emu.R
+import org.citra.citra_emu.features.settings.ui.SettingsActivityView
+import org.citra.citra_emu.features.settings.utils.SettingsFile
+import java.util.TreeMap
+
+class Settings {
+    private var gameId: String? = null
+
+    var isLoaded = false
+
+    /**
+     * A HashMap<String></String>, SettingSection> that constructs a new SettingSection instead of returning null
+     * when getting a key not already in the map
+     */
+    class SettingsSectionMap : HashMap<String, SettingSection?>() {
+        override operator fun get(key: String): SettingSection? {
+            if (!super.containsKey(key)) {
+                val section = SettingSection(key)
+                super.put(key, section)
+                return section
+            }
+            return super.get(key)
+        }
+    }
+
+    var sections: HashMap<String, SettingSection?> = SettingsSectionMap()
+
+    fun getSection(sectionName: String): SettingSection? {
+        return sections[sectionName]
+    }
+
+    val isEmpty: Boolean
+        get() = sections.isEmpty()
+
+    fun loadSettings(view: SettingsActivityView? = null) {
+        sections = SettingsSectionMap()
+        loadCitraSettings(view)
+        if (!TextUtils.isEmpty(gameId)) {
+            loadCustomGameSettings(gameId!!, view)
+        }
+        isLoaded = true
+    }
+
+    private fun loadCitraSettings(view: SettingsActivityView?) {
+        for ((fileName) in configFileSectionsMap) {
+            sections.putAll(SettingsFile.readFile(fileName, view))
+        }
+    }
+
+    private fun loadCustomGameSettings(gameId: String, view: SettingsActivityView?) {
+        // Custom game settings
+        mergeSections(SettingsFile.readCustomGameSettings(gameId, view))
+    }
+
+    private fun mergeSections(updatedSections: HashMap<String, SettingSection?>) {
+        for ((key, updatedSection) in updatedSections) {
+            if (sections.containsKey(key)) {
+                val originalSection = sections[key]
+                originalSection!!.mergeSection(updatedSection!!)
+            } else {
+                sections[key] = updatedSection
+            }
+        }
+    }
+
+    fun loadSettings(gameId: String, view: SettingsActivityView) {
+        this.gameId = gameId
+        loadSettings(view)
+    }
+
+    fun saveSettings(view: SettingsActivityView) {
+        if (TextUtils.isEmpty(gameId)) {
+            view.showToastMessage(
+                CitraApplication.appContext.getString(R.string.ini_saved),
+                false
+            )
+            for ((fileName, sectionNames) in configFileSectionsMap.entries) {
+                val iniSections = TreeMap<String, SettingSection?>()
+                for (section in sectionNames) {
+                    iniSections[section] = sections[section]
+                }
+                SettingsFile.saveFile(fileName, iniSections, view)
+            }
+        } else {
+            // TODO: Implement per game settings
+        }
+    }
+
+    companion object {
+        const val SECTION_CORE = "Core"
+        const val SECTION_SYSTEM = "System"
+        const val SECTION_CAMERA = "Camera"
+        const val SECTION_CONTROLS = "Controls"
+        const val SECTION_RENDERER = "Renderer"
+        const val SECTION_LAYOUT = "Layout"
+        const val SECTION_UTILITY = "Utility"
+        const val SECTION_AUDIO = "Audio"
+        const val SECTION_DEBUG = "Debugging"
+        const val SECTION_THEME = "Theme"
+
+        const val KEY_BUTTON_A = "button_a"
+        const val KEY_BUTTON_B = "button_b"
+        const val KEY_BUTTON_X = "button_x"
+        const val KEY_BUTTON_Y = "button_y"
+        const val KEY_BUTTON_SELECT = "button_select"
+        const val KEY_BUTTON_START = "button_start"
+        const val KEY_BUTTON_HOME = "button_home"
+        const val KEY_BUTTON_UP = "button_up"
+        const val KEY_BUTTON_DOWN = "button_down"
+        const val KEY_BUTTON_LEFT = "button_left"
+        const val KEY_BUTTON_RIGHT = "button_right"
+        const val KEY_BUTTON_L = "button_l"
+        const val KEY_BUTTON_R = "button_r"
+        const val KEY_BUTTON_ZL = "button_zl"
+        const val KEY_BUTTON_ZR = "button_zr"
+        const val KEY_CIRCLEPAD_AXIS_VERTICAL = "circlepad_axis_vertical"
+        const val KEY_CIRCLEPAD_AXIS_HORIZONTAL = "circlepad_axis_horizontal"
+        const val KEY_CSTICK_AXIS_VERTICAL = "cstick_axis_vertical"
+        const val KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal"
+        const val KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical"
+        const val KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal"
+
+        val buttonKeys = listOf(
+            KEY_BUTTON_A,
+            KEY_BUTTON_B,
+            KEY_BUTTON_X,
+            KEY_BUTTON_Y,
+            KEY_BUTTON_SELECT,
+            KEY_BUTTON_START,
+            KEY_BUTTON_HOME
+        )
+        val buttonTitles = listOf(
+            R.string.button_a,
+            R.string.button_b,
+            R.string.button_x,
+            R.string.button_y,
+            R.string.button_select,
+            R.string.button_start,
+            R.string.button_home
+        )
+        val circlePadKeys = listOf(
+            KEY_CIRCLEPAD_AXIS_VERTICAL,
+            KEY_CIRCLEPAD_AXIS_HORIZONTAL
+        )
+        val cStickKeys = listOf(
+            KEY_CSTICK_AXIS_VERTICAL,
+            KEY_CSTICK_AXIS_HORIZONTAL
+        )
+        val dPadKeys = listOf(
+            KEY_DPAD_AXIS_VERTICAL,
+            KEY_DPAD_AXIS_HORIZONTAL
+        )
+        val axisTitles = listOf(
+            R.string.controller_axis_vertical,
+            R.string.controller_axis_horizontal
+        )
+        val triggerKeys = listOf(
+            KEY_BUTTON_L,
+            KEY_BUTTON_R,
+            KEY_BUTTON_ZL,
+            KEY_BUTTON_ZR
+        )
+        val triggerTitles = listOf(
+            R.string.button_l,
+            R.string.button_r,
+            R.string.button_zl,
+            R.string.button_zr
+        )
+
+        const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
+        const val PREF_MATERIAL_YOU = "MaterialYouTheme"
+        const val PREF_THEME_MODE = "ThemeMode"
+        const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds"
+        const val PREF_SHOW_HOME_APPS = "ShowHomeApps"
+
+        private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap()
+
+        init {
+            configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] =
+                listOf(
+                    SECTION_CORE,
+                    SECTION_SYSTEM,
+                    SECTION_CAMERA,
+                    SECTION_CONTROLS,
+                    SECTION_RENDERER,
+                    SECTION_LAYOUT,
+                    SECTION_UTILITY,
+                    SECTION_AUDIO,
+                    SECTION_DEBUG
+                )
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt
new file mode 100644
index 0000000000..3f9b4ad1fc
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/SettingsViewModel.kt
@@ -0,0 +1,11 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model
+
+import androidx.lifecycle.ViewModel
+
+class SettingsViewModel : ViewModel() {
+    val settings = Settings()
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.java
deleted file mode 100644
index b906b70109..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.citra.citra_emu.features.settings.model;
-
-public final class StringSetting extends Setting {
-    private String mValue;
-
-    public StringSetting(String key, String section, String value) {
-        super(key, section);
-        mValue = value;
-    }
-
-    public String getValue() {
-        return mValue;
-    }
-
-    public void setValue(String value) {
-        mValue = value;
-    }
-
-    @Override
-    public String getValueAsString() {
-        return mValue;
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.kt
new file mode 100644
index 0000000000..87b425f5b6
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/StringSetting.kt
@@ -0,0 +1,50 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model
+
+enum class StringSetting(
+    override val key: String,
+    override val section: String,
+    override val defaultValue: String
+) : AbstractStringSetting {
+    INIT_TIME("init_time", Settings.SECTION_SYSTEM, "946731601"),
+    CAMERA_INNER_NAME("camera_inner_name", Settings.SECTION_CAMERA, "ndk"),
+    CAMERA_INNER_CONFIG("camera_inner_config", Settings.SECTION_CAMERA, "_front"),
+    CAMERA_OUTER_LEFT_NAME("camera_outer_left_name", Settings.SECTION_CAMERA, "ndk"),
+    CAMERA_OUTER_LEFT_CONFIG("camera_outer_left_config", Settings.SECTION_CAMERA, "_back"),
+    CAMERA_OUTER_RIGHT_NAME("camera_outer_right_name", Settings.SECTION_CAMERA, "ndk"),
+    CAMERA_OUTER_RIGHT_CONFIG("camera_outer_right_config", Settings.SECTION_CAMERA, "_back");
+
+    override var string: String = defaultValue
+
+    override val valueAsString: String
+        get() = string
+
+    override val isRuntimeEditable: Boolean
+        get() {
+            for (setting in NOT_RUNTIME_EDITABLE) {
+                if (setting == this) {
+                    return false
+                }
+            }
+            return true
+        }
+
+    companion object {
+        private val NOT_RUNTIME_EDITABLE = listOf(
+            INIT_TIME,
+            CAMERA_INNER_NAME,
+            CAMERA_INNER_CONFIG,
+            CAMERA_OUTER_LEFT_NAME,
+            CAMERA_OUTER_LEFT_CONFIG,
+            CAMERA_OUTER_RIGHT_NAME,
+            CAMERA_OUTER_RIGHT_CONFIG
+        )
+
+        fun from(key: String): StringSetting? = StringSetting.values().firstOrNull { it.key == key }
+
+        fun clear() = StringSetting.values().forEach { it.string = it.defaultValue }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/AbstractShortSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/AbstractShortSetting.kt
new file mode 100644
index 0000000000..865ebbdd0e
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/AbstractShortSetting.kt
@@ -0,0 +1,11 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model.view
+
+import org.citra.citra_emu.features.settings.model.AbstractSetting
+
+interface AbstractShortSetting : AbstractSetting {
+    var short: Short
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/CheckBoxSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/CheckBoxSetting.java
deleted file mode 100644
index 6bafecfe03..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/CheckBoxSetting.java
+++ /dev/null
@@ -1,80 +0,0 @@
-package org.citra.citra_emu.features.settings.model.view;
-
-import org.citra.citra_emu.CitraApplication;
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.settings.model.BooleanSetting;
-import org.citra.citra_emu.features.settings.model.IntSetting;
-import org.citra.citra_emu.features.settings.model.Setting;
-import org.citra.citra_emu.features.settings.ui.SettingsFragmentView;
-
-public final class CheckBoxSetting extends SettingsItem {
-    private boolean mDefaultValue;
-    private boolean mShowPerformanceWarning;
-    private SettingsFragmentView mView;
-
-    public CheckBoxSetting(String key, String section, int titleId, int descriptionId,
-                           boolean defaultValue, Setting setting) {
-        super(key, section, setting, titleId, descriptionId);
-        mDefaultValue = defaultValue;
-        mShowPerformanceWarning = false;
-    }
-
-    public CheckBoxSetting(String key, String section, int titleId, int descriptionId,
-                           boolean defaultValue, Setting setting, boolean show_performance_warning, SettingsFragmentView view) {
-        super(key, section, setting, titleId, descriptionId);
-        mDefaultValue = defaultValue;
-        mView = view;
-        mShowPerformanceWarning = show_performance_warning;
-    }
-
-    public boolean isChecked() {
-        if (getSetting() == null) {
-            return mDefaultValue;
-        }
-
-        // Try integer setting
-        try {
-            IntSetting setting = (IntSetting) getSetting();
-            return setting.getValue() == 1;
-        } catch (ClassCastException exception) {
-        }
-
-        // Try boolean setting
-        try {
-            BooleanSetting setting = (BooleanSetting) getSetting();
-            return setting.getValue() == true;
-        } catch (ClassCastException exception) {
-        }
-
-        return mDefaultValue;
-    }
-
-    /**
-     * Write a value to the backing boolean. If that boolean was previously null,
-     * initializes a new one and returns it, so it can be added to the Hashmap.
-     *
-     * @param checked Pretty self explanatory.
-     * @return null if overwritten successfully; otherwise, a newly created BooleanSetting.
-     */
-    public IntSetting setChecked(boolean checked) {
-        // Show a performance warning if the setting has been disabled
-        if (mShowPerformanceWarning && !checked) {
-            mView.showToastMessage(CitraApplication.Companion.getAppContext().getString(R.string.performance_warning), true);
-        }
-
-        if (getSetting() == null) {
-            IntSetting setting = new IntSetting(getKey(), getSection(), checked ? 1 : 0);
-            setSetting(setting);
-            return setting;
-        } else {
-            IntSetting setting = (IntSetting) getSetting();
-            setting.setValue(checked ? 1 : 0);
-            return null;
-        }
-    }
-
-    @Override
-    public int getType() {
-        return TYPE_CHECKBOX;
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.java
deleted file mode 100644
index afc3352cc0..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package org.citra.citra_emu.features.settings.model.view;
-
-import org.citra.citra_emu.features.settings.model.Setting;
-import org.citra.citra_emu.features.settings.model.StringSetting;
-
-public final class DateTimeSetting extends SettingsItem {
-    private String mDefaultValue;
-
-    public DateTimeSetting(String key, String section, int titleId, int descriptionId,
-                           String defaultValue, Setting setting) {
-        super(key, section, setting, titleId, descriptionId);
-        mDefaultValue = defaultValue;
-    }
-
-    public String getValue() {
-        if (getSetting() != null) {
-            StringSetting setting = (StringSetting) getSetting();
-            return setting.getValue();
-        } else {
-            return mDefaultValue;
-        }
-    }
-
-    public StringSetting setSelectedValue(String datetime) {
-        if (getSetting() == null) {
-            StringSetting setting = new StringSetting(getKey(), getSection(), datetime);
-            setSetting(setting);
-            return setting;
-        } else {
-            StringSetting setting = (StringSetting) getSetting();
-            setting.setValue(datetime);
-            return null;
-        }
-    }
-
-    @Override
-    public int getType() {
-        return TYPE_DATETIME_SETTING;
-    }
-}
\ No newline at end of file
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.kt
new file mode 100644
index 0000000000..6332e0e310
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/DateTimeSetting.kt
@@ -0,0 +1,32 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model.view
+
+import org.citra.citra_emu.features.settings.model.AbstractSetting
+import org.citra.citra_emu.features.settings.model.AbstractStringSetting
+
+class DateTimeSetting(
+    setting: AbstractSetting?,
+    titleId: Int,
+    descriptionId: Int,
+    val key: String? = null,
+    private val defaultValue: String? = null
+) : SettingsItem(setting, titleId, descriptionId) {
+    override val type = TYPE_DATETIME_SETTING
+
+    val value: String
+        get() = if (setting != null) {
+            val setting = setting as AbstractStringSetting
+            setting.string
+        } else {
+            defaultValue!!
+        }
+
+    fun setSelectedValue(datetime: String): AbstractStringSetting {
+        val stringSetting = setting as AbstractStringSetting
+        stringSetting.string = datetime
+        return stringSetting
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.java
deleted file mode 100644
index bac8876cdf..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.citra.citra_emu.features.settings.model.view;
-
-import org.citra.citra_emu.features.settings.model.Setting;
-
-public final class HeaderSetting extends SettingsItem {
-    public HeaderSetting(String key, Setting setting, int titleId, int descriptionId) {
-        super(key, null, setting, titleId, descriptionId);
-    }
-
-    @Override
-    public int getType() {
-        return SettingsItem.TYPE_HEADER;
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.kt
new file mode 100644
index 0000000000..e99b842f9f
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/HeaderSetting.kt
@@ -0,0 +1,9 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model.view
+
+class HeaderSetting(titleId: Int) : SettingsItem(null, titleId, 0) {
+    override val type = TYPE_HEADER
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.java
deleted file mode 100644
index 6d4d954e8c..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.java
+++ /dev/null
@@ -1,382 +0,0 @@
-package org.citra.citra_emu.features.settings.model.view;
-
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
-import android.view.InputDevice;
-import android.view.KeyEvent;
-import android.widget.Toast;
-
-import org.citra.citra_emu.CitraApplication;
-import org.citra.citra_emu.NativeLibrary;
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.settings.model.Setting;
-import org.citra.citra_emu.features.settings.model.StringSetting;
-import org.citra.citra_emu.features.settings.utils.SettingsFile;
-
-public final class InputBindingSetting extends SettingsItem {
-    private static final String INPUT_MAPPING_PREFIX = "InputMapping";
-
-    public InputBindingSetting(String key, String section, int titleId, Setting setting) {
-        super(key, section, setting, titleId, 0);
-    }
-
-    public String getValue() {
-        if (getSetting() == null) {
-            return "";
-        }
-
-        StringSetting setting = (StringSetting) getSetting();
-        return setting.getValue();
-    }
-
-    /**
-     * Returns true if this key is for the 3DS Circle Pad
-     */
-    private boolean IsCirclePad() {
-        switch (getKey()) {
-            case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL:
-            case SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL:
-                return true;
-        }
-        return false;
-    }
-
-    /**
-     * Returns true if this key is for a horizontal axis for a 3DS analog stick or D-pad
-     */
-    public boolean IsHorizontalOrientation() {
-        switch (getKey()) {
-            case SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL:
-            case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL:
-            case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL:
-                return true;
-        }
-        return false;
-    }
-
-    /**
-     * Returns true if this key is for the 3DS C-Stick
-     */
-    private boolean IsCStick() {
-        switch (getKey()) {
-            case SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL:
-            case SettingsFile.KEY_CSTICK_AXIS_VERTICAL:
-                return true;
-        }
-        return false;
-    }
-
-    /**
-     * Returns true if this key is for the 3DS D-Pad
-     */
-    private boolean IsDPad() {
-        switch (getKey()) {
-            case SettingsFile.KEY_DPAD_AXIS_HORIZONTAL:
-            case SettingsFile.KEY_DPAD_AXIS_VERTICAL:
-                return true;
-        }
-        return false;
-    }
-
-    /**
-     * Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real
-     * triggers on the 3DS, but we support them as such on a physical gamepad.
-     */
-    public boolean IsTrigger() {
-        switch (getKey()) {
-            case SettingsFile.KEY_BUTTON_L:
-            case SettingsFile.KEY_BUTTON_R:
-            case SettingsFile.KEY_BUTTON_ZL:
-            case SettingsFile.KEY_BUTTON_ZR:
-                return true;
-        }
-        return false;
-    }
-
-    /**
-     * Returns true if a gamepad axis can be used to map this key.
-     */
-    public boolean IsAxisMappingSupported() {
-        return IsCirclePad() || IsCStick() || IsDPad() || IsTrigger();
-    }
-
-    /**
-     * Returns true if a gamepad button can be used to map this key.
-     */
-    private boolean IsButtonMappingSupported() {
-        return !IsAxisMappingSupported() || IsTrigger();
-    }
-
-    /**
-     * Returns the Citra button code for the settings key.
-     */
-    private int getButtonCode() {
-        switch (getKey()) {
-            case SettingsFile.KEY_BUTTON_A:
-                return NativeLibrary.ButtonType.BUTTON_A;
-            case SettingsFile.KEY_BUTTON_B:
-                return NativeLibrary.ButtonType.BUTTON_B;
-            case SettingsFile.KEY_BUTTON_X:
-                return NativeLibrary.ButtonType.BUTTON_X;
-            case SettingsFile.KEY_BUTTON_Y:
-                return NativeLibrary.ButtonType.BUTTON_Y;
-            case SettingsFile.KEY_BUTTON_L:
-                return NativeLibrary.ButtonType.TRIGGER_L;
-            case SettingsFile.KEY_BUTTON_R:
-                return NativeLibrary.ButtonType.TRIGGER_R;
-            case SettingsFile.KEY_BUTTON_ZL:
-                return NativeLibrary.ButtonType.BUTTON_ZL;
-            case SettingsFile.KEY_BUTTON_ZR:
-                return NativeLibrary.ButtonType.BUTTON_ZR;
-            case SettingsFile.KEY_BUTTON_SELECT:
-                return NativeLibrary.ButtonType.BUTTON_SELECT;
-            case SettingsFile.KEY_BUTTON_START:
-                return NativeLibrary.ButtonType.BUTTON_START;
-            case SettingsFile.KEY_BUTTON_UP:
-                return NativeLibrary.ButtonType.DPAD_UP;
-            case SettingsFile.KEY_BUTTON_DOWN:
-                return NativeLibrary.ButtonType.DPAD_DOWN;
-            case SettingsFile.KEY_BUTTON_LEFT:
-                return NativeLibrary.ButtonType.DPAD_LEFT;
-            case SettingsFile.KEY_BUTTON_RIGHT:
-                return NativeLibrary.ButtonType.DPAD_RIGHT;
-        }
-        return -1;
-    }
-
-    /**
-     * Returns the settings key for the specified Citra button code.
-     */
-    private static String getButtonKey(int buttonCode) {
-        switch (buttonCode) {
-            case NativeLibrary.ButtonType.BUTTON_A:
-                return SettingsFile.KEY_BUTTON_A;
-            case NativeLibrary.ButtonType.BUTTON_B:
-                return SettingsFile.KEY_BUTTON_B;
-            case NativeLibrary.ButtonType.BUTTON_X:
-                return SettingsFile.KEY_BUTTON_X;
-            case NativeLibrary.ButtonType.BUTTON_Y:
-                return SettingsFile.KEY_BUTTON_Y;
-            case NativeLibrary.ButtonType.TRIGGER_L:
-                return SettingsFile.KEY_BUTTON_L;
-            case NativeLibrary.ButtonType.TRIGGER_R:
-                return SettingsFile.KEY_BUTTON_R;
-            case NativeLibrary.ButtonType.BUTTON_ZL:
-                return SettingsFile.KEY_BUTTON_ZL;
-            case NativeLibrary.ButtonType.BUTTON_ZR:
-                return SettingsFile.KEY_BUTTON_ZR;
-            case NativeLibrary.ButtonType.BUTTON_SELECT:
-                return SettingsFile.KEY_BUTTON_SELECT;
-            case NativeLibrary.ButtonType.BUTTON_START:
-                return SettingsFile.KEY_BUTTON_START;
-            case NativeLibrary.ButtonType.DPAD_UP:
-                return SettingsFile.KEY_BUTTON_UP;
-            case NativeLibrary.ButtonType.DPAD_DOWN:
-                return SettingsFile.KEY_BUTTON_DOWN;
-            case NativeLibrary.ButtonType.DPAD_LEFT:
-                return SettingsFile.KEY_BUTTON_LEFT;
-            case NativeLibrary.ButtonType.DPAD_RIGHT:
-                return SettingsFile.KEY_BUTTON_RIGHT;
-        }
-        return "";
-    }
-
-    /**
-     * Returns the key used to lookup the reverse mapping for this key, which is used to cleanup old
-     * settings on re-mapping or clearing of a setting.
-     */
-    private String getReverseKey() {
-        String reverseKey = INPUT_MAPPING_PREFIX + "_ReverseMapping_" + getKey();
-
-        if (IsAxisMappingSupported() && !IsTrigger()) {
-            // Triggers are the only axis-supported mappings without orientation
-            reverseKey += "_" + (IsHorizontalOrientation() ? 0 : 1);
-        }
-
-        return reverseKey;
-    }
-
-    /**
-     * Removes the old mapping for this key from the settings, e.g. on user clearing the setting.
-     */
-    public void removeOldMapping() {
-        // Get preferences editor
-        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext());
-        SharedPreferences.Editor editor = preferences.edit();
-
-        // Try remove all possible keys we wrote for this setting
-        String oldKey = preferences.getString(getReverseKey(), "");
-        if (!oldKey.equals("")) {
-            editor.remove(getKey()); // Used for ui text
-            editor.remove(oldKey); // Used for button mapping
-            editor.remove(oldKey + "_GuestOrientation"); // Used for axis orientation
-            editor.remove(oldKey + "_GuestButton"); // Used for axis button
-        }
-
-        // Apply changes
-        editor.apply();
-    }
-
-    /**
-     * Helper function to get the settings key for an gamepad button.
-     */
-    public static String getInputButtonKey(int keyCode) {
-        return INPUT_MAPPING_PREFIX + "_Button_" + keyCode;
-    }
-
-    /**
-     * Helper function to get the settings key for an gamepad axis.
-     */
-    public static String getInputAxisKey(int axis) {
-        return INPUT_MAPPING_PREFIX + "_HostAxis_" + axis;
-    }
-
-    /**
-     * Helper function to get the settings key for an gamepad axis button (stick or trigger).
-     */
-    public static String getInputAxisButtonKey(int axis) {
-        return getInputAxisKey(axis) + "_GuestButton";
-    }
-
-    /**
-     * Helper function to get the settings key for an gamepad axis orientation.
-     */
-    public static String getInputAxisOrientationKey(int axis) {
-        return getInputAxisKey(axis) + "_GuestOrientation";
-    }
-
-    /**
-     * Helper function to write a gamepad button mapping for the setting.
-     */
-    private void WriteButtonMapping(String key) {
-        // Get preferences editor
-        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext());
-        SharedPreferences.Editor editor = preferences.edit();
-
-        // Remove mapping for another setting using this input
-        int oldButtonCode = preferences.getInt(key, -1);
-        if (oldButtonCode != -1) {
-            String oldKey = getButtonKey(oldButtonCode);
-            editor.remove(oldKey); // Only need to remove UI text setting, others will be overwritten
-        }
-
-        // Cleanup old mapping for this setting
-        removeOldMapping();
-
-        // Write new mapping
-        editor.putInt(key, getButtonCode());
-
-        // Write next reverse mapping for future cleanup
-        editor.putString(getReverseKey(), key);
-
-        // Apply changes
-        editor.apply();
-    }
-
-    /**
-     * Helper function to write a gamepad axis mapping for the setting.
-     */
-    private void WriteAxisMapping(int axis, int value) {
-        // Get preferences editor
-        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext());
-        SharedPreferences.Editor editor = preferences.edit();
-
-        // Cleanup old mapping
-        removeOldMapping();
-
-        // Write new mapping
-        editor.putInt(getInputAxisOrientationKey(axis), IsHorizontalOrientation() ? 0 : 1);
-        editor.putInt(getInputAxisButtonKey(axis), value);
-
-        // Write next reverse mapping for future cleanup
-        editor.putString(getReverseKey(), getInputAxisKey(axis));
-
-        // Apply changes
-        editor.apply();
-    }
-
-    /**
-     * Saves the provided key input setting as an Android preference.
-     *
-     * @param keyEvent KeyEvent of this key press.
-     */
-    public void onKeyInput(KeyEvent keyEvent) {
-        if (!IsButtonMappingSupported()) {
-            Toast.makeText(CitraApplication.Companion.getAppContext(), R.string.input_message_analog_only, Toast.LENGTH_LONG).show();
-            return;
-        }
-
-        InputDevice device = keyEvent.getDevice();
-
-        WriteButtonMapping(getInputButtonKey(keyEvent.getKeyCode()));
-
-        String uiString = device.getName() + ": Button " + keyEvent.getKeyCode();
-        setUiString(uiString);
-    }
-
-    /**
-     * Saves the provided motion input setting as an Android preference.
-     *
-     * @param device      InputDevice from which the input event originated.
-     * @param motionRange MotionRange of the movement
-     * @param axisDir     Either '-' or '+' (currently unused)
-     */
-    public void onMotionInput(InputDevice device, InputDevice.MotionRange motionRange,
-                              char axisDir) {
-        if (!IsAxisMappingSupported()) {
-            Toast.makeText(CitraApplication.Companion.getAppContext(), R.string.input_message_button_only, Toast.LENGTH_LONG).show();
-            return;
-        }
-
-        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext());
-        SharedPreferences.Editor editor = preferences.edit();
-
-        int button;
-        if (IsCirclePad()) {
-            button = NativeLibrary.ButtonType.STICK_LEFT;
-        } else if (IsCStick()) {
-            button = NativeLibrary.ButtonType.STICK_C;
-        } else if (IsDPad()) {
-            button = NativeLibrary.ButtonType.DPAD;
-        } else {
-            button = getButtonCode();
-        }
-
-        WriteAxisMapping(motionRange.getAxis(), button);
-
-        String uiString = device.getName() + ": Axis " + motionRange.getAxis();
-        setUiString(uiString);
-
-        editor.apply();
-    }
-
-    /**
-     * Sets the string to use in the configuration UI for the gamepad input.
-     */
-    private StringSetting setUiString(String ui) {
-        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.Companion.getAppContext());
-        SharedPreferences.Editor editor = preferences.edit();
-
-        if (getSetting() == null) {
-            StringSetting setting = new StringSetting(getKey(), getSection(), "");
-            setSetting(setting);
-
-            editor.putString(setting.getKey(), ui);
-            editor.apply();
-
-            return setting;
-        } else {
-            StringSetting setting = (StringSetting) getSetting();
-
-            editor.putString(setting.getKey(), ui);
-            editor.apply();
-
-            return null;
-        }
-    }
-
-    @Override
-    public int getType() {
-        return TYPE_INPUT_BINDING;
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt
new file mode 100644
index 0000000000..a6ae5e31c0
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/InputBindingSetting.kt
@@ -0,0 +1,299 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model.view
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.preference.PreferenceManager
+import android.view.InputDevice
+import android.view.InputDevice.MotionRange
+import android.view.KeyEvent
+import android.widget.Toast
+import org.citra.citra_emu.CitraApplication
+import org.citra.citra_emu.NativeLibrary
+import org.citra.citra_emu.R
+import org.citra.citra_emu.features.settings.model.AbstractSetting
+import org.citra.citra_emu.features.settings.model.Settings
+
+class InputBindingSetting(
+    val abstractSetting: AbstractSetting,
+    titleId: Int
+) : SettingsItem(abstractSetting, titleId, 0) {
+    private val context: Context get() = CitraApplication.appContext
+    private val preferences: SharedPreferences
+        get() = PreferenceManager.getDefaultSharedPreferences(context)
+
+    var value: String
+        get() = preferences.getString(abstractSetting.key, "")!!
+        set(string) {
+            preferences.edit()
+                .putString(abstractSetting.key, string)
+                .apply()
+        }
+
+    /**
+     * Returns true if this key is for the 3DS Circle Pad
+     */
+    fun isCirclePad(): Boolean =
+        when (abstractSetting.key) {
+            Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL,
+            Settings.KEY_CIRCLEPAD_AXIS_VERTICAL -> true
+
+            else -> false
+        }
+
+    /**
+     * Returns true if this key is for a horizontal axis for a 3DS analog stick or D-pad
+     */
+    fun isHorizontalOrientation(): Boolean =
+        when (abstractSetting.key) {
+            Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL,
+            Settings.KEY_CSTICK_AXIS_HORIZONTAL,
+            Settings.KEY_DPAD_AXIS_HORIZONTAL -> true
+
+            else -> false
+        }
+
+    /**
+     * Returns true if this key is for the 3DS C-Stick
+     */
+    fun isCStick(): Boolean =
+        when (abstractSetting.key) {
+            Settings.KEY_CSTICK_AXIS_HORIZONTAL,
+            Settings.KEY_CSTICK_AXIS_VERTICAL -> true
+
+            else -> false
+        }
+
+    /**
+     * Returns true if this key is for the 3DS D-Pad
+     */
+    fun isDPad(): Boolean =
+        when (abstractSetting.key) {
+            Settings.KEY_DPAD_AXIS_HORIZONTAL,
+            Settings.KEY_DPAD_AXIS_VERTICAL -> true
+
+            else -> false
+        }
+
+    /**
+     * Returns true if this key is for the 3DS L/R or ZL/ZR buttons. Note, these are not real
+     * triggers on the 3DS, but we support them as such on a physical gamepad.
+     */
+    fun isTrigger(): Boolean =
+        when (abstractSetting.key) {
+            Settings.KEY_BUTTON_L,
+            Settings.KEY_BUTTON_R,
+            Settings.KEY_BUTTON_ZL,
+            Settings.KEY_BUTTON_ZR -> true
+
+            else -> false
+        }
+
+    /**
+     * Returns true if a gamepad axis can be used to map this key.
+     */
+    fun isAxisMappingSupported(): Boolean {
+        return isCirclePad() || isCStick() || isDPad() || isTrigger()
+    }
+
+    /**
+     * Returns true if a gamepad button can be used to map this key.
+     */
+    fun isButtonMappingSupported(): Boolean {
+        return !isAxisMappingSupported() || isTrigger()
+    }
+
+    /**
+     * Returns the Citra button code for the settings key.
+     */
+    private val buttonCode: Int
+        get() =
+            when (abstractSetting.key) {
+                Settings.KEY_BUTTON_A -> NativeLibrary.ButtonType.BUTTON_A
+                Settings.KEY_BUTTON_B -> NativeLibrary.ButtonType.BUTTON_B
+                Settings.KEY_BUTTON_X -> NativeLibrary.ButtonType.BUTTON_X
+                Settings.KEY_BUTTON_Y -> NativeLibrary.ButtonType.BUTTON_Y
+                Settings.KEY_BUTTON_L -> NativeLibrary.ButtonType.TRIGGER_L
+                Settings.KEY_BUTTON_R -> NativeLibrary.ButtonType.TRIGGER_R
+                Settings.KEY_BUTTON_ZL -> NativeLibrary.ButtonType.BUTTON_ZL
+                Settings.KEY_BUTTON_ZR -> NativeLibrary.ButtonType.BUTTON_ZR
+                Settings.KEY_BUTTON_SELECT -> NativeLibrary.ButtonType.BUTTON_SELECT
+                Settings.KEY_BUTTON_START -> NativeLibrary.ButtonType.BUTTON_START
+                Settings.KEY_BUTTON_HOME -> NativeLibrary.ButtonType.BUTTON_HOME
+                Settings.KEY_BUTTON_UP -> NativeLibrary.ButtonType.DPAD_UP
+                Settings.KEY_BUTTON_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
+                Settings.KEY_BUTTON_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
+                Settings.KEY_BUTTON_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
+                else -> -1
+            }
+
+    /**
+     * Returns the key used to lookup the reverse mapping for this key, which is used to cleanup old
+     * settings on re-mapping or clearing of a setting.
+     */
+    private val reverseKey: String
+        get() {
+            var reverseKey = "${INPUT_MAPPING_PREFIX}_ReverseMapping_${abstractSetting.key}"
+            if (isAxisMappingSupported() && !isTrigger()) {
+                // Triggers are the only axis-supported mappings without orientation
+                reverseKey += "_" + if (isHorizontalOrientation()) {
+                    0
+                } else {
+                    1
+                }
+            }
+            return reverseKey
+        }
+
+    /**
+     * Removes the old mapping for this key from the settings, e.g. on user clearing the setting.
+     */
+    fun removeOldMapping() {
+        // Try remove all possible keys we wrote for this setting
+        val oldKey = preferences.getString(reverseKey, "")
+        if (oldKey != "") {
+            preferences.edit()
+                .remove(abstractSetting.key) // Used for ui text
+                .remove(oldKey) // Used for button mapping
+                .remove(oldKey + "_GuestOrientation") // Used for axis orientation
+                .remove(oldKey + "_GuestButton") // Used for axis button
+                .apply()
+        }
+    }
+
+    /**
+     * Helper function to write a gamepad button mapping for the setting.
+     */
+    private fun writeButtonMapping(key: String) {
+        val editor = preferences.edit()
+
+        // Remove mapping for another setting using this input
+        val oldButtonCode = preferences.getInt(key, -1)
+        if (oldButtonCode != -1) {
+            val oldKey = getButtonKey(oldButtonCode)
+            editor.remove(oldKey) // Only need to remove UI text setting, others will be overwritten
+        }
+
+        // Cleanup old mapping for this setting
+        removeOldMapping()
+
+        // Write new mapping
+        editor.putInt(key, buttonCode)
+
+        // Write next reverse mapping for future cleanup
+        editor.putString(reverseKey, key)
+
+        // Apply changes
+        editor.apply()
+    }
+
+    /**
+     * Helper function to write a gamepad axis mapping for the setting.
+     */
+    private fun writeAxisMapping(axis: Int, value: Int) {
+        // Cleanup old mapping
+        removeOldMapping()
+
+        // Write new mapping
+        preferences.edit()
+            .putInt(getInputAxisOrientationKey(axis), if (isHorizontalOrientation()) 0 else 1)
+            .putInt(getInputAxisButtonKey(axis), value)
+            // Write next reverse mapping for future cleanup
+            .putString(reverseKey, getInputAxisKey(axis))
+            .apply()
+    }
+
+    /**
+     * Saves the provided key input setting as an Android preference.
+     *
+     * @param keyEvent KeyEvent of this key press.
+     */
+    fun onKeyInput(keyEvent: KeyEvent) {
+        if (!isButtonMappingSupported()) {
+            Toast.makeText(context, R.string.input_message_analog_only, Toast.LENGTH_LONG).show()
+            return
+        }
+        writeButtonMapping(getInputButtonKey(keyEvent.keyCode))
+        val uiString = "${keyEvent.device.name}: Button ${keyEvent.keyCode}"
+        value = uiString
+    }
+
+    /**
+     * Saves the provided motion input setting as an Android preference.
+     *
+     * @param device      InputDevice from which the input event originated.
+     * @param motionRange MotionRange of the movement
+     * @param axisDir     Either '-' or '+' (currently unused)
+     */
+    fun onMotionInput(device: InputDevice, motionRange: MotionRange, axisDir: Char) {
+        if (!isAxisMappingSupported()) {
+            Toast.makeText(context, R.string.input_message_button_only, Toast.LENGTH_LONG).show()
+            return
+        }
+        val button = if (isCirclePad()) {
+            NativeLibrary.ButtonType.STICK_LEFT
+        } else if (isCStick()) {
+            NativeLibrary.ButtonType.STICK_C
+        } else if (isDPad()) {
+            NativeLibrary.ButtonType.DPAD
+        } else {
+            buttonCode
+        }
+        writeAxisMapping(motionRange.axis, button)
+        val uiString = "${device.name}: Axis ${motionRange.axis}"
+        value = uiString
+    }
+
+    override val type = TYPE_INPUT_BINDING
+
+    companion object {
+        private const val INPUT_MAPPING_PREFIX = "InputMapping"
+
+        /**
+         * Returns the settings key for the specified Citra button code.
+         */
+        private fun getButtonKey(buttonCode: Int): String =
+            when (buttonCode) {
+                NativeLibrary.ButtonType.BUTTON_A -> Settings.KEY_BUTTON_A
+                NativeLibrary.ButtonType.BUTTON_B -> Settings.KEY_BUTTON_B
+                NativeLibrary.ButtonType.BUTTON_X -> Settings.KEY_BUTTON_X
+                NativeLibrary.ButtonType.BUTTON_Y -> Settings.KEY_BUTTON_Y
+                NativeLibrary.ButtonType.TRIGGER_L -> Settings.KEY_BUTTON_L
+                NativeLibrary.ButtonType.TRIGGER_R -> Settings.KEY_BUTTON_R
+                NativeLibrary.ButtonType.BUTTON_ZL -> Settings.KEY_BUTTON_ZL
+                NativeLibrary.ButtonType.BUTTON_ZR -> Settings.KEY_BUTTON_ZR
+                NativeLibrary.ButtonType.BUTTON_SELECT -> Settings.KEY_BUTTON_SELECT
+                NativeLibrary.ButtonType.BUTTON_START -> Settings.KEY_BUTTON_START
+                NativeLibrary.ButtonType.BUTTON_HOME -> Settings.KEY_BUTTON_HOME
+                NativeLibrary.ButtonType.DPAD_UP -> Settings.KEY_BUTTON_UP
+                NativeLibrary.ButtonType.DPAD_DOWN -> Settings.KEY_BUTTON_DOWN
+                NativeLibrary.ButtonType.DPAD_LEFT -> Settings.KEY_BUTTON_LEFT
+                NativeLibrary.ButtonType.DPAD_RIGHT -> Settings.KEY_BUTTON_RIGHT
+                else -> ""
+            }
+
+        /**
+         * Helper function to get the settings key for an gamepad button.
+         */
+        fun getInputButtonKey(keyCode: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyCode}"
+
+        /**
+         * Helper function to get the settings key for an gamepad axis.
+         */
+        fun getInputAxisKey(axis: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${axis}"
+
+        /**
+         * Helper function to get the settings key for an gamepad axis button (stick or trigger).
+         */
+        fun getInputAxisButtonKey(axis: Int): String = "${getInputAxisKey(axis)}_GuestButton"
+
+        /**
+         * Helper function to get the settings key for an gamepad axis orientation.
+         */
+        fun getInputAxisOrientationKey(axis: Int): String =
+            "${getInputAxisKey(axis)}_GuestOrientation"
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt
new file mode 100644
index 0000000000..8a237a14e9
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/RunnableSetting.kt
@@ -0,0 +1,15 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model.view
+
+class RunnableSetting(
+    titleId: Int,
+    descriptionId: Int,
+    val isRuntimeRunnable: Boolean,
+    val runnable: () -> Unit,
+    val value: (() -> String)? = null
+) : SettingsItem(null, titleId, descriptionId) {
+    override val type = TYPE_RUNNABLE
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.java
deleted file mode 100644
index 8a56426965..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.java
+++ /dev/null
@@ -1,100 +0,0 @@
-package org.citra.citra_emu.features.settings.model.view;
-
-import org.citra.citra_emu.features.settings.model.Setting;
-import org.citra.citra_emu.features.settings.model.Settings;
-import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
-
-/**
- * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
- * Each one corresponds to a {@link Setting} object, so this class's subclasses
- * should vaguely correspond to those subclasses. There are a few with multiple analogues
- * and a few with none (Headers, for example, do not correspond to anything in the ini
- * file.)
- */
-public abstract class SettingsItem {
-    public static final int TYPE_HEADER = 0;
-    public static final int TYPE_CHECKBOX = 1;
-    public static final int TYPE_SINGLE_CHOICE = 2;
-    public static final int TYPE_SLIDER = 3;
-    public static final int TYPE_SUBMENU = 4;
-    public static final int TYPE_INPUT_BINDING = 5;
-    public static final int TYPE_STRING_SINGLE_CHOICE = 6;
-    public static final int TYPE_DATETIME_SETTING = 7;
-
-    private String mKey;
-    private String mSection;
-
-    private Setting mSetting;
-
-    private int mNameId;
-    private int mDescriptionId;
-
-    /**
-     * Base constructor. Takes a key / section name in case the third parameter, the Setting,
-     * is null; in which case, one can be constructed and saved using the key / section.
-     *
-     * @param key           Identifier for the Setting represented by this Item.
-     * @param section       Section to which the Setting belongs.
-     * @param setting       A possibly-null backing Setting, to be modified on UI events.
-     * @param nameId        Resource ID for a text string to be displayed as this setting's name.
-     * @param descriptionId Resource ID for a text string to be displayed as this setting's description.
-     */
-    public SettingsItem(String key, String section, Setting setting, int nameId,
-                        int descriptionId) {
-        mKey = key;
-        mSection = section;
-        mSetting = setting;
-        mNameId = nameId;
-        mDescriptionId = descriptionId;
-    }
-
-    /**
-     * @return The identifier for the backing Setting.
-     */
-    public String getKey() {
-        return mKey;
-    }
-
-    /**
-     * @return The header under which the backing Setting belongs.
-     */
-    public String getSection() {
-        return mSection;
-    }
-
-    /**
-     * @return The backing Setting, possibly null.
-     */
-    public Setting getSetting() {
-        return mSetting;
-    }
-
-    /**
-     * Replace the backing setting with a new one. Generally used in cases where
-     * the backing setting is null.
-     *
-     * @param setting A non-null Setting.
-     */
-    public void setSetting(Setting setting) {
-        mSetting = setting;
-    }
-
-    /**
-     * @return A resource ID for a text string representing this Setting's name.
-     */
-    public int getNameId() {
-        return mNameId;
-    }
-
-    public int getDescriptionId() {
-        return mDescriptionId;
-    }
-
-    /**
-     * Used by {@link SettingsAdapter}'s onCreateViewHolder()
-     * method to determine which type of ViewHolder should be created.
-     *
-     * @return An integer (ideally, one of the constants defined in this file)
-     */
-    public abstract int getType();
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt
new file mode 100644
index 0000000000..5f6b4b70ce
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SettingsItem.kt
@@ -0,0 +1,42 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model.view
+
+import org.citra.citra_emu.NativeLibrary
+import org.citra.citra_emu.features.settings.model.AbstractSetting
+
+/**
+ * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
+ * Each one corresponds to a [AbstractSetting] object, so this class's subclasses
+ * should vaguely correspond to those subclasses. There are a few with multiple analogues
+ * and a few with none (Headers, for example, do not correspond to anything in the ini
+ * file.)
+ */
+abstract class SettingsItem(
+    var setting: AbstractSetting?,
+    val nameId: Int,
+    val descriptionId: Int
+) {
+    abstract val type: Int
+
+    val isEditable: Boolean
+        get() {
+            if (!NativeLibrary.isRunning()) return true
+            return setting?.isRuntimeEditable ?: false
+        }
+
+    companion object {
+        const val TYPE_HEADER = 0
+        const val TYPE_SWITCH = 1
+        const val TYPE_SINGLE_CHOICE = 2
+        const val TYPE_SLIDER = 3
+        const val TYPE_SUBMENU = 4
+        const val TYPE_STRING_SINGLE_CHOICE = 5
+        const val TYPE_DATETIME_SETTING = 6
+        const val TYPE_RUNNABLE = 7
+        const val TYPE_INPUT_BINDING = 8
+        const val TYPE_STRING_INPUT = 9
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.java
deleted file mode 100644
index ee9d225d60..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package org.citra.citra_emu.features.settings.model.view;
-
-import org.citra.citra_emu.features.settings.model.IntSetting;
-import org.citra.citra_emu.features.settings.model.Setting;
-
-public final class SingleChoiceSetting extends SettingsItem {
-    private int mDefaultValue;
-
-    private int mChoicesId;
-    private int mValuesId;
-
-    public SingleChoiceSetting(String key, String section, int titleId, int descriptionId,
-                               int choicesId, int valuesId, int defaultValue, Setting setting) {
-        super(key, section, setting, titleId, descriptionId);
-        mValuesId = valuesId;
-        mChoicesId = choicesId;
-        mDefaultValue = defaultValue;
-    }
-
-    public int getChoicesId() {
-        return mChoicesId;
-    }
-
-    public int getValuesId() {
-        return mValuesId;
-    }
-
-    public int getSelectedValue() {
-        if (getSetting() != null) {
-            IntSetting setting = (IntSetting) getSetting();
-            return setting.getValue();
-        } else {
-            return mDefaultValue;
-        }
-    }
-
-    /**
-     * Write a value to the backing int. If that int was previously null,
-     * initializes a new one and returns it, so it can be added to the Hashmap.
-     *
-     * @param selection New value of the int.
-     * @return null if overwritten successfully otherwise; a newly created IntSetting.
-     */
-    public IntSetting setSelectedValue(int selection) {
-        if (getSetting() == null) {
-            IntSetting setting = new IntSetting(getKey(), getSection(), selection);
-            setSetting(setting);
-            return setting;
-        } else {
-            IntSetting setting = (IntSetting) getSetting();
-            setting.setValue(selection);
-            return null;
-        }
-    }
-
-    @Override
-    public int getType() {
-        return TYPE_SINGLE_CHOICE;
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.kt
new file mode 100644
index 0000000000..75c6ec3317
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SingleChoiceSetting.kt
@@ -0,0 +1,60 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model.view
+
+import org.citra.citra_emu.features.settings.model.AbstractIntSetting
+import org.citra.citra_emu.features.settings.model.AbstractSetting
+
+class SingleChoiceSetting(
+    setting: AbstractSetting?,
+    titleId: Int,
+    descriptionId: Int,
+    val choicesId: Int,
+    val valuesId: Int,
+    val key: String? = null,
+    val defaultValue: Int? = null
+) : SettingsItem(setting, titleId, descriptionId) {
+    override val type = TYPE_SINGLE_CHOICE
+
+    val selectedValue: Int
+        get() {
+            if (setting == null) {
+                return defaultValue!!
+            }
+
+            try {
+                val setting = setting as AbstractIntSetting
+                return setting.int
+            } catch (_: ClassCastException) {
+            }
+
+            try {
+                val setting = setting as AbstractShortSetting
+                return setting.short.toInt()
+            } catch (_: ClassCastException) {
+            }
+
+            return defaultValue!!
+        }
+
+    /**
+     * Write a value to the backing int. If that int was previously null,
+     * initializes a new one and returns it, so it can be added to the Hashmap.
+     *
+     * @param selection New value of the int.
+     * @return the existing setting with the new value applied.
+     */
+    fun setSelectedValue(selection: Int): AbstractIntSetting {
+        val intSetting = setting as AbstractIntSetting
+        intSetting.int = selection
+        return intSetting
+    }
+
+    fun setSelectedValue(selection: Short): AbstractShortSetting {
+        val shortSetting = setting as AbstractShortSetting
+        shortSetting.short = selection
+        return shortSetting
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.java
deleted file mode 100644
index 551b13f999..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.java
+++ /dev/null
@@ -1,101 +0,0 @@
-package org.citra.citra_emu.features.settings.model.view;
-
-import org.citra.citra_emu.features.settings.model.FloatSetting;
-import org.citra.citra_emu.features.settings.model.IntSetting;
-import org.citra.citra_emu.features.settings.model.Setting;
-import org.citra.citra_emu.utils.Log;
-
-public final class SliderSetting extends SettingsItem {
-    private int mMin;
-    private int mMax;
-    private int mDefaultValue;
-
-    private String mUnits;
-
-    public SliderSetting(String key, String section, int titleId, int descriptionId,
-                         int min, int max, String units, int defaultValue, Setting setting) {
-        super(key, section, setting, titleId, descriptionId);
-        mMin = min;
-        mMax = max;
-        mUnits = units;
-        mDefaultValue = defaultValue;
-    }
-
-    public int getMin() {
-        return mMin;
-    }
-
-    public int getMax() {
-        return mMax;
-    }
-
-    public int getDefaultValue() {
-        return mDefaultValue;
-    }
-
-    public int getSelectedValue() {
-        Setting setting = getSetting();
-
-        if (setting == null) {
-            return mDefaultValue;
-        }
-
-        if (setting instanceof IntSetting) {
-            IntSetting intSetting = (IntSetting) setting;
-            return intSetting.getValue();
-        } else if (setting instanceof FloatSetting) {
-            FloatSetting floatSetting = (FloatSetting) setting;
-            return Math.round(floatSetting.getValue());
-        } else {
-            Log.error("[SliderSetting] Error casting setting type.");
-            return -1;
-        }
-    }
-
-    /**
-     * Write a value to the backing int. If that int was previously null,
-     * initializes a new one and returns it, so it can be added to the Hashmap.
-     *
-     * @param selection New value of the int.
-     * @return null if overwritten successfully otherwise; a newly created IntSetting.
-     */
-    public IntSetting setSelectedValue(int selection) {
-        if (getSetting() == null) {
-            IntSetting setting = new IntSetting(getKey(), getSection(), selection);
-            setSetting(setting);
-            return setting;
-        } else {
-            IntSetting setting = (IntSetting) getSetting();
-            setting.setValue(selection);
-            return null;
-        }
-    }
-
-    /**
-     * Write a value to the backing float. If that float was previously null,
-     * initializes a new one and returns it, so it can be added to the Hashmap.
-     *
-     * @param selection New value of the float.
-     * @return null if overwritten successfully otherwise; a newly created FloatSetting.
-     */
-    public FloatSetting setSelectedValue(float selection) {
-        if (getSetting() == null) {
-            FloatSetting setting = new FloatSetting(getKey(), getSection(), selection);
-            setSetting(setting);
-            return setting;
-        } else {
-            FloatSetting setting = (FloatSetting) getSetting();
-            setting.setValue(selection);
-            return null;
-        }
-    }
-
-    public String getUnits() {
-        return mUnits;
-    }
-
-    @Override
-    public int getType() {
-        return TYPE_SLIDER;
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.kt
new file mode 100644
index 0000000000..1227bf46d1
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SliderSetting.kt
@@ -0,0 +1,70 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model.view
+
+import org.citra.citra_emu.features.settings.model.AbstractFloatSetting
+import org.citra.citra_emu.features.settings.model.AbstractIntSetting
+import org.citra.citra_emu.features.settings.model.AbstractSetting
+import org.citra.citra_emu.features.settings.model.FloatSetting
+import org.citra.citra_emu.features.settings.model.ScaledFloatSetting
+import org.citra.citra_emu.utils.Log
+import kotlin.math.roundToInt
+
+class SliderSetting(
+    setting: AbstractSetting?,
+    titleId: Int,
+    descriptionId: Int,
+    val min: Int,
+    val max: Int,
+    val units: String,
+    val key: String? = null,
+    val defaultValue: Float? = null
+) : SettingsItem(setting, titleId, descriptionId) {
+    override val type = TYPE_SLIDER
+
+    val selectedValue: Int
+        get() {
+            val setting = setting ?: return defaultValue!!.toInt()
+            return when (setting) {
+                is AbstractIntSetting -> setting.int
+                is FloatSetting -> setting.float.roundToInt()
+                is ScaledFloatSetting -> setting.float.roundToInt()
+                else -> {
+                    Log.error("[SliderSetting] Error casting setting type.")
+                    -1
+                }
+            }
+        }
+
+    /**
+     * Write a value to the backing int. If that int was previously null,
+     * initializes a new one and returns it, so it can be added to the Hashmap.
+     *
+     * @param selection New value of the int.
+     * @return the existing setting with the new value applied.
+     */
+    fun setSelectedValue(selection: Int): AbstractIntSetting {
+        val intSetting = setting as AbstractIntSetting
+        intSetting.int = selection
+        return intSetting
+    }
+
+    /**
+     * Write a value to the backing float. If that float was previously null,
+     * initializes a new one and returns it, so it can be added to the Hashmap.
+     *
+     * @param selection New value of the float.
+     * @return the existing setting with the new value applied.
+     */
+    fun setSelectedValue(selection: Float): AbstractFloatSetting {
+        val floatSetting = setting as AbstractFloatSetting
+        if (floatSetting is ScaledFloatSetting) {
+            floatSetting.float = selection
+        } else {
+            floatSetting.float = selection
+        }
+        return floatSetting
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringInputSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringInputSetting.kt
new file mode 100644
index 0000000000..ebbe1a675a
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringInputSetting.kt
@@ -0,0 +1,27 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model.view
+
+import org.citra.citra_emu.features.settings.model.AbstractSetting
+import org.citra.citra_emu.features.settings.model.AbstractStringSetting
+
+class StringInputSetting(
+    setting: AbstractSetting?,
+    titleId: Int,
+    descriptionId: Int,
+    val defaultValue: String,
+    val characterLimit: Int = 0
+) : SettingsItem(setting, titleId, descriptionId) {
+    override val type = TYPE_STRING_INPUT
+
+    val selectedValue: String
+        get() = setting?.valueAsString ?: defaultValue
+
+    fun setSelectedValue(selection: String): AbstractStringSetting {
+        val stringSetting = setting as AbstractStringSetting
+        stringSetting.string = selection
+        return stringSetting
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.java
deleted file mode 100644
index 057145d9da..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.java
+++ /dev/null
@@ -1,82 +0,0 @@
-package org.citra.citra_emu.features.settings.model.view;
-
-import org.citra.citra_emu.features.settings.model.Setting;
-import org.citra.citra_emu.features.settings.model.StringSetting;
-
-public class StringSingleChoiceSetting extends SettingsItem {
-    private String mDefaultValue;
-
-    private String[] mChoicesId;
-    private String[] mValuesId;
-
-    public StringSingleChoiceSetting(String key, String section, int titleId, int descriptionId,
-                                     String[] choicesId, String[] valuesId, String defaultValue, Setting setting) {
-        super(key, section, setting, titleId, descriptionId);
-        mValuesId = valuesId;
-        mChoicesId = choicesId;
-        mDefaultValue = defaultValue;
-    }
-
-    public String[] getChoicesId() {
-        return mChoicesId;
-    }
-
-    public String[] getValuesId() {
-        return mValuesId;
-    }
-
-    public String getValueAt(int index) {
-        if (mValuesId == null)
-            return null;
-
-        if (index >= 0 && index < mValuesId.length) {
-            return mValuesId[index];
-        }
-
-        return "";
-    }
-
-    public String getSelectedValue() {
-        if (getSetting() != null) {
-            StringSetting setting = (StringSetting) getSetting();
-            return setting.getValue();
-        } else {
-            return mDefaultValue;
-        }
-    }
-
-    public int getSelectValueIndex() {
-        String selectedValue = getSelectedValue();
-        for (int i = 0; i < mValuesId.length; i++) {
-            if (mValuesId[i].equals(selectedValue)) {
-                return i;
-            }
-        }
-
-        return -1;
-    }
-
-    /**
-     * Write a value to the backing int. If that int was previously null,
-     * initializes a new one and returns it, so it can be added to the Hashmap.
-     *
-     * @param selection New value of the int.
-     * @return null if overwritten successfully otherwise; a newly created IntSetting.
-     */
-    public StringSetting setSelectedValue(String selection) {
-        if (getSetting() == null) {
-            StringSetting setting = new StringSetting(getKey(), getSection(), selection);
-            setSetting(setting);
-            return setting;
-        } else {
-            StringSetting setting = (StringSetting) getSetting();
-            setting.setValue(selection);
-            return null;
-        }
-    }
-
-    @Override
-    public int getType() {
-        return TYPE_STRING_SINGLE_CHOICE;
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.kt
new file mode 100644
index 0000000000..c763e29f50
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/StringSingleChoiceSetting.kt
@@ -0,0 +1,78 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model.view
+
+import org.citra.citra_emu.features.settings.model.AbstractSetting
+import org.citra.citra_emu.features.settings.model.AbstractStringSetting
+
+class StringSingleChoiceSetting(
+    setting: AbstractSetting?,
+    titleId: Int,
+    descriptionId: Int,
+    val choices: Array<String>,
+    val values: Array<String>?,
+    val key: String? = null,
+    private val defaultValue: String? = null
+) : SettingsItem(setting, titleId, descriptionId) {
+    override val type = TYPE_STRING_SINGLE_CHOICE
+
+    fun getValueAt(index: Int): String? {
+        if (values == null) return null
+        return if (index >= 0 && index < values.size) {
+            values[index]
+        } else {
+            ""
+        }
+    }
+
+    val selectedValue: String
+        get() {
+            if (setting == null) {
+                return defaultValue!!
+            }
+
+            try {
+                val setting = setting as AbstractStringSetting
+                return setting.string
+            } catch (_: ClassCastException) {
+            }
+
+            try {
+                val setting = setting as AbstractShortSetting
+                return setting.short.toString()
+            } catch (_: ClassCastException) {
+            }
+            return defaultValue!!
+        }
+    val selectValueIndex: Int
+        get() {
+            val selectedValue = selectedValue
+            for (i in values!!.indices) {
+                if (values[i] == selectedValue) {
+                    return i
+                }
+            }
+            return -1
+        }
+
+    /**
+     * Write a value to the backing int. If that int was previously null,
+     * initializes a new one and returns it, so it can be added to the Hashmap.
+     *
+     * @param selection New value of the int.
+     * @return the existing setting with the new value applied.
+     */
+    fun setSelectedValue(selection: String): AbstractStringSetting {
+        val stringSetting = setting as AbstractStringSetting
+        stringSetting.string = selection
+        return stringSetting
+    }
+
+    fun setSelectedValue(selection: Short): AbstractShortSetting {
+        val shortSetting = setting as AbstractShortSetting
+        shortSetting.short = selection
+        return shortSetting
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.java
deleted file mode 100644
index 9d44a923fd..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package org.citra.citra_emu.features.settings.model.view;
-
-import org.citra.citra_emu.features.settings.model.Setting;
-
-public final class SubmenuSetting extends SettingsItem {
-    private String mMenuKey;
-
-    public SubmenuSetting(String key, Setting setting, int titleId, int descriptionId, String menuKey) {
-        super(key, null, setting, titleId, descriptionId);
-        mMenuKey = menuKey;
-    }
-
-    public String getMenuKey() {
-        return mMenuKey;
-    }
-
-    @Override
-    public int getType() {
-        return TYPE_SUBMENU;
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.kt
new file mode 100644
index 0000000000..08e1c60477
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SubmenuSetting.kt
@@ -0,0 +1,13 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model.view
+
+class SubmenuSetting(
+    titleId: Int,
+    descriptionId: Int,
+    val menuKey: String
+) : SettingsItem(null, titleId, descriptionId) {
+    override val type = TYPE_SUBMENU
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SwitchSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SwitchSetting.kt
new file mode 100644
index 0000000000..a1d27849d3
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/SwitchSetting.kt
@@ -0,0 +1,63 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.model.view
+
+import org.citra.citra_emu.features.settings.model.AbstractBooleanSetting
+import org.citra.citra_emu.features.settings.model.AbstractIntSetting
+import org.citra.citra_emu.features.settings.model.AbstractSetting
+
+class SwitchSetting(
+    setting: AbstractSetting,
+    titleId: Int,
+    descriptionId: Int,
+    val key: String? = null,
+    val defaultValue: Any? = null
+) : SettingsItem(setting, titleId, descriptionId) {
+    override val type = TYPE_SWITCH
+
+    val isChecked: Boolean
+        get() {
+            if (setting == null) {
+                return defaultValue as Boolean
+            }
+
+            // Try integer setting
+            try {
+                val setting = setting as AbstractIntSetting
+                return setting.int == 1
+            } catch (_: ClassCastException) {
+            }
+
+            // Try boolean setting
+            try {
+                val setting = setting as AbstractBooleanSetting
+                return setting.boolean
+            } catch (_: ClassCastException) {
+            }
+            return defaultValue as Boolean
+        }
+
+    /**
+     * Write a value to the backing boolean. If that boolean was previously null,
+     * initializes a new one and returns it, so it can be added to the Hashmap.
+     *
+     * @param checked Pretty self explanatory.
+     * @return the existing setting with the new value applied.
+     */
+    fun setChecked(checked: Boolean): AbstractSetting {
+        // Try integer setting
+        try {
+            val setting = setting as AbstractIntSetting
+            setting.int = if (checked) 1 else 0
+            return setting
+        } catch (_: ClassCastException) {
+        }
+
+        // Try boolean setting
+        val setting = setting as AbstractBooleanSetting
+        setting.boolean = checked
+        return setting
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.java
deleted file mode 100644
index 58ffbbfea8..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.java
+++ /dev/null
@@ -1,227 +0,0 @@
-package org.citra.citra_emu.features.settings.ui;
-
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.os.Bundle;
-import android.provider.Settings;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.widget.FrameLayout;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.core.graphics.Insets;
-import androidx.core.view.ViewCompat;
-import androidx.core.view.WindowCompat;
-import androidx.core.view.WindowInsetsCompat;
-import androidx.fragment.app.FragmentTransaction;
-import androidx.localbroadcastmanager.content.LocalBroadcastManager;
-
-import com.google.android.material.appbar.AppBarLayout;
-import com.google.android.material.appbar.MaterialToolbar;
-
-import org.citra.citra_emu.NativeLibrary;
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.utils.DirectoryInitialization;
-import org.citra.citra_emu.utils.EmulationMenuSettings;
-import org.citra.citra_emu.utils.InsetsHelper;
-import org.citra.citra_emu.utils.ThemeUtil;
-
-public final class SettingsActivity extends AppCompatActivity implements SettingsActivityView {
-    private static final String ARG_MENU_TAG = "menu_tag";
-    private static final String ARG_GAME_ID = "game_id";
-    private static final String FRAGMENT_TAG = "settings";
-    private SettingsActivityPresenter mPresenter = new SettingsActivityPresenter(this);
-
-    private ProgressDialog dialog;
-
-    public static void launch(Context context, String menuTag, String gameId) {
-        Intent settings = new Intent(context, SettingsActivity.class);
-        settings.putExtra(ARG_MENU_TAG, menuTag);
-        settings.putExtra(ARG_GAME_ID, gameId);
-        context.startActivity(settings);
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        ThemeUtil.INSTANCE.setTheme(this);
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_settings);
-
-        WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
-
-        Intent launcher = getIntent();
-        String gameID = launcher.getStringExtra(ARG_GAME_ID);
-        String menuTag = launcher.getStringExtra(ARG_MENU_TAG);
-
-        mPresenter.onCreate(savedInstanceState, menuTag, gameID);
-
-        // Show "Back" button in the action bar for navigation
-        MaterialToolbar toolbar = findViewById(R.id.toolbar_settings);
-        setSupportActionBar(toolbar);
-        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
-
-        setInsets();
-    }
-
-    @Override
-    public boolean onSupportNavigateUp() {
-        onBackPressed();
-
-        return true;
-    }
-
-    @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        MenuInflater inflater = getMenuInflater();
-        inflater.inflate(R.menu.menu_settings, menu);
-
-        return true;
-    }
-
-    @Override
-    protected void onSaveInstanceState(@NonNull Bundle outState) {
-        // Critical: If super method is not called, rotations will be busted.
-        super.onSaveInstanceState(outState);
-        mPresenter.saveState(outState);
-    }
-
-    @Override
-    protected void onStart() {
-        super.onStart();
-        mPresenter.onStart();
-    }
-
-    /**
-     * If this is called, the user has left the settings screen (potentially through the
-     * home button) and will expect their changes to be persisted. So we kick off an
-     * IntentService which will do so on a background thread.
-     */
-    @Override
-    protected void onStop() {
-        super.onStop();
-
-        mPresenter.onStop(isFinishing());
-
-        // Update framebuffer layout when closing the settings
-        NativeLibrary.INSTANCE.notifyOrientationChange(EmulationMenuSettings.getLandscapeScreenLayout(),
-                getWindowManager().getDefaultDisplay().getRotation());
-    }
-
-    @Override
-    public void showSettingsFragment(String menuTag, boolean addToStack, String gameID) {
-        if (!addToStack && getFragment() != null) {
-            return;
-        }
-
-        FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
-
-        if (addToStack) {
-            if (areSystemAnimationsEnabled()) {
-                transaction.setCustomAnimations(
-                        R.anim.anim_settings_fragment_in,
-                        R.anim.anim_settings_fragment_out,
-                        0,
-                        R.anim.anim_pop_settings_fragment_out);
-            }
-
-            transaction.addToBackStack(null);
-        }
-        transaction.replace(R.id.frame_content, SettingsFragment.newInstance(menuTag, gameID), FRAGMENT_TAG);
-
-        transaction.commit();
-    }
-
-    private boolean areSystemAnimationsEnabled() {
-        float duration = Settings.Global.getFloat(
-                getContentResolver(),
-                Settings.Global.ANIMATOR_DURATION_SCALE, 1);
-        float transition = Settings.Global.getFloat(
-                getContentResolver(),
-                Settings.Global.TRANSITION_ANIMATION_SCALE, 1);
-        return duration != 0 && transition != 0;
-    }
-
-    @Override
-    public void showLoading() {
-        if (dialog == null) {
-            dialog = new ProgressDialog(this);
-            dialog.setMessage(getString(R.string.load_settings));
-            dialog.setIndeterminate(true);
-        }
-
-        dialog.show();
-    }
-
-    @Override
-    public void hideLoading() {
-        dialog.dismiss();
-    }
-
-    @Override
-    public void showPermissionNeededHint() {
-        Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_SHORT)
-                .show();
-    }
-
-    @Override
-    public void showExternalStorageNotMountedHint() {
-        Toast.makeText(this, R.string.external_storage_not_mounted, Toast.LENGTH_SHORT)
-                .show();
-    }
-
-    @Override
-    public org.citra.citra_emu.features.settings.model.Settings getSettings() {
-        return mPresenter.getSettings();
-    }
-
-    @Override
-    public void setSettings(org.citra.citra_emu.features.settings.model.Settings settings) {
-        mPresenter.setSettings(settings);
-    }
-
-    @Override
-    public void onSettingsFileLoaded(org.citra.citra_emu.features.settings.model.Settings settings) {
-        SettingsFragmentView fragment = getFragment();
-
-        if (fragment != null) {
-            fragment.onSettingsFileLoaded(settings);
-        }
-    }
-
-    @Override
-    public void onSettingsFileNotFound() {
-        SettingsFragmentView fragment = getFragment();
-
-        if (fragment != null) {
-            fragment.loadDefaultSettings();
-        }
-    }
-
-    @Override
-    public void showToastMessage(String message, boolean is_long) {
-        Toast.makeText(this, message, is_long ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show();
-    }
-
-    @Override
-    public void onSettingChanged() {
-        mPresenter.onSettingChanged();
-    }
-
-    private SettingsFragment getFragment() {
-        return (SettingsFragment) getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG);
-    }
-
-    private void setInsets() {
-        AppBarLayout appBar = findViewById(R.id.appbar_settings);
-        FrameLayout frame = findViewById(R.id.frame_content);
-        ViewCompat.setOnApplyWindowInsetsListener(frame, (v, windowInsets) -> {
-            Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
-            InsetsHelper.insetAppBar(insets, appBar);
-            return windowInsets;
-        });
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt
new file mode 100644
index 0000000000..796f577b1e
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivity.kt
@@ -0,0 +1,292 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.ui
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import android.view.ViewGroup.MarginLayoutParams
+import android.widget.Toast
+import androidx.activity.OnBackPressedCallback
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.viewModels
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.preference.PreferenceManager
+import com.google.android.material.color.MaterialColors
+import org.citra.citra_emu.CitraApplication
+import org.citra.citra_emu.NativeLibrary
+import org.citra.citra_emu.R
+import org.citra.citra_emu.databinding.ActivitySettingsBinding
+import java.io.IOException
+import org.citra.citra_emu.features.settings.model.BooleanSetting
+import org.citra.citra_emu.features.settings.model.FloatSetting
+import org.citra.citra_emu.features.settings.model.IntSetting
+import org.citra.citra_emu.features.settings.model.ScaledFloatSetting
+import org.citra.citra_emu.features.settings.model.Settings
+import org.citra.citra_emu.features.settings.model.SettingsViewModel
+import org.citra.citra_emu.features.settings.model.StringSetting
+import org.citra.citra_emu.features.settings.utils.SettingsFile
+import org.citra.citra_emu.utils.SystemSaveGame
+import org.citra.citra_emu.utils.DirectoryInitialization
+import org.citra.citra_emu.utils.InsetsHelper
+import org.citra.citra_emu.utils.ThemeUtil
+
+class SettingsActivity : AppCompatActivity(), SettingsActivityView {
+    private val presenter = SettingsActivityPresenter(this)
+
+    private lateinit var binding: ActivitySettingsBinding
+
+    private val settingsViewModel: SettingsViewModel by viewModels()
+
+    override val settings: Settings get() = settingsViewModel.settings
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        ThemeUtil.setTheme(this)
+
+        super.onCreate(savedInstanceState)
+
+        binding = ActivitySettingsBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        WindowCompat.setDecorFitsSystemWindows(window, false)
+
+        val launcher = intent
+        val gameID = launcher.getStringExtra(ARG_GAME_ID)
+        val menuTag = launcher.getStringExtra(ARG_MENU_TAG)
+        presenter.onCreate(savedInstanceState, menuTag!!, gameID!!)
+
+        // Show "Back" button in the action bar for navigation
+        setSupportActionBar(binding.toolbarSettings)
+        supportActionBar!!.setDisplayHomeAsUpEnabled(true)
+
+        if (InsetsHelper.getSystemGestureType(applicationContext) !=
+            InsetsHelper.GESTURE_NAVIGATION
+        ) {
+            binding.navigationBarShade.setBackgroundColor(
+                ThemeUtil.getColorWithOpacity(
+                    MaterialColors.getColor(
+                        binding.navigationBarShade,
+                        com.google.android.material.R.attr.colorSurface
+                    ),
+                    ThemeUtil.SYSTEM_BAR_ALPHA
+                )
+            )
+        }
+
+        onBackPressedDispatcher.addCallback(
+            this,
+            object : OnBackPressedCallback(true) {
+                override fun handleOnBackPressed() = navigateBack()
+            }
+        )
+
+        setInsets()
+    }
+
+    override fun onSupportNavigateUp(): Boolean {
+        navigateBack()
+        return true
+    }
+
+    private fun navigateBack() {
+        if (supportFragmentManager.backStackEntryCount > 0) {
+            supportFragmentManager.popBackStack()
+        } else {
+            finish()
+        }
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        // Critical: If super method is not called, rotations will be busted.
+        super.onSaveInstanceState(outState)
+        presenter.saveState(outState)
+    }
+
+    override fun onStart() {
+        super.onStart()
+        presenter.onStart()
+    }
+
+    /**
+     * If this is called, the user has left the settings screen (potentially through the
+     * home button) and will expect their changes to be persisted. So we kick off an
+     * IntentService which will do so on a background thread.
+     */
+    override fun onStop() {
+        super.onStop()
+        presenter.onStop(isFinishing)
+    }
+
+    override fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String) {
+        if (!addToStack && settingsFragment != null) {
+            return
+        }
+
+        val transaction = supportFragmentManager.beginTransaction()
+        if (addToStack) {
+            if (areSystemAnimationsEnabled()) {
+                transaction.setCustomAnimations(
+                    R.anim.anim_settings_fragment_in,
+                    R.anim.anim_settings_fragment_out,
+                    0,
+                    R.anim.anim_pop_settings_fragment_out
+                )
+            }
+            transaction.addToBackStack(null)
+        }
+        transaction.replace(
+            R.id.frame_content,
+            SettingsFragment.newInstance(menuTag, gameId),
+            FRAGMENT_TAG
+        )
+        transaction.commit()
+    }
+
+    private fun areSystemAnimationsEnabled(): Boolean {
+        val duration = android.provider.Settings.Global.getFloat(
+            contentResolver,
+            android.provider.Settings.Global.ANIMATOR_DURATION_SCALE,
+            1f
+        )
+        val transition = android.provider.Settings.Global.getFloat(
+            contentResolver,
+            android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE,
+            1f
+        )
+        return duration != 0f && transition != 0f
+    }
+
+    override fun onSettingsFileLoaded() {
+        val fragment: SettingsFragmentView? = settingsFragment
+        fragment?.loadSettingsList()
+    }
+
+    override fun onSettingsFileNotFound() {
+        val fragment: SettingsFragmentView? = settingsFragment
+        fragment?.loadSettingsList()
+    }
+
+    override fun showToastMessage(message: String, isLong: Boolean) {
+        Toast.makeText(
+            this,
+            message,
+            if (isLong) Toast.LENGTH_LONG else Toast.LENGTH_SHORT
+        ).show()
+    }
+
+    override fun onSettingChanged() {
+        presenter.onSettingChanged()
+    }
+
+    fun onSettingsReset() {
+        // Prevents saving to a non-existent settings file
+        presenter.onSettingsReset()
+
+        val controllerKeys = Settings.buttonKeys + Settings.circlePadKeys + Settings.cStickKeys +
+                Settings.dPadKeys + Settings.triggerKeys
+        val editor =
+            PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext).edit()
+        controllerKeys.forEach { editor.remove(it) }
+        editor.apply()
+
+        // Reset the static memory representation of each setting
+        BooleanSetting.clear()
+        FloatSetting.clear()
+        ScaledFloatSetting.clear()
+        IntSetting.clear()
+        StringSetting.clear()
+
+        // Delete settings file because the user may have changed values that do not exist in the UI
+        val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
+        if (!settingsFile.delete()) {
+            throw IOException("Failed to delete $settingsFile")
+        }
+
+        // Set the root of the document tree before we create a new config file or the native code
+        // will fail when creating the file.
+        if (DirectoryInitialization.setCitraUserDirectory()) {
+            CitraApplication.documentsTree.setRoot(Uri.parse(DirectoryInitialization.userPath))
+            NativeLibrary.createConfigFile()
+        } else {
+            throw IllegalStateException("Citra directory unavailable when accessing config file!")
+        }
+
+        // Set default values for system config file
+        SystemSaveGame.apply {
+            setUsername("CITRA")
+            setBirthday(3, 25)
+            setSystemLanguage(1)
+            setSoundOutputMode(2)
+            setCountryCode(49)
+            setPlayCoins(42)
+        }
+
+        showToastMessage(getString(R.string.settings_reset), true)
+        finish()
+    }
+
+    fun setToolbarTitle(title: String) {
+        binding.toolbarSettingsLayout.title = title
+    }
+
+    private val settingsFragment: SettingsFragment?
+        get() = supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as SettingsFragment?
+
+    private fun setInsets() {
+        ViewCompat.setOnApplyWindowInsetsListener(
+            binding.frameContent
+        ) { view: View, windowInsets: WindowInsetsCompat ->
+            val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+            val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+            view.updatePadding(
+                left = barInsets.left + cutoutInsets.left,
+                right = barInsets.right + cutoutInsets.right
+            )
+
+            val mlpAppBar = binding.appbarSettings.layoutParams as MarginLayoutParams
+            mlpAppBar.leftMargin = barInsets.left + cutoutInsets.left
+            mlpAppBar.rightMargin = barInsets.right + cutoutInsets.right
+            binding.appbarSettings.layoutParams = mlpAppBar
+
+            val mlpShade = binding.navigationBarShade.layoutParams as MarginLayoutParams
+            mlpShade.height = barInsets.bottom
+            binding.navigationBarShade.layoutParams = mlpShade
+
+            windowInsets
+        }
+    }
+
+    companion object {
+        private const val ARG_MENU_TAG = "menu_tag"
+        private const val ARG_GAME_ID = "game_id"
+        private const val FRAGMENT_TAG = "settings"
+
+        @JvmStatic
+        fun launch(context: Context, menuTag: String?, gameId: String?) {
+            val settings = Intent(context, SettingsActivity::class.java)
+            settings.putExtra(ARG_MENU_TAG, menuTag)
+            settings.putExtra(ARG_GAME_ID, gameId)
+            context.startActivity(settings)
+        }
+
+        fun launch(
+            context: Context,
+            launcher: ActivityResultLauncher<Intent>,
+            menuTag: String?,
+            gameId: String?
+        ) {
+            val settings = Intent(context, SettingsActivity::class.java)
+            settings.putExtra(ARG_MENU_TAG, menuTag)
+            settings.putExtra(ARG_GAME_ID, gameId)
+            launcher.launch(settings)
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.java
deleted file mode 100644
index 84a7d9d646..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.java
+++ /dev/null
@@ -1,91 +0,0 @@
-package org.citra.citra_emu.features.settings.ui;
-
-import android.content.IntentFilter;
-import android.os.Bundle;
-import android.text.TextUtils;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.documentfile.provider.DocumentFile;
-import java.io.File;
-import org.citra.citra_emu.NativeLibrary;
-import org.citra.citra_emu.features.settings.model.Settings;
-import org.citra.citra_emu.features.settings.utils.SettingsFile;
-import org.citra.citra_emu.utils.DirectoryInitialization;
-import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState;
-import org.citra.citra_emu.utils.Log;
-import org.citra.citra_emu.utils.ThemeUtil;
-
-public final class SettingsActivityPresenter {
-    private static final String KEY_SHOULD_SAVE = "should_save";
-
-    private SettingsActivityView mView;
-
-    private Settings mSettings = new Settings();
-
-    private boolean mShouldSave;
-
-    private String menuTag;
-    private String gameId;
-
-    public SettingsActivityPresenter(SettingsActivityView view) {
-        mView = view;
-    }
-
-    public void onCreate(Bundle savedInstanceState, String menuTag, String gameId) {
-        if (savedInstanceState == null) {
-            this.menuTag = menuTag;
-            this.gameId = gameId;
-        } else {
-            mShouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE);
-        }
-    }
-
-    public void onStart() {
-        prepareCitraDirectoriesIfNeeded();
-    }
-
-    void loadSettingsUI() {
-        if (mSettings.isEmpty()) {
-            if (!TextUtils.isEmpty(gameId)) {
-                mSettings.loadSettings(gameId, mView);
-            } else {
-                mSettings.loadSettings(mView);
-            }
-        }
-
-        mView.showSettingsFragment(menuTag, false, gameId);
-        mView.onSettingsFileLoaded(mSettings);
-    }
-
-    private void prepareCitraDirectoriesIfNeeded() {
-        DocumentFile configFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG);
-        if (configFile == null || !configFile.exists()) {
-            Log.error("Citra config file could not be found!");
-        }
-        loadSettingsUI();
-    }
-
-    public void setSettings(Settings settings) {
-        mSettings = settings;
-    }
-
-    public Settings getSettings() {
-        return mSettings;
-    }
-
-    public void onStop(boolean finishing) {
-        if (mSettings != null && finishing && mShouldSave) {
-            Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...");
-            mSettings.saveSettings(mView);
-        }
-
-        NativeLibrary.INSTANCE.reloadSettings();
-    }
-
-    public void onSettingChanged() {
-        mShouldSave = true;
-    }
-
-    public void saveState(Bundle outState) {
-        outState.putBoolean(KEY_SHOULD_SAVE, mShouldSave);
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt
new file mode 100644
index 0000000000..ae36531092
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityPresenter.kt
@@ -0,0 +1,78 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.ui
+
+import android.os.Bundle
+import android.text.TextUtils
+import org.citra.citra_emu.NativeLibrary
+import org.citra.citra_emu.features.settings.model.Settings
+import org.citra.citra_emu.utils.SystemSaveGame
+import org.citra.citra_emu.utils.DirectoryInitialization
+import org.citra.citra_emu.utils.Log
+
+class SettingsActivityPresenter(private val activityView: SettingsActivityView) {
+    val settings: Settings get() = activityView.settings
+
+    private var shouldSave = false
+    private lateinit var menuTag: String
+    private lateinit var gameId: String
+
+    fun onCreate(savedInstanceState: Bundle?, menuTag: String, gameId: String) {
+        this.menuTag = menuTag
+        this.gameId = gameId
+        if (savedInstanceState != null) {
+            shouldSave = savedInstanceState.getBoolean(KEY_SHOULD_SAVE)
+        }
+    }
+
+    fun onStart() {
+        SystemSaveGame.load()
+        prepareDirectoriesIfNeeded()
+    }
+
+    private fun loadSettingsUI() {
+        if (!settings.isLoaded) {
+            if (!TextUtils.isEmpty(gameId)) {
+                settings.loadSettings(gameId, activityView)
+            } else {
+                settings.loadSettings(activityView)
+            }
+        }
+        activityView.showSettingsFragment(menuTag, false, gameId)
+        activityView.onSettingsFileLoaded()
+    }
+
+    private fun prepareDirectoriesIfNeeded() {
+        if (!DirectoryInitialization.areCitraDirectoriesReady()) {
+            DirectoryInitialization.start()
+        }
+        loadSettingsUI()
+    }
+
+    fun onStop(finishing: Boolean) {
+        if (finishing && shouldSave) {
+            Log.debug("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
+            settings.saveSettings(activityView)
+            SystemSaveGame.save()
+        }
+        NativeLibrary.reloadSettings()
+    }
+
+    fun onSettingChanged() {
+        shouldSave = true
+    }
+
+    fun onSettingsReset() {
+        shouldSave = false
+    }
+
+    fun saveState(outState: Bundle) {
+        outState.putBoolean(KEY_SHOULD_SAVE, shouldSave)
+    }
+
+    companion object {
+        private const val KEY_SHOULD_SAVE = "should_save"
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.java
deleted file mode 100644
index bd2f5f5aa6..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package org.citra.citra_emu.features.settings.ui;
-
-import android.content.IntentFilter;
-
-import org.citra.citra_emu.features.settings.model.Settings;
-
-/**
- * Abstraction for the Activity that manages SettingsFragments.
- */
-public interface SettingsActivityView {
-    /**
-     * Show a new SettingsFragment.
-     *
-     * @param menuTag    Identifier for the settings group that should be displayed.
-     * @param addToStack Whether or not this fragment should replace a previous one.
-     */
-    void showSettingsFragment(String menuTag, boolean addToStack, String gameId);
-
-    /**
-     * Called by a contained Fragment to get access to the Setting HashMap
-     * loaded from disk, so that each Fragment doesn't need to perform its own
-     * read operation.
-     *
-     * @return A possibly null HashMap of Settings.
-     */
-    Settings getSettings();
-
-    /**
-     * Used to provide the Activity with Settings HashMaps if a Fragment already
-     * has one; for example, if a rotation occurs, the Fragment will not be killed,
-     * but the Activity will, so the Activity needs to have its HashMaps resupplied.
-     *
-     * @param settings The ArrayList of all the Settings HashMaps.
-     */
-    void setSettings(Settings settings);
-
-    /**
-     * Called when an asynchronous load operation completes.
-     *
-     * @param settings The (possibly null) result of the ini load operation.
-     */
-    void onSettingsFileLoaded(Settings settings);
-
-    /**
-     * Called when an asynchronous load operation fails.
-     */
-    void onSettingsFileNotFound();
-
-    /**
-     * Display a popup text message on screen.
-     *
-     * @param message The contents of the onscreen message.
-     * @param is_long Whether this should be a long Toast or short one.
-     */
-    void showToastMessage(String message, boolean is_long);
-
-    /**
-     * End the activity.
-     */
-    void finish();
-
-    /**
-     * Called by a containing Fragment to tell the Activity that a setting was changed;
-     * unless this has been called, the Activity will not save to disk.
-     */
-    void onSettingChanged();
-
-    /**
-     * Show loading dialog while loading the settings
-     */
-    void showLoading();
-
-    /**
-     * Hide the loading the dialog
-     */
-    void hideLoading();
-
-    /**
-     * Show a hint to the user that the app needs write to external storage access
-     */
-    void showPermissionNeededHint();
-
-    /**
-     * Show a hint to the user that the app needs the external storage to be mounted
-     */
-    void showExternalStorageNotMountedHint();
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.kt
new file mode 100644
index 0000000000..126a010f01
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsActivityView.kt
@@ -0,0 +1,58 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.ui
+
+import org.citra.citra_emu.features.settings.model.Settings
+
+/**
+ * Abstraction for the Activity that manages SettingsFragments.
+ */
+interface SettingsActivityView {
+    /**
+     * Show a new SettingsFragment.
+     *
+     * @param menuTag    Identifier for the settings group that should be displayed.
+     * @param addToStack Whether or not this fragment should replace a previous one.
+     */
+    fun showSettingsFragment(menuTag: String, addToStack: Boolean, gameId: String)
+
+    /**
+     * Called by a contained Fragment to get access to the Setting HashMap
+     * loaded from disk, so that each Fragment doesn't need to perform its own
+     * read operation.
+     *
+     * @return A HashMap of Settings.
+     */
+    val settings: Settings
+
+    /**
+     * Called when a load operation completes.
+     */
+    fun onSettingsFileLoaded()
+
+    /**
+     * Called when a load operation fails.
+     */
+    fun onSettingsFileNotFound()
+
+    /**
+     * Display a popup text message on screen.
+     *
+     * @param message The contents of the onscreen message.
+     * @param isLong Whether this should be a long Toast or short one.
+     */
+    fun showToastMessage(message: String, isLong: Boolean)
+
+    /**
+     * End the activity.
+     */
+    fun finish()
+
+    /**
+     * Called by a containing Fragment to tell the Activity that a setting was changed;
+     * unless this has been called, the Activity will not save to disk.
+     */
+    fun onSettingChanged()
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.java
deleted file mode 100644
index b03ec11b4a..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.java
+++ /dev/null
@@ -1,393 +0,0 @@
-package org.citra.citra_emu.features.settings.ui;
-
-import android.content.Context;
-import android.content.DialogInterface;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.DatePicker;
-import android.widget.TextView;
-import android.widget.TimePicker;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.google.android.material.slider.Slider;
-
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.dialogs.MotionAlertDialog;
-import org.citra.citra_emu.features.settings.model.FloatSetting;
-import org.citra.citra_emu.features.settings.model.IntSetting;
-import org.citra.citra_emu.features.settings.model.StringSetting;
-import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
-import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
-import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
-import org.citra.citra_emu.features.settings.model.view.SettingsItem;
-import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
-import org.citra.citra_emu.features.settings.model.view.SliderSetting;
-import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
-import org.citra.citra_emu.features.settings.model.view.SubmenuSetting;
-import org.citra.citra_emu.features.settings.ui.viewholder.CheckBoxSettingViewHolder;
-import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder;
-import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder;
-import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder;
-import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder;
-import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder;
-import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder;
-import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder;
-import org.citra.citra_emu.utils.Log;
-
-import java.util.ArrayList;
-
-public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolder> implements DialogInterface.OnClickListener, Slider.OnChangeListener {
-    private SettingsFragmentView mView;
-    private Context mContext;
-    private ArrayList<SettingsItem> mSettings;
-
-    private SettingsItem mClickedItem;
-    private int mClickedPosition;
-    private int mSliderProgress;
-
-    private AlertDialog mDialog;
-    private TextView mTextSliderValue;
-
-    public SettingsAdapter(SettingsFragmentView view, Context context) {
-        mView = view;
-        mContext = context;
-        mClickedPosition = -1;
-    }
-
-    @Override
-    public SettingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-        View view;
-        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
-
-        switch (viewType) {
-            case SettingsItem.TYPE_HEADER:
-                view = inflater.inflate(R.layout.list_item_settings_header, parent, false);
-                return new HeaderViewHolder(view, this);
-
-            case SettingsItem.TYPE_CHECKBOX:
-                view = inflater.inflate(R.layout.list_item_setting_checkbox, parent, false);
-                return new CheckBoxSettingViewHolder(view, this);
-
-            case SettingsItem.TYPE_SINGLE_CHOICE:
-            case SettingsItem.TYPE_STRING_SINGLE_CHOICE:
-                view = inflater.inflate(R.layout.list_item_setting, parent, false);
-                return new SingleChoiceViewHolder(view, this);
-
-            case SettingsItem.TYPE_SLIDER:
-                view = inflater.inflate(R.layout.list_item_setting, parent, false);
-                return new SliderViewHolder(view, this);
-
-            case SettingsItem.TYPE_SUBMENU:
-                view = inflater.inflate(R.layout.list_item_setting, parent, false);
-                return new SubmenuViewHolder(view, this);
-
-            case SettingsItem.TYPE_INPUT_BINDING:
-                view = inflater.inflate(R.layout.list_item_setting, parent, false);
-                return new InputBindingSettingViewHolder(view, this, mContext);
-
-            case SettingsItem.TYPE_DATETIME_SETTING:
-                view = inflater.inflate(R.layout.list_item_setting, parent, false);
-                return new DateTimeViewHolder(view, this);
-
-            default:
-                Log.error("[SettingsAdapter] Invalid view type: " + viewType);
-                return null;
-        }
-    }
-
-    @Override
-    public void onBindViewHolder(SettingViewHolder holder, int position) {
-        holder.bind(getItem(position));
-    }
-
-    private SettingsItem getItem(int position) {
-        return mSettings.get(position);
-    }
-
-    @Override
-    public int getItemCount() {
-        if (mSettings != null) {
-            return mSettings.size();
-        } else {
-            return 0;
-        }
-    }
-
-    @Override
-    public int getItemViewType(int position) {
-        return getItem(position).getType();
-    }
-
-    public void setSettings(ArrayList<SettingsItem> settings) {
-        mSettings = settings;
-        notifyDataSetChanged();
-    }
-
-    public void onBooleanClick(CheckBoxSetting item, int position, boolean checked) {
-        IntSetting setting = item.setChecked(checked);
-        notifyItemChanged(position);
-
-        if (setting != null) {
-            mView.putSetting(setting);
-        }
-
-        mView.onSettingChanged();
-    }
-
-    public void onSingleChoiceClick(SingleChoiceSetting item) {
-        mClickedItem = item;
-
-        int value = getSelectionForSingleChoiceValue(item);
-
-        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
-                .setTitle(item.getNameId())
-                .setSingleChoiceItems(item.getChoicesId(), value, this);
-        mDialog = builder.show();
-    }
-
-    public void onSingleChoiceClick(SingleChoiceSetting item, int position) {
-        mClickedPosition = position;
-        onSingleChoiceClick(item);
-    }
-
-    public void onStringSingleChoiceClick(StringSingleChoiceSetting item) {
-        mClickedItem = item;
-
-        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
-                .setTitle(item.getNameId())
-                .setSingleChoiceItems(item.getChoicesId(), item.getSelectValueIndex(), this);
-        mDialog = builder.show();
-    }
-
-    public void onStringSingleChoiceClick(StringSingleChoiceSetting item, int position) {
-        mClickedPosition = position;
-        onStringSingleChoiceClick(item);
-    }
-
-    DialogInterface.OnClickListener defaultCancelListener = (dialog, which) -> closeDialog();
-
-    public void onDateTimeClick(DateTimeSetting item, int position) {
-        mClickedItem = item;
-        mClickedPosition = position;
-
-        LayoutInflater inflater = LayoutInflater.from(mView.getActivity());
-        View view = inflater.inflate(R.layout.sysclock_datetime_picker, null);
-
-        DatePicker dp = view.findViewById(R.id.date_picker);
-        TimePicker tp = view.findViewById(R.id.time_picker);
-
-        //set date and time to substrings of settingValue; format = 2018-12-24 04:20:69 (alright maybe not that 69)
-        String settingValue = item.getValue();
-        dp.updateDate(Integer.parseInt(settingValue.substring(0, 4)), Integer.parseInt(settingValue.substring(5, 7)) - 1, Integer.parseInt(settingValue.substring(8, 10)));
-
-        tp.setIs24HourView(true);
-        tp.setHour(Integer.parseInt(settingValue.substring(11, 13)));
-        tp.setMinute(Integer.parseInt(settingValue.substring(14, 16)));
-
-        DialogInterface.OnClickListener ok = (dialog, which) -> {
-            //set it
-            int year = dp.getYear();
-            if (year < 2000) {
-                year = 2000;
-            }
-            String month = ("00" + (dp.getMonth() + 1)).substring(String.valueOf(dp.getMonth() + 1).length());
-            String day = ("00" + dp.getDayOfMonth()).substring(String.valueOf(dp.getDayOfMonth()).length());
-            String hr = ("00" + tp.getHour()).substring(String.valueOf(tp.getHour()).length());
-            String min = ("00" + tp.getMinute()).substring(String.valueOf(tp.getMinute()).length());
-            String datetime = year + "-" + month + "-" + day + " " + hr + ":" + min + ":01";
-
-            StringSetting setting = item.setSelectedValue(datetime);
-            if (setting != null) {
-                mView.putSetting(setting);
-            }
-
-            mView.onSettingChanged();
-
-            mClickedItem = null;
-            closeDialog();
-        };
-
-        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
-                .setView(view)
-                .setPositiveButton(android.R.string.ok, ok)
-                .setNegativeButton(android.R.string.cancel, defaultCancelListener);
-        mDialog = builder.show();
-    }
-
-    public void onSliderClick(SliderSetting item, int position) {
-        mClickedItem = item;
-        mClickedPosition = position;
-        mSliderProgress = item.getSelectedValue();
-
-        LayoutInflater inflater = LayoutInflater.from(mView.getActivity());
-        View view = inflater.inflate(R.layout.dialog_slider, null);
-
-        Slider slider = view.findViewById(R.id.slider);
-
-        MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(mView.getActivity())
-                .setTitle(item.getNameId())
-                .setView(view)
-                .setPositiveButton(android.R.string.ok, this)
-                .setNegativeButton(android.R.string.cancel, defaultCancelListener)
-                .setNeutralButton(R.string.slider_default, (DialogInterface dialog, int which) -> {
-                    slider.setValue(item.getDefaultValue());
-                    onClick(dialog, which);
-                });
-        mDialog = builder.show();
-
-        mTextSliderValue = view.findViewById(R.id.text_value);
-        mTextSliderValue.setText(String.valueOf(mSliderProgress));
-
-        TextView units = view.findViewById(R.id.text_units);
-        units.setText(item.getUnits());
-
-        slider.setValueFrom(item.getMin());
-        slider.setValueTo(item.getMax());
-        slider.setValue(mSliderProgress);
-
-        slider.addOnChangeListener(this);
-    }
-
-    public void onSubmenuClick(SubmenuSetting item) {
-        mView.loadSubMenu(item.getMenuKey());
-    }
-
-    public void onInputBindingClick(final InputBindingSetting item, final int position) {
-        final MotionAlertDialog dialog = new MotionAlertDialog(mContext, item);
-        dialog.setTitle(R.string.input_binding);
-
-        int messageResId = R.string.input_binding_description;
-        if (item.IsAxisMappingSupported() && !item.IsTrigger()) {
-            // Use specialized message for axis left/right or up/down
-            if (item.IsHorizontalOrientation()) {
-                messageResId = R.string.input_binding_description_horizontal_axis;
-            } else {
-                messageResId = R.string.input_binding_description_vertical_axis;
-            }
-        }
-
-        dialog.setMessage(String.format(mContext.getString(messageResId), mContext.getString(item.getNameId())));
-        dialog.setButton(AlertDialog.BUTTON_NEGATIVE, mContext.getString(android.R.string.cancel), this);
-        dialog.setButton(AlertDialog.BUTTON_NEUTRAL, mContext.getString(R.string.clear), (dialogInterface, i) ->
-                item.removeOldMapping());
-        dialog.setOnDismissListener(dialog1 ->
-        {
-            StringSetting setting = new StringSetting(item.getKey(), item.getSection(), item.getValue());
-            notifyItemChanged(position);
-
-            mView.putSetting(setting);
-
-            mView.onSettingChanged();
-        });
-        dialog.setCanceledOnTouchOutside(false);
-        dialog.show();
-    }
-
-    @Override
-    public void onClick(DialogInterface dialog, int which) {
-        if (mClickedItem instanceof SingleChoiceSetting) {
-            SingleChoiceSetting scSetting = (SingleChoiceSetting) mClickedItem;
-
-            int value = getValueForSingleChoiceSelection(scSetting, which);
-            if (scSetting.getSelectedValue() != value) {
-                mView.onSettingChanged();
-            }
-
-            // Get the backing Setting, which may be null (if for example it was missing from the file)
-            IntSetting setting = scSetting.setSelectedValue(value);
-            if (setting != null) {
-                mView.putSetting(setting);
-            }
-
-            closeDialog();
-        } else if (mClickedItem instanceof StringSingleChoiceSetting) {
-            StringSingleChoiceSetting scSetting = (StringSingleChoiceSetting) mClickedItem;
-            String value = scSetting.getValueAt(which);
-            if (!scSetting.getSelectedValue().equals(value))
-                mView.onSettingChanged();
-
-            StringSetting setting = scSetting.setSelectedValue(value);
-            if (setting != null) {
-                mView.putSetting(setting);
-            }
-
-            closeDialog();
-        } else if (mClickedItem instanceof SliderSetting) {
-            SliderSetting sliderSetting = (SliderSetting) mClickedItem;
-            if (sliderSetting.getSelectedValue() != mSliderProgress) {
-                mView.onSettingChanged();
-            }
-
-            if (sliderSetting.getSetting() instanceof FloatSetting) {
-                float value = (float) mSliderProgress;
-
-                FloatSetting setting = sliderSetting.setSelectedValue(value);
-                if (setting != null) {
-                    mView.putSetting(setting);
-                }
-            } else {
-                IntSetting setting = sliderSetting.setSelectedValue(mSliderProgress);
-                if (setting != null) {
-                    mView.putSetting(setting);
-                }
-            }
-
-            closeDialog();
-        }
-
-        mClickedItem = null;
-        mSliderProgress = -1;
-    }
-
-    public void closeDialog() {
-        if (mDialog != null) {
-            if (mClickedPosition != -1) {
-                notifyItemChanged(mClickedPosition);
-                mClickedPosition = -1;
-            }
-            mDialog.dismiss();
-            mDialog = null;
-        }
-    }
-
-    private int getValueForSingleChoiceSelection(SingleChoiceSetting item, int which) {
-        int valuesId = item.getValuesId();
-
-        if (valuesId > 0) {
-            int[] valuesArray = mContext.getResources().getIntArray(valuesId);
-            return valuesArray[which];
-        } else {
-            return which;
-        }
-    }
-
-    private int getSelectionForSingleChoiceValue(SingleChoiceSetting item) {
-        int value = item.getSelectedValue();
-        int valuesId = item.getValuesId();
-
-        if (valuesId > 0) {
-            int[] valuesArray = mContext.getResources().getIntArray(valuesId);
-            for (int index = 0; index < valuesArray.length; index++) {
-                int current = valuesArray[index];
-                if (current == value) {
-                    return index;
-                }
-            }
-        } else {
-            return value;
-        }
-
-        return -1;
-    }
-
-    @Override
-    public void onValueChange(@NonNull Slider slider, float value, boolean fromUser) {
-        mSliderProgress = (int) value;
-        mTextSliderValue.setText(String.valueOf(mSliderProgress));
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt
new file mode 100644
index 0000000000..6d9cb97985
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsAdapter.kt
@@ -0,0 +1,503 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.ui
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.DialogInterface
+import android.icu.util.Calendar
+import android.icu.util.TimeZone
+import android.text.InputFilter
+import android.text.format.DateFormat
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.appcompat.app.AlertDialog
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.widget.doOnTextChanged
+import androidx.fragment.app.FragmentActivity
+import androidx.recyclerview.widget.RecyclerView
+import com.google.android.material.datepicker.MaterialDatePicker
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.android.material.slider.Slider
+import com.google.android.material.timepicker.MaterialTimePicker
+import com.google.android.material.timepicker.TimeFormat
+import org.citra.citra_emu.R
+import org.citra.citra_emu.databinding.DialogSliderBinding
+import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding
+import org.citra.citra_emu.databinding.ListItemSettingBinding
+import org.citra.citra_emu.databinding.ListItemSettingSwitchBinding
+import org.citra.citra_emu.databinding.ListItemSettingsHeaderBinding
+import org.citra.citra_emu.features.settings.model.AbstractBooleanSetting
+import org.citra.citra_emu.features.settings.model.AbstractFloatSetting
+import org.citra.citra_emu.features.settings.model.AbstractIntSetting
+import org.citra.citra_emu.features.settings.model.AbstractSetting
+import org.citra.citra_emu.features.settings.model.AbstractStringSetting
+import org.citra.citra_emu.features.settings.model.FloatSetting
+import org.citra.citra_emu.features.settings.model.ScaledFloatSetting
+import org.citra.citra_emu.features.settings.model.view.AbstractShortSetting
+import org.citra.citra_emu.features.settings.model.view.DateTimeSetting
+import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
+import org.citra.citra_emu.features.settings.model.view.SettingsItem
+import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting
+import org.citra.citra_emu.features.settings.model.view.SliderSetting
+import org.citra.citra_emu.features.settings.model.view.StringInputSetting
+import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting
+import org.citra.citra_emu.features.settings.model.view.SubmenuSetting
+import org.citra.citra_emu.features.settings.model.view.SwitchSetting
+import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder
+import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder
+import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder
+import org.citra.citra_emu.features.settings.ui.viewholder.RunnableViewHolder
+import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder
+import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder
+import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder
+import org.citra.citra_emu.features.settings.ui.viewholder.StringInputViewHolder
+import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder
+import org.citra.citra_emu.features.settings.ui.viewholder.SwitchSettingViewHolder
+import org.citra.citra_emu.fragments.MessageDialogFragment
+import org.citra.citra_emu.fragments.MotionBottomSheetDialogFragment
+import org.citra.citra_emu.utils.SystemSaveGame
+import java.lang.IllegalStateException
+import java.lang.NumberFormatException
+import java.text.SimpleDateFormat
+
+class SettingsAdapter(
+    private val fragmentView: SettingsFragmentView,
+    private val context: Context
+) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener {
+    private var settings: ArrayList<SettingsItem>? = null
+    private var clickedItem: SettingsItem? = null
+    private var clickedPosition: Int
+    private var dialog: AlertDialog? = null
+    private var sliderProgress = 0
+    private var textSliderValue: TextView? = null
+    private var textInputValue: String = ""
+
+    private var defaultCancelListener =
+        DialogInterface.OnClickListener { _: DialogInterface?, _: Int -> closeDialog() }
+
+    init {
+        clickedPosition = -1
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingViewHolder {
+        val inflater = LayoutInflater.from(parent.context)
+        return when (viewType) {
+            SettingsItem.TYPE_HEADER -> {
+                HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
+            }
+
+            SettingsItem.TYPE_SWITCH -> {
+                SwitchSettingViewHolder(ListItemSettingSwitchBinding.inflate(inflater), this)
+            }
+
+            SettingsItem.TYPE_SINGLE_CHOICE, SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
+                SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this)
+            }
+
+            SettingsItem.TYPE_SLIDER -> {
+                SliderViewHolder(ListItemSettingBinding.inflate(inflater), this)
+            }
+
+            SettingsItem.TYPE_SUBMENU -> {
+                SubmenuViewHolder(ListItemSettingBinding.inflate(inflater), this)
+            }
+
+            SettingsItem.TYPE_DATETIME_SETTING -> {
+                DateTimeViewHolder(ListItemSettingBinding.inflate(inflater), this)
+            }
+
+            SettingsItem.TYPE_RUNNABLE -> {
+                RunnableViewHolder(ListItemSettingBinding.inflate(inflater), this)
+            }
+
+            SettingsItem.TYPE_INPUT_BINDING -> {
+                InputBindingSettingViewHolder(ListItemSettingBinding.inflate(inflater), this)
+            }
+
+            SettingsItem.TYPE_STRING_INPUT -> {
+                StringInputViewHolder(ListItemSettingBinding.inflate(inflater), this)
+            }
+
+            else -> {
+                // TODO: Create an error view since we can't return null now
+                HeaderViewHolder(ListItemSettingsHeaderBinding.inflate(inflater), this)
+            }
+        }
+    }
+
+    override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
+        holder.bind(getItem(position))
+    }
+
+    private fun getItem(position: Int): SettingsItem {
+        return settings!![position]
+    }
+
+    override fun getItemCount(): Int {
+        return if (settings != null) {
+            settings!!.size
+        } else {
+            0
+        }
+    }
+
+    override fun getItemViewType(position: Int): Int {
+        return getItem(position).type
+    }
+
+    fun setSettingsList(settings: ArrayList<SettingsItem>?) {
+        this.settings = settings
+        notifyDataSetChanged()
+    }
+
+    fun onBooleanClick(item: SwitchSetting, position: Int, checked: Boolean) {
+        val setting = item.setChecked(checked)
+        fragmentView.putSetting(setting)
+        fragmentView.onSettingChanged()
+    }
+
+    private fun onSingleChoiceClick(item: SingleChoiceSetting) {
+        clickedItem = item
+        val value = getSelectionForSingleChoiceValue(item)
+        dialog = MaterialAlertDialogBuilder(context)
+            .setTitle(item.nameId)
+            .setSingleChoiceItems(item.choicesId, value, this)
+            .show()
+    }
+
+    fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
+        clickedPosition = position
+        onSingleChoiceClick(item)
+    }
+
+    private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) {
+        clickedItem = item
+        dialog = MaterialAlertDialogBuilder(context)
+            .setTitle(item.nameId)
+            .setSingleChoiceItems(item.choices, item.selectValueIndex, this)
+            .show()
+    }
+
+    fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) {
+        clickedPosition = position
+        onStringSingleChoiceClick(item)
+    }
+
+    @SuppressLint("SimpleDateFormat")
+    fun onDateTimeClick(item: DateTimeSetting, position: Int) {
+        clickedItem = item
+        clickedPosition = position
+
+        val storedTime: Long = try {
+            java.lang.Long.decode(item.value) * 1000
+        } catch (e: NumberFormatException) {
+            val date = item.value.substringBefore(" ")
+            val time = item.value.substringAfter(" ")
+
+            val formatter = SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssZZZZ")
+            val gmt = formatter.parse("${date}T${time}+0000")
+            gmt!!.time
+        }
+
+        // Helper to extract hour and minute from epoch time
+        val calendar: Calendar = Calendar.getInstance()
+        calendar.timeInMillis = storedTime
+        calendar.timeZone = TimeZone.getTimeZone("UTC")
+
+        var timeFormat: Int = TimeFormat.CLOCK_12H
+        if (DateFormat.is24HourFormat(fragmentView.activityView as AppCompatActivity)) {
+            timeFormat = TimeFormat.CLOCK_24H
+        }
+
+        val datePicker: MaterialDatePicker<Long> = MaterialDatePicker.Builder.datePicker()
+            .setSelection(storedTime)
+            .setTitleText(R.string.select_rtc_date)
+            .build()
+        val timePicker: MaterialTimePicker = MaterialTimePicker.Builder()
+            .setTimeFormat(timeFormat)
+            .setHour(calendar.get(Calendar.HOUR_OF_DAY))
+            .setMinute(calendar.get(Calendar.MINUTE))
+            .setTitleText(R.string.select_rtc_time)
+            .build()
+
+        datePicker.addOnPositiveButtonClickListener {
+            timePicker.show(
+                (fragmentView.activityView as AppCompatActivity).supportFragmentManager,
+                "TimePicker"
+            )
+        }
+        timePicker.addOnPositiveButtonClickListener {
+            var epochTime: Long = datePicker.selection!! / 1000
+            epochTime += timePicker.hour.toLong() * 60 * 60
+            epochTime += timePicker.minute.toLong() * 60
+            val rtcString = epochTime.toString()
+            if (item.value != rtcString) {
+                fragmentView.onSettingChanged()
+            }
+            notifyItemChanged(clickedPosition)
+            val setting = item.setSelectedValue(rtcString)
+            fragmentView.putSetting(setting)
+            clickedItem = null
+        }
+        datePicker.show(
+            (fragmentView.activityView as AppCompatActivity).supportFragmentManager,
+            "DatePicker"
+        )
+    }
+
+    fun onSliderClick(item: SliderSetting, position: Int) {
+        clickedItem = item
+        clickedPosition = position
+        sliderProgress = item.selectedValue
+
+        val inflater = LayoutInflater.from(context)
+        val sliderBinding = DialogSliderBinding.inflate(inflater)
+
+        textSliderValue = sliderBinding.textValue
+        textSliderValue!!.text = sliderProgress.toString()
+        sliderBinding.textUnits.text = item.units
+
+        sliderBinding.slider.apply {
+            valueFrom = item.min.toFloat()
+            valueTo = item.max.toFloat()
+            value = sliderProgress.toFloat()
+            addOnChangeListener { _: Slider, value: Float, _: Boolean ->
+                sliderProgress = value.toInt()
+                textSliderValue!!.text = sliderProgress.toString()
+            }
+        }
+
+        dialog = MaterialAlertDialogBuilder(context)
+            .setTitle(item.nameId)
+            .setView(sliderBinding.root)
+            .setPositiveButton(android.R.string.ok, this)
+            .setNegativeButton(android.R.string.cancel, defaultCancelListener)
+            .setNeutralButton(R.string.slider_default) { dialog: DialogInterface, which: Int ->
+                sliderBinding.slider.value = when (item.setting) {
+                    is ScaledFloatSetting -> {
+                        val scaledSetting = item.setting as ScaledFloatSetting
+                        scaledSetting.defaultValue * scaledSetting.scale
+                    }
+
+                    is FloatSetting -> (item.setting as FloatSetting).defaultValue
+                    else -> item.defaultValue!!
+                }
+                onClick(dialog, which)
+            }
+            .show()
+    }
+
+    fun onSubmenuClick(item: SubmenuSetting) {
+        fragmentView.loadSubMenu(item.menuKey)
+    }
+
+    fun onInputBindingClick(item: InputBindingSetting, position: Int) {
+        val activity = fragmentView.activityView as FragmentActivity
+        MotionBottomSheetDialogFragment.newInstance(
+            item,
+            { closeDialog() },
+            {
+                notifyItemChanged(position)
+                fragmentView.onSettingChanged()
+            }
+        ).show(activity.supportFragmentManager, MotionBottomSheetDialogFragment.TAG)
+    }
+
+    fun onStringInputClick(item: StringInputSetting, position: Int) {
+        clickedItem = item
+        clickedPosition = position
+        textInputValue = item.selectedValue
+
+        val inflater = LayoutInflater.from(context)
+        val inputBinding = DialogSoftwareKeyboardBinding.inflate(inflater)
+
+        inputBinding.editTextInput.setText(textInputValue)
+        inputBinding.editTextInput.doOnTextChanged { text, _, _, _ ->
+            textInputValue = text.toString()
+        }
+        if (item.characterLimit != 0) {
+            inputBinding.editTextInput.filters =
+                arrayOf(InputFilter.LengthFilter(item.characterLimit))
+        }
+
+        dialog = MaterialAlertDialogBuilder(context)
+            .setView(inputBinding.root)
+            .setTitle(item.nameId)
+            .setPositiveButton(android.R.string.ok, this)
+            .setNegativeButton(android.R.string.cancel, defaultCancelListener)
+            .show()
+    }
+
+    override fun onClick(dialog: DialogInterface, which: Int) {
+        when (clickedItem) {
+            is SingleChoiceSetting -> {
+                val scSetting = clickedItem as SingleChoiceSetting
+                val setting = when (scSetting.setting) {
+                    is AbstractIntSetting -> {
+                        val value = getValueForSingleChoiceSelection(scSetting, which)
+                        if (scSetting.selectedValue != value) {
+                            fragmentView.onSettingChanged()
+                        }
+                        scSetting.setSelectedValue(value)
+                    }
+
+                    is AbstractShortSetting -> {
+                        val value = getValueForSingleChoiceSelection(scSetting, which).toShort()
+                        if (scSetting.selectedValue.toShort() != value) {
+                            fragmentView.onSettingChanged()
+                        }
+                        scSetting.setSelectedValue(value)
+                    }
+
+                    else -> throw IllegalStateException("Unrecognized type used for SingleChoiceSetting!")
+                }
+
+                fragmentView.putSetting(setting)
+                closeDialog()
+            }
+
+            is StringSingleChoiceSetting -> {
+                val scSetting = clickedItem as StringSingleChoiceSetting
+                val setting = when (scSetting.setting) {
+                    is AbstractStringSetting -> {
+                        val value = scSetting.getValueAt(which)
+                        if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
+                        scSetting.setSelectedValue(value!!)
+                    }
+
+                    is AbstractShortSetting -> {
+                        if (scSetting.selectValueIndex != which) fragmentView.onSettingChanged()
+                        scSetting.setSelectedValue(scSetting.getValueAt(which)?.toShort() ?: 1)
+                    }
+
+                    else -> throw IllegalStateException("Unrecognized type used for StringSingleChoiceSetting!")
+                }
+
+                fragmentView.putSetting(setting)
+                closeDialog()
+            }
+
+            is SliderSetting -> {
+                val sliderSetting = clickedItem as SliderSetting
+                if (sliderSetting.selectedValue != sliderProgress) {
+                    fragmentView.onSettingChanged()
+                }
+                when (sliderSetting.setting) {
+                    is FloatSetting,
+                    is ScaledFloatSetting -> {
+                        val value = sliderProgress.toFloat()
+                        val setting = sliderSetting.setSelectedValue(value)
+                        fragmentView.putSetting(setting)
+                    }
+
+                    else -> {
+                        val setting = sliderSetting.setSelectedValue(sliderProgress)
+                        fragmentView.putSetting(setting)
+                    }
+                }
+                closeDialog()
+            }
+
+            is StringInputSetting -> {
+                val inputSetting = clickedItem as StringInputSetting
+                if (inputSetting.selectedValue != textInputValue) {
+                    fragmentView.onSettingChanged()
+                }
+                val setting = inputSetting.setSelectedValue(textInputValue)
+                fragmentView.putSetting(setting)
+                closeDialog()
+            }
+        }
+        clickedItem = null
+        sliderProgress = -1
+        textInputValue = ""
+    }
+
+    fun onLongClick(setting: AbstractSetting, position: Int): Boolean {
+        MaterialAlertDialogBuilder(context)
+            .setMessage(R.string.reset_setting_confirmation)
+            .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
+                when (setting) {
+                    is AbstractBooleanSetting -> setting.boolean = setting.defaultValue as Boolean
+                    is AbstractFloatSetting -> {
+                        if (setting is ScaledFloatSetting) {
+                            setting.float = setting.defaultValue * setting.scale
+                        } else {
+                            setting.float = setting.defaultValue as Float
+                        }
+                    }
+
+                    is AbstractIntSetting -> setting.int = setting.defaultValue as Int
+                    is AbstractStringSetting -> setting.string = setting.defaultValue as String
+                    is AbstractShortSetting -> setting.short = setting.defaultValue as Short
+                }
+                notifyItemChanged(position)
+                fragmentView.onSettingChanged()
+            }
+            .setNegativeButton(android.R.string.cancel, null)
+            .show()
+
+        return true
+    }
+
+    fun onClickDisabledSetting() {
+        MessageDialogFragment.newInstance(
+            R.string.setting_not_editable,
+            R.string.setting_not_editable_description
+        ).show((fragmentView as SettingsFragment).childFragmentManager, MessageDialogFragment.TAG)
+    }
+
+    fun onClickRegenerateConsoleId() {
+        MaterialAlertDialogBuilder(context)
+            .setTitle(R.string.regenerate_console_id)
+            .setMessage(R.string.regenerate_console_id_description)
+            .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
+                SystemSaveGame.regenerateConsoleId()
+                notifyDataSetChanged()
+            }
+            .setNegativeButton(android.R.string.cancel, null)
+            .show()
+    }
+
+    fun closeDialog() {
+        if (dialog != null) {
+            if (clickedPosition != -1) {
+                notifyItemChanged(clickedPosition)
+                clickedPosition = -1
+            }
+            dialog!!.dismiss()
+            dialog = null
+        }
+    }
+
+    private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int {
+        val valuesId = item.valuesId
+        return if (valuesId > 0) {
+            val valuesArray = context.resources.getIntArray(valuesId)
+            valuesArray[which]
+        } else {
+            which
+        }
+    }
+
+    private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
+        val value = item.selectedValue
+        val valuesId = item.valuesId
+        if (valuesId > 0) {
+            val valuesArray = context.resources.getIntArray(valuesId)
+            for (index in valuesArray.indices) {
+                val current = valuesArray[index]
+                if (current == value) {
+                    return index
+                }
+            }
+        } else {
+            return value
+        }
+        return -1
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.java
deleted file mode 100644
index 76d4223f5c..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.java
+++ /dev/null
@@ -1,151 +0,0 @@
-package org.citra.citra_emu.features.settings.ui;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.graphics.Insets;
-import androidx.core.view.ViewCompat;
-import androidx.core.view.WindowInsetsCompat;
-import androidx.fragment.app.Fragment;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.settings.model.Setting;
-import org.citra.citra_emu.features.settings.model.Settings;
-import org.citra.citra_emu.features.settings.model.view.SettingsItem;
-import org.citra.citra_emu.ui.DividerItemDecoration;
-
-import java.util.ArrayList;
-
-public final class SettingsFragment extends Fragment implements SettingsFragmentView {
-    private static final String ARGUMENT_MENU_TAG = "menu_tag";
-    private static final String ARGUMENT_GAME_ID = "game_id";
-
-    private SettingsFragmentPresenter mPresenter = new SettingsFragmentPresenter(this);
-    private SettingsActivityView mActivity;
-
-    private SettingsAdapter mAdapter;
-
-    private RecyclerView mRecyclerView;
-
-    public static Fragment newInstance(String menuTag, String gameId) {
-        SettingsFragment fragment = new SettingsFragment();
-
-        Bundle arguments = new Bundle();
-        arguments.putString(ARGUMENT_MENU_TAG, menuTag);
-        arguments.putString(ARGUMENT_GAME_ID, gameId);
-
-        fragment.setArguments(arguments);
-        return fragment;
-    }
-
-    @Override
-    public void onAttach(@NonNull Context context) {
-        super.onAttach(context);
-
-        mActivity = (SettingsActivityView) context;
-        mPresenter.onAttach();
-    }
-
-    @Override
-    public void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-
-        setRetainInstance(true);
-        String menuTag = getArguments().getString(ARGUMENT_MENU_TAG);
-        String gameId = getArguments().getString(ARGUMENT_GAME_ID);
-
-        mAdapter = new SettingsAdapter(this, getActivity());
-
-        mPresenter.onCreate(menuTag, gameId);
-    }
-
-    @Nullable
-    @Override
-    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
-        return inflater.inflate(R.layout.fragment_settings, container, false);
-    }
-
-    @Override
-    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
-        LinearLayoutManager manager = new LinearLayoutManager(getActivity());
-
-        mRecyclerView = view.findViewById(R.id.list_settings);
-
-        mRecyclerView.setAdapter(mAdapter);
-        mRecyclerView.setLayoutManager(manager);
-        mRecyclerView.addItemDecoration(new DividerItemDecoration(getActivity(), null));
-
-        SettingsActivityView activity = (SettingsActivityView) getActivity();
-
-        mPresenter.onViewCreated(activity.getSettings());
-
-        setInsets();
-    }
-
-    @Override
-    public void onDetach() {
-        super.onDetach();
-        mActivity = null;
-
-        if (mAdapter != null) {
-            mAdapter.closeDialog();
-        }
-    }
-
-    @Override
-    public void onSettingsFileLoaded(Settings settings) {
-        mPresenter.setSettings(settings);
-    }
-
-    @Override
-    public void passSettingsToActivity(Settings settings) {
-        if (mActivity != null) {
-            mActivity.setSettings(settings);
-        }
-    }
-
-    @Override
-    public void showSettingsList(ArrayList<SettingsItem> settingsList) {
-        mAdapter.setSettings(settingsList);
-    }
-
-    @Override
-    public void loadDefaultSettings() {
-        mPresenter.loadDefaultSettings();
-    }
-
-    @Override
-    public void loadSubMenu(String menuKey) {
-        mActivity.showSettingsFragment(menuKey, true, getArguments().getString(ARGUMENT_GAME_ID));
-    }
-
-    @Override
-    public void showToastMessage(String message, boolean is_long) {
-        mActivity.showToastMessage(message, is_long);
-    }
-
-    @Override
-    public void putSetting(Setting setting) {
-        mPresenter.putSetting(setting);
-    }
-
-    @Override
-    public void onSettingChanged() {
-        mActivity.onSettingChanged();
-    }
-
-    private void setInsets() {
-        ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> {
-            Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
-            v.setPadding(insets.left, 0, insets.right, insets.bottom);
-            return windowInsets;
-        });
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt
new file mode 100644
index 0000000000..77458cd545
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragment.kt
@@ -0,0 +1,128 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.ui
+
+import android.content.Context
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.fragment.app.Fragment
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.divider.MaterialDividerItemDecoration
+import org.citra.citra_emu.databinding.FragmentSettingsBinding
+import org.citra.citra_emu.features.settings.model.AbstractSetting
+import org.citra.citra_emu.features.settings.model.view.SettingsItem
+
+class SettingsFragment : Fragment(), SettingsFragmentView {
+    override var activityView: SettingsActivityView? = null
+
+    private val fragmentPresenter = SettingsFragmentPresenter(this)
+    private var settingsAdapter: SettingsAdapter? = null
+
+    private var _binding: FragmentSettingsBinding? = null
+    private val binding get() = _binding!!
+
+    override fun onAttach(context: Context) {
+        super.onAttach(context)
+        activityView = requireActivity() as SettingsActivityView
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        val menuTag = requireArguments().getString(ARGUMENT_MENU_TAG)
+        val gameId = requireArguments().getString(ARGUMENT_GAME_ID)
+        fragmentPresenter.onCreate(menuTag!!, gameId!!)
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        _binding = FragmentSettingsBinding.inflate(layoutInflater)
+        return binding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        settingsAdapter = SettingsAdapter(this, requireActivity())
+        val dividerDecoration = MaterialDividerItemDecoration(
+            requireContext(),
+            LinearLayoutManager.VERTICAL
+        )
+        dividerDecoration.isLastItemDecorated = false
+        binding.listSettings.apply {
+            adapter = settingsAdapter
+            layoutManager = LinearLayoutManager(activity)
+            addItemDecoration(dividerDecoration)
+        }
+        fragmentPresenter.onViewCreated(settingsAdapter!!)
+
+        setInsets()
+    }
+
+    override fun onDetach() {
+        super.onDetach()
+        activityView = null
+        if (settingsAdapter != null) {
+            settingsAdapter!!.closeDialog()
+        }
+    }
+
+    override fun showSettingsList(settingsList: ArrayList<SettingsItem>) {
+        settingsAdapter!!.setSettingsList(settingsList)
+    }
+
+    override fun loadSettingsList() {
+        fragmentPresenter.loadSettingsList()
+    }
+
+    override fun loadSubMenu(menuKey: String) {
+        activityView!!.showSettingsFragment(
+            menuKey,
+            true,
+            requireArguments().getString(ARGUMENT_GAME_ID)!!
+        )
+    }
+
+    override fun showToastMessage(message: String?, is_long: Boolean) {
+        activityView!!.showToastMessage(message!!, is_long)
+    }
+
+    override fun putSetting(setting: AbstractSetting) {
+        fragmentPresenter.putSetting(setting)
+    }
+
+    override fun onSettingChanged() {
+        activityView!!.onSettingChanged()
+    }
+
+    private fun setInsets() {
+        ViewCompat.setOnApplyWindowInsetsListener(
+            binding.listSettings
+        ) { view: View, windowInsets: WindowInsetsCompat ->
+            val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+            view.updatePadding(bottom = insets.bottom)
+            windowInsets
+        }
+    }
+
+    companion object {
+        private const val ARGUMENT_MENU_TAG = "menu_tag"
+        private const val ARGUMENT_GAME_ID = "game_id"
+
+        fun newInstance(menuTag: String?, gameId: String?): Fragment {
+            val fragment = SettingsFragment()
+            val arguments = Bundle()
+            arguments.putString(ARGUMENT_MENU_TAG, menuTag)
+            arguments.putString(ARGUMENT_GAME_ID, gameId)
+            fragment.arguments = arguments
+            return fragment
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java
deleted file mode 100644
index 3e53cf4658..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.java
+++ /dev/null
@@ -1,410 +0,0 @@
-package org.citra.citra_emu.features.settings.ui;
-
-import android.app.Activity;
-import android.content.Context;
-import android.hardware.camera2.CameraAccessException;
-import android.hardware.camera2.CameraCharacteristics;
-import android.hardware.camera2.CameraManager;
-import android.text.TextUtils;
-
-import org.citra.citra_emu.NativeLibrary;
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.settings.model.Setting;
-import org.citra.citra_emu.features.settings.model.SettingSection;
-import org.citra.citra_emu.features.settings.model.Settings;
-import org.citra.citra_emu.features.settings.model.StringSetting;
-import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
-import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
-import org.citra.citra_emu.features.settings.model.view.HeaderSetting;
-import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
-import org.citra.citra_emu.features.settings.model.view.SettingsItem;
-import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
-import org.citra.citra_emu.features.settings.model.view.SliderSetting;
-import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
-import org.citra.citra_emu.features.settings.model.view.SubmenuSetting;
-import org.citra.citra_emu.features.settings.utils.SettingsFile;
-import org.citra.citra_emu.utils.Log;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Objects;
-
-public final class SettingsFragmentPresenter {
-    private SettingsFragmentView mView;
-
-    private String mMenuTag;
-    private String mGameID;
-
-    private Settings mSettings;
-    private ArrayList<SettingsItem> mSettingsList;
-
-    public SettingsFragmentPresenter(SettingsFragmentView view) {
-        mView = view;
-    }
-
-    public void onCreate(String menuTag, String gameId) {
-        mGameID = gameId;
-        mMenuTag = menuTag;
-    }
-
-    public void onViewCreated(Settings settings) {
-        setSettings(settings);
-    }
-
-    /**
-     * If the screen is rotated, the Activity will forget the settings map. This fragment
-     * won't, though; so rather than have the Activity reload from disk, have the fragment pass
-     * the settings map back to the Activity.
-     */
-    public void onAttach() {
-        if (mSettings != null) {
-            mView.passSettingsToActivity(mSettings);
-        }
-    }
-
-    public void putSetting(Setting setting) {
-        mSettings.getSection(setting.getSection()).putSetting(setting);
-    }
-
-    private StringSetting asStringSetting(Setting setting) {
-        if (setting == null) {
-            return null;
-        }
-
-        StringSetting stringSetting = new StringSetting(setting.getKey(), setting.getSection(), setting.getValueAsString());
-        putSetting(stringSetting);
-        return stringSetting;
-    }
-
-    public void loadDefaultSettings() {
-        loadSettingsList();
-    }
-
-    public void setSettings(Settings settings) {
-        if (mSettingsList == null && settings != null) {
-            mSettings = settings;
-
-            loadSettingsList();
-        } else {
-            mView.getActivity().setTitle(R.string.preferences_settings);
-            mView.showSettingsList(mSettingsList);
-        }
-    }
-
-    private void loadSettingsList() {
-        if (!TextUtils.isEmpty(mGameID)) {
-            mView.getActivity().setTitle("Game Settings: " + mGameID);
-        }
-        ArrayList<SettingsItem> sl = new ArrayList<>();
-
-        if (mMenuTag == null) {
-            return;
-        }
-
-        switch (mMenuTag) {
-            case SettingsFile.FILE_NAME_CONFIG:
-                addConfigSettings(sl);
-                break;
-            case Settings.SECTION_CORE:
-                addGeneralSettings(sl);
-                break;
-            case Settings.SECTION_SYSTEM:
-                addSystemSettings(sl);
-                break;
-            case Settings.SECTION_CAMERA:
-                addCameraSettings(sl);
-                break;
-            case Settings.SECTION_CONTROLS:
-                addInputSettings(sl);
-                break;
-            case Settings.SECTION_RENDERER:
-                addGraphicsSettings(sl);
-                break;
-            case Settings.SECTION_AUDIO:
-                addAudioSettings(sl);
-                break;
-            case Settings.SECTION_DEBUG:
-                addDebugSettings(sl);
-                break;
-            default:
-                mView.showToastMessage("Unimplemented menu", false);
-                return;
-        }
-
-        mSettingsList = sl;
-        mView.showSettingsList(mSettingsList);
-    }
-
-    private void addConfigSettings(ArrayList<SettingsItem> sl) {
-        mView.getActivity().setTitle(R.string.preferences_settings);
-
-        sl.add(new SubmenuSetting(null, null, R.string.preferences_general, 0, Settings.SECTION_CORE));
-        sl.add(new SubmenuSetting(null, null, R.string.preferences_system, 0, Settings.SECTION_SYSTEM));
-        sl.add(new SubmenuSetting(null, null, R.string.preferences_camera, 0, Settings.SECTION_CAMERA));
-        sl.add(new SubmenuSetting(null, null, R.string.preferences_controls, 0, Settings.SECTION_CONTROLS));
-        sl.add(new SubmenuSetting(null, null, R.string.preferences_graphics, 0, Settings.SECTION_RENDERER));
-        sl.add(new SubmenuSetting(null, null, R.string.preferences_audio, 0, Settings.SECTION_AUDIO));
-        sl.add(new SubmenuSetting(null, null, R.string.preferences_debug, 0, Settings.SECTION_DEBUG));
-    }
-
-    private void addGeneralSettings(ArrayList<SettingsItem> sl) {
-        mView.getActivity().setTitle(R.string.preferences_general);
-
-        SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
-        Setting frameLimitEnable = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED);
-        Setting frameLimitValue = rendererSection.getSetting(SettingsFile.KEY_FRAME_LIMIT);
-
-        sl.add(new CheckBoxSetting(SettingsFile.KEY_FRAME_LIMIT_ENABLED, Settings.SECTION_RENDERER, R.string.frame_limit_enable, R.string.frame_limit_enable_description, true, frameLimitEnable));
-        sl.add(new SliderSetting(SettingsFile.KEY_FRAME_LIMIT, Settings.SECTION_RENDERER, R.string.frame_limit_slider, R.string.frame_limit_slider_description, 1, 200, "%", 100, frameLimitValue));
-    }
-
-    private void addSystemSettings(ArrayList<SettingsItem> sl) {
-        mView.getActivity().setTitle(R.string.preferences_system);
-
-        SettingSection systemSection = mSettings.getSection(Settings.SECTION_SYSTEM);
-        Setting region = systemSection.getSetting(SettingsFile.KEY_REGION_VALUE);
-        Setting language = systemSection.getSetting(SettingsFile.KEY_LANGUAGE);
-        Setting systemClock = systemSection.getSetting(SettingsFile.KEY_INIT_CLOCK);
-        Setting dateTime = systemSection.getSetting(SettingsFile.KEY_INIT_TIME);
-        Setting pluginLoader = systemSection.getSetting(SettingsFile.KEY_PLUGIN_LOADER);
-        Setting allowPluginLoader = systemSection.getSetting(SettingsFile.KEY_ALLOW_PLUGIN_LOADER);
-
-        sl.add(new SingleChoiceSetting(SettingsFile.KEY_REGION_VALUE, Settings.SECTION_SYSTEM, R.string.emulated_region, 0, R.array.regionNames, R.array.regionValues, -1, region));
-        sl.add(new SingleChoiceSetting(SettingsFile.KEY_LANGUAGE, Settings.SECTION_SYSTEM, R.string.emulated_language, 0, R.array.languageNames, R.array.languageValues, 1, language));
-
-        sl.add(new HeaderSetting(null, null, R.string.clock, 0));
-        sl.add(new SingleChoiceSetting(SettingsFile.KEY_INIT_CLOCK, Settings.SECTION_SYSTEM, R.string.init_clock, R.string.init_clock_description, R.array.systemClockNames, R.array.systemClockValues, 0, systemClock));
-        sl.add(new DateTimeSetting(SettingsFile.KEY_INIT_TIME, Settings.SECTION_SYSTEM, R.string.init_time, R.string.init_time_description, "2000-01-01 00:00:01", dateTime));
-
-        sl.add(new HeaderSetting(null, null, R.string.plugin_loader, 0));
-        sl.add(new CheckBoxSetting(SettingsFile.KEY_PLUGIN_LOADER, Settings.SECTION_SYSTEM, R.string.plugin_loader, R.string.plugin_loader_description, false, pluginLoader));
-        sl.add(new CheckBoxSetting(SettingsFile.KEY_ALLOW_PLUGIN_LOADER, Settings.SECTION_SYSTEM, R.string.allow_plugin_loader, R.string.allow_plugin_loader_description, true, allowPluginLoader));
-    }
-
-    private void addCameraSettings(ArrayList<SettingsItem> sl) {
-        final Activity activity = mView.getActivity();
-        activity.setTitle(R.string.preferences_camera);
-
-        // Get the camera IDs
-        CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
-        ArrayList<String> supportedCameraNameList = new ArrayList<>();
-        ArrayList<String> supportedCameraIdList = new ArrayList<>();
-        if (cameraManager != null) {
-            try {
-                for (String id : cameraManager.getCameraIdList()) {
-                    final CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
-                    if (Objects.requireNonNull(characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
-                        continue; // Legacy cameras cannot be used with the NDK
-                    }
-
-                    supportedCameraIdList.add(id);
-
-                    final int facing = Objects.requireNonNull(characteristics.get(CameraCharacteristics.LENS_FACING));
-                    int stringId = R.string.camera_facing_external;
-                    switch (facing) {
-                        case CameraCharacteristics.LENS_FACING_FRONT:
-                            stringId = R.string.camera_facing_front;
-                            break;
-                        case CameraCharacteristics.LENS_FACING_BACK:
-                            stringId = R.string.camera_facing_back;
-                            break;
-                        case CameraCharacteristics.LENS_FACING_EXTERNAL:
-                            stringId = R.string.camera_facing_external;
-                            break;
-                    }
-                    supportedCameraNameList.add(String.format("%1$s (%2$s)", id, activity.getString(stringId)));
-                }
-            } catch (CameraAccessException e) {
-                Log.error("Couldn't retrieve camera list");
-                e.printStackTrace();
-            }
-        }
-
-        // Create the names and values for display
-        ArrayList<String> cameraDeviceNameList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceNames)));
-        cameraDeviceNameList.addAll(supportedCameraNameList);
-        ArrayList<String> cameraDeviceValueList = new ArrayList<>(Arrays.asList(activity.getResources().getStringArray(R.array.cameraDeviceValues)));
-        cameraDeviceValueList.addAll(supportedCameraIdList);
-
-        final String[] cameraDeviceNames = cameraDeviceNameList.toArray(new String[]{});
-        final String[] cameraDeviceValues = cameraDeviceValueList.toArray(new String[]{});
-
-        final boolean haveCameraDevices = !supportedCameraIdList.isEmpty();
-
-        String[] imageSourceNames = activity.getResources().getStringArray(R.array.cameraImageSourceNames);
-        String[] imageSourceValues = activity.getResources().getStringArray(R.array.cameraImageSourceValues);
-        if (!haveCameraDevices) {
-            // Remove the last entry (ndk / Device Camera)
-            imageSourceNames = Arrays.copyOfRange(imageSourceNames, 0, imageSourceNames.length - 1);
-            imageSourceValues = Arrays.copyOfRange(imageSourceValues, 0, imageSourceValues.length - 1);
-        }
-
-        final String defaultImageSource = haveCameraDevices ? "ndk" : "image";
-
-        SettingSection cameraSection = mSettings.getSection(Settings.SECTION_CAMERA);
-
-        Setting innerCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_NAME);
-        Setting innerCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG));
-        Setting innerCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_INNER_FLIP);
-        sl.add(new HeaderSetting(null, null, R.string.inner_camera, 0));
-        sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, innerCameraImageSource));
-        if (haveCameraDevices)
-            sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_front", innerCameraConfig));
-        sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_INNER_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, innerCameraFlip));
-
-        Setting outerLeftCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME);
-        Setting outerLeftCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG));
-        Setting outerLeftCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP);
-        sl.add(new HeaderSetting(null, null, R.string.outer_left_camera, 0));
-        sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerLeftCameraImageSource));
-        if (haveCameraDevices)
-            sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerLeftCameraConfig));
-        sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_LEFT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerLeftCameraFlip));
-
-        Setting outerRightCameraImageSource = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME);
-        Setting outerRightCameraConfig = asStringSetting(cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG));
-        Setting outerRightCameraFlip = cameraSection.getSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP);
-        sl.add(new HeaderSetting(null, null, R.string.outer_right_camera, 0));
-        sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_NAME, Settings.SECTION_CAMERA, R.string.image_source, R.string.image_source_description, imageSourceNames, imageSourceValues, defaultImageSource, outerRightCameraImageSource));
-        if (haveCameraDevices)
-            sl.add(new StringSingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_CONFIG, Settings.SECTION_CAMERA, R.string.camera_device, R.string.camera_device_description, cameraDeviceNames, cameraDeviceValues, "_back", outerRightCameraConfig));
-        sl.add(new SingleChoiceSetting(SettingsFile.KEY_CAMERA_OUTER_RIGHT_FLIP, Settings.SECTION_CAMERA, R.string.image_flip, 0, R.array.cameraFlipNames, R.array.cameraFlipValues, 0, outerRightCameraFlip));
-    }
-
-    private void addInputSettings(ArrayList<SettingsItem> sl) {
-        mView.getActivity().setTitle(R.string.preferences_controls);
-
-        SettingSection controlsSection = mSettings.getSection(Settings.SECTION_CONTROLS);
-        Setting buttonA = controlsSection.getSetting(SettingsFile.KEY_BUTTON_A);
-        Setting buttonB = controlsSection.getSetting(SettingsFile.KEY_BUTTON_B);
-        Setting buttonX = controlsSection.getSetting(SettingsFile.KEY_BUTTON_X);
-        Setting buttonY = controlsSection.getSetting(SettingsFile.KEY_BUTTON_Y);
-        Setting buttonSelect = controlsSection.getSetting(SettingsFile.KEY_BUTTON_SELECT);
-        Setting buttonStart = controlsSection.getSetting(SettingsFile.KEY_BUTTON_START);
-        Setting circlepadAxisVert = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL);
-        Setting circlepadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL);
-        Setting cstickAxisVert = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL);
-        Setting cstickAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL);
-        Setting dpadAxisVert = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL);
-        Setting dpadAxisHoriz = controlsSection.getSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL);
-        // Setting buttonUp = controlsSection.getSetting(SettingsFile.KEY_BUTTON_UP);
-        // Setting buttonDown = controlsSection.getSetting(SettingsFile.KEY_BUTTON_DOWN);
-        // Setting buttonLeft = controlsSection.getSetting(SettingsFile.KEY_BUTTON_LEFT);
-        // Setting buttonRight = controlsSection.getSetting(SettingsFile.KEY_BUTTON_RIGHT);
-        Setting buttonL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_L);
-        Setting buttonR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_R);
-        Setting buttonZL = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZL);
-        Setting buttonZR = controlsSection.getSetting(SettingsFile.KEY_BUTTON_ZR);
-
-        sl.add(new HeaderSetting(null, null, R.string.generic_buttons, 0));
-        sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_A, Settings.SECTION_CONTROLS, R.string.button_a, buttonA));
-        sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_B, Settings.SECTION_CONTROLS, R.string.button_b, buttonB));
-        sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_X, Settings.SECTION_CONTROLS, R.string.button_x, buttonX));
-        sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_Y, Settings.SECTION_CONTROLS, R.string.button_y, buttonY));
-        sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_SELECT, Settings.SECTION_CONTROLS, R.string.button_select, buttonSelect));
-        sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_START, Settings.SECTION_CONTROLS, R.string.button_start, buttonStart));
-
-        sl.add(new HeaderSetting(null, null, R.string.controller_circlepad, 0));
-        sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, circlepadAxisVert));
-        sl.add(new InputBindingSetting(SettingsFile.KEY_CIRCLEPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, circlepadAxisHoriz));
-
-        sl.add(new HeaderSetting(null, null, R.string.controller_c, 0));
-        sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, cstickAxisVert));
-        sl.add(new InputBindingSetting(SettingsFile.KEY_CSTICK_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, cstickAxisHoriz));
-
-        sl.add(new HeaderSetting(null, null, R.string.controller_dpad, 0));
-        sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_VERTICAL, Settings.SECTION_CONTROLS, R.string.controller_axis_vertical, dpadAxisVert));
-        sl.add(new InputBindingSetting(SettingsFile.KEY_DPAD_AXIS_HORIZONTAL, Settings.SECTION_CONTROLS, R.string.controller_axis_horizontal, dpadAxisHoriz));
-
-        // TODO(bunnei): Figure out what to do with these. Configuring is functional, but removing for MVP because they are confusing.
-        // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_UP, Settings.SECTION_CONTROLS, R.string.generic_up, buttonUp));
-        // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_DOWN, Settings.SECTION_CONTROLS, R.string.generic_down, buttonDown));
-        // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_LEFT, Settings.SECTION_CONTROLS, R.string.generic_left, buttonLeft));
-        // sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_RIGHT, Settings.SECTION_CONTROLS, R.string.generic_right, buttonRight));
-
-        sl.add(new HeaderSetting(null, null, R.string.controller_triggers, 0));
-        sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_L, Settings.SECTION_CONTROLS, R.string.button_l, buttonL));
-        sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_R, Settings.SECTION_CONTROLS, R.string.button_r, buttonR));
-        sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZL, Settings.SECTION_CONTROLS, R.string.button_zl, buttonZL));
-        sl.add(new InputBindingSetting(SettingsFile.KEY_BUTTON_ZR, Settings.SECTION_CONTROLS, R.string.button_zr, buttonZR));
-    }
-
-    private void addGraphicsSettings(ArrayList<SettingsItem> sl) {
-        mView.getActivity().setTitle(R.string.preferences_graphics);
-
-        SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
-        Setting graphicsApi = rendererSection.getSetting(SettingsFile.KEY_GRAPHICS_API);
-        Setting spirvShaderGen = rendererSection.getSetting(SettingsFile.KEY_SPIRV_SHADER_GEN);
-        Setting asyncShaders = rendererSection.getSetting(SettingsFile.KEY_ASYNC_SHADERS);
-        Setting resolutionFactor = rendererSection.getSetting(SettingsFile.KEY_RESOLUTION_FACTOR);
-        Setting filterMode = rendererSection.getSetting(SettingsFile.KEY_FILTER_MODE);
-        Setting shadersAccurateMul = rendererSection.getSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL);
-        Setting render3dMode = rendererSection.getSetting(SettingsFile.KEY_RENDER_3D);
-        Setting factor3d = rendererSection.getSetting(SettingsFile.KEY_FACTOR_3D);
-        Setting useDiskShaderCache = rendererSection.getSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE);
-        Setting textureFilterName = rendererSection.getSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME);
-        SettingSection layoutSection = mSettings.getSection(Settings.SECTION_LAYOUT);
-        Setting cardboardScreenSize = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE);
-        Setting cardboardXShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT);
-        Setting cardboardYShift = layoutSection.getSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT);
-        SettingSection utilitySection = mSettings.getSection(Settings.SECTION_UTILITY);
-        Setting dumpTextures = utilitySection.getSetting(SettingsFile.KEY_DUMP_TEXTURES);
-        Setting customTextures = utilitySection.getSetting(SettingsFile.KEY_CUSTOM_TEXTURES);
-        Setting asyncCustomLoading = utilitySection.getSetting(SettingsFile.KEY_ASYNC_CUSTOM_LOADING);
-        //Setting preloadTextures = utilitySection.getSetting(SettingsFile.KEY_PRELOAD_TEXTURES);
-
-        sl.add(new HeaderSetting(null, null, R.string.renderer, 0));
-        sl.add(new SingleChoiceSetting(SettingsFile.KEY_GRAPHICS_API, Settings.SECTION_RENDERER, R.string.graphics_api, 0, R.array.graphicsApiNames, R.array.graphicsApiValues, 0, graphicsApi));
-        sl.add(new CheckBoxSetting(SettingsFile.KEY_SPIRV_SHADER_GEN, Settings.SECTION_RENDERER, R.string.spirv_shader_gen, R.string.spirv_shader_gen_description, true, spirvShaderGen));
-        sl.add(new CheckBoxSetting(SettingsFile.KEY_ASYNC_SHADERS, Settings.SECTION_RENDERER, R.string.async_shaders, R.string.async_shaders_description, false, asyncShaders));
-        sl.add(new SliderSetting(SettingsFile.KEY_RESOLUTION_FACTOR, Settings.SECTION_RENDERER, R.string.internal_resolution, R.string.internal_resolution_description, 1, 4, "x", 1, resolutionFactor));
-        sl.add(new CheckBoxSetting(SettingsFile.KEY_FILTER_MODE, Settings.SECTION_RENDERER, R.string.linear_filtering, R.string.linear_filtering_description, true, filterMode));
-        sl.add(new CheckBoxSetting(SettingsFile.KEY_SHADERS_ACCURATE_MUL, Settings.SECTION_RENDERER, R.string.shaders_accurate_mul, R.string.shaders_accurate_mul_description, false, shadersAccurateMul));
-        sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_DISK_SHADER_CACHE, Settings.SECTION_RENDERER, R.string.use_disk_shader_cache, R.string.use_disk_shader_cache_description, true, useDiskShaderCache));
-        sl.add(new SingleChoiceSetting(SettingsFile.KEY_TEXTURE_FILTER_NAME, Settings.SECTION_RENDERER, R.string.texture_filter_name, 0, R.array.textureFilterNames, R.array.textureFilterValues, 0, textureFilterName));
-
-        sl.add(new HeaderSetting(null, null, R.string.stereoscopy, 0));
-        sl.add(new SingleChoiceSetting(SettingsFile.KEY_RENDER_3D, Settings.SECTION_RENDERER, R.string.render3d, 0, R.array.render3dModes, R.array.render3dValues, 0, render3dMode));
-        sl.add(new SliderSetting(SettingsFile.KEY_FACTOR_3D, Settings.SECTION_RENDERER, R.string.factor3d, R.string.factor3d_description, 0, 100, "%", 0, factor3d));
-
-        sl.add(new HeaderSetting(null, null, R.string.cardboard_vr, 0));
-        sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_SCREEN_SIZE, Settings.SECTION_LAYOUT, R.string.cardboard_screen_size, R.string.cardboard_screen_size_description, 30, 100, "%", 85, cardboardScreenSize));
-        sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_X_SHIFT, Settings.SECTION_LAYOUT, R.string.cardboard_x_shift, R.string.cardboard_x_shift_description, -100, 100, "%", 0, cardboardXShift));
-        sl.add(new SliderSetting(SettingsFile.KEY_CARDBOARD_Y_SHIFT, Settings.SECTION_LAYOUT, R.string.cardboard_y_shift, R.string.cardboard_y_shift_description, -100, 100, "%", 0, cardboardYShift));
-
-        sl.add(new HeaderSetting(null, null, R.string.utility, 0));
-        sl.add(new CheckBoxSetting(SettingsFile.KEY_DUMP_TEXTURES, Settings.SECTION_UTILITY, R.string.dump_textures, R.string.dump_textures_description, false, dumpTextures));
-        sl.add(new CheckBoxSetting(SettingsFile.KEY_CUSTOM_TEXTURES, Settings.SECTION_UTILITY, R.string.custom_textures, R.string.custom_textures_description, false, customTextures));
-        sl.add(new CheckBoxSetting(SettingsFile.KEY_ASYNC_CUSTOM_LOADING, Settings.SECTION_UTILITY, R.string.async_custom_loading, R.string.async_custom_loading_description, true, asyncCustomLoading));
-        //Disabled until custom texture implementation gets rewrite, current one overloads RAM and crashes Citra.
-        //sl.add(new CheckBoxSetting(SettingsFile.KEY_PRELOAD_TEXTURES, Settings.SECTION_UTILITY, R.string.preload_textures, R.string.preload_textures_description, false, preloadTextures));
-    }
-
-    private void addAudioSettings(ArrayList<SettingsItem> sl) {
-        mView.getActivity().setTitle(R.string.preferences_audio);
-
-        SettingSection audioSection = mSettings.getSection(Settings.SECTION_AUDIO);
-        Setting audioStretch = audioSection.getSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING);
-        Setting audioInputType = audioSection.getSetting(SettingsFile.KEY_AUDIO_INPUT_TYPE);
-
-        sl.add(new CheckBoxSetting(SettingsFile.KEY_ENABLE_AUDIO_STRETCHING, Settings.SECTION_AUDIO, R.string.audio_stretch, R.string.audio_stretch_description, true, audioStretch));
-        sl.add(new SingleChoiceSetting(SettingsFile.KEY_AUDIO_INPUT_TYPE, Settings.SECTION_AUDIO, R.string.audio_input_type, 0, R.array.audioInputTypeNames, R.array.audioInputTypeValues, 0, audioInputType));
-    }
-
-    private void addDebugSettings(ArrayList<SettingsItem> sl) {
-        mView.getActivity().setTitle(R.string.preferences_debug);
-
-        SettingSection coreSection = mSettings.getSection(Settings.SECTION_CORE);
-        SettingSection rendererSection = mSettings.getSection(Settings.SECTION_RENDERER);
-        Setting useCpuJit = coreSection.getSetting(SettingsFile.KEY_CPU_JIT);
-        Setting hardwareShader = rendererSection.getSetting(SettingsFile.KEY_HW_SHADER);
-        Setting vsyncEnable = rendererSection.getSetting(SettingsFile.KEY_USE_VSYNC);
-        Setting rendererDebug = rendererSection.getSetting(SettingsFile.KEY_RENDERER_DEBUG);
-
-        sl.add(new HeaderSetting(null, null, R.string.debug_warning, 0));
-        sl.add(new CheckBoxSetting(SettingsFile.KEY_CPU_JIT, Settings.SECTION_CORE, R.string.cpu_jit, R.string.cpu_jit_description, true, useCpuJit, true, mView));
-        sl.add(new CheckBoxSetting(SettingsFile.KEY_HW_SHADER, Settings.SECTION_RENDERER, R.string.hw_shaders, R.string.hw_shaders_description, true, hardwareShader, true, mView));
-        sl.add(new CheckBoxSetting(SettingsFile.KEY_USE_VSYNC, Settings.SECTION_RENDERER, R.string.vsync, R.string.vsync_description, true, vsyncEnable));
-        sl.add(new CheckBoxSetting(SettingsFile.KEY_RENDERER_DEBUG, Settings.SECTION_DEBUG, R.string.renderer_debug, R.string.renderer_debug_description, false, rendererDebug));
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt
new file mode 100644
index 0000000000..de0cc18266
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -0,0 +1,1040 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.ui
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.hardware.camera2.CameraAccessException
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraManager
+import android.os.Build
+import android.text.TextUtils
+import androidx.preference.PreferenceManager
+import org.citra.citra_emu.CitraApplication
+import org.citra.citra_emu.R
+import org.citra.citra_emu.features.settings.model.AbstractBooleanSetting
+import org.citra.citra_emu.features.settings.model.AbstractIntSetting
+import org.citra.citra_emu.features.settings.model.AbstractSetting
+import org.citra.citra_emu.features.settings.model.AbstractStringSetting
+import org.citra.citra_emu.features.settings.model.BooleanSetting
+import org.citra.citra_emu.features.settings.model.IntSetting
+import org.citra.citra_emu.features.settings.model.ScaledFloatSetting
+import org.citra.citra_emu.features.settings.model.Settings
+import org.citra.citra_emu.features.settings.model.StringSetting
+import org.citra.citra_emu.features.settings.model.view.AbstractShortSetting
+import org.citra.citra_emu.features.settings.model.view.DateTimeSetting
+import org.citra.citra_emu.features.settings.model.view.HeaderSetting
+import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
+import org.citra.citra_emu.features.settings.model.view.RunnableSetting
+import org.citra.citra_emu.features.settings.model.view.SettingsItem
+import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting
+import org.citra.citra_emu.features.settings.model.view.SliderSetting
+import org.citra.citra_emu.features.settings.model.view.StringInputSetting
+import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting
+import org.citra.citra_emu.features.settings.model.view.SubmenuSetting
+import org.citra.citra_emu.features.settings.model.view.SwitchSetting
+import org.citra.citra_emu.features.settings.utils.SettingsFile
+import org.citra.citra_emu.fragments.ResetSettingsDialogFragment
+import org.citra.citra_emu.utils.BirthdayMonth
+import org.citra.citra_emu.utils.SystemSaveGame
+import org.citra.citra_emu.utils.Log
+import org.citra.citra_emu.utils.ThemeUtil
+
+class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) {
+    private var menuTag: String? = null
+    private lateinit var gameId: String
+    private var settingsList: ArrayList<SettingsItem>? = null
+
+    private val settingsActivity get() = fragmentView.activityView as SettingsActivity
+    private val settings get() = fragmentView.activityView!!.settings
+    private lateinit var settingsAdapter: SettingsAdapter
+
+    private lateinit var preferences: SharedPreferences
+
+    fun onCreate(menuTag: String, gameId: String) {
+        this.gameId = gameId
+        this.menuTag = menuTag
+    }
+
+    fun onViewCreated(settingsAdapter: SettingsAdapter) {
+        this.settingsAdapter = settingsAdapter
+        preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
+        loadSettingsList()
+    }
+
+    fun putSetting(setting: AbstractSetting) {
+        if (setting.section == null || setting.key == null) {
+            return
+        }
+
+        val section = settings.getSection(setting.section!!)!!
+        if (section.getSetting(setting.key!!) == null) {
+            section.putSetting(setting)
+        }
+    }
+
+    fun loadSettingsList() {
+        if (!TextUtils.isEmpty(gameId)) {
+            settingsActivity.setToolbarTitle("Game Settings: $gameId")
+        }
+        val sl = ArrayList<SettingsItem>()
+        if (menuTag == null) {
+            return
+        }
+        when (menuTag) {
+            SettingsFile.FILE_NAME_CONFIG -> addConfigSettings(sl)
+            Settings.SECTION_CORE -> addGeneralSettings(sl)
+            Settings.SECTION_SYSTEM -> addSystemSettings(sl)
+            Settings.SECTION_CAMERA -> addCameraSettings(sl)
+            Settings.SECTION_CONTROLS -> addControlsSettings(sl)
+            Settings.SECTION_RENDERER -> addGraphicsSettings(sl)
+            Settings.SECTION_AUDIO -> addAudioSettings(sl)
+            Settings.SECTION_DEBUG -> addDebugSettings(sl)
+            Settings.SECTION_THEME -> addThemeSettings(sl)
+            else -> {
+                fragmentView.showToastMessage("Unimplemented menu", false)
+                return
+            }
+        }
+        settingsList = sl
+        fragmentView.showSettingsList(settingsList!!)
+    }
+
+    private fun addConfigSettings(sl: ArrayList<SettingsItem>) {
+        settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_settings))
+        sl.apply {
+            add(
+                SubmenuSetting(
+                    R.string.preferences_general,
+                    0,
+                    Settings.SECTION_CORE
+                )
+            )
+            add(
+                SubmenuSetting(
+                    R.string.preferences_system,
+                    0,
+                    Settings.SECTION_SYSTEM
+                )
+            )
+            add(
+                SubmenuSetting(
+                    R.string.preferences_camera,
+                    0,
+                    Settings.SECTION_CAMERA
+                )
+            )
+            add(
+                SubmenuSetting(
+                    R.string.preferences_controls,
+                    0,
+                    Settings.SECTION_CONTROLS
+                )
+            )
+            add(
+                SubmenuSetting(
+                    R.string.preferences_graphics,
+                    0,
+                    Settings.SECTION_RENDERER
+                )
+            )
+            add(
+                SubmenuSetting(
+                    R.string.preferences_audio,
+                    0,
+                    Settings.SECTION_AUDIO
+                )
+            )
+            add(
+                SubmenuSetting(
+                    R.string.preferences_debug,
+                    0,
+                    Settings.SECTION_DEBUG
+                )
+            )
+            add(
+                RunnableSetting(
+                    R.string.reset_to_default,
+                    0,
+                    false,
+                    {
+                        ResetSettingsDialogFragment().show(
+                            settingsActivity.supportFragmentManager,
+                            ResetSettingsDialogFragment.TAG
+                        )
+                    }
+                )
+            )
+        }
+    }
+
+    private fun addGeneralSettings(sl: ArrayList<SettingsItem>) {
+        settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_general))
+        sl.apply {
+            add(
+                SwitchSetting(
+                    IntSetting.USE_FRAME_LIMIT,
+                    R.string.frame_limit_enable,
+                    R.string.frame_limit_enable_description,
+                    IntSetting.USE_FRAME_LIMIT.key,
+                    IntSetting.USE_FRAME_LIMIT.defaultValue
+                )
+            )
+            add(
+                SliderSetting(
+                    IntSetting.FRAME_LIMIT,
+                    R.string.frame_limit_slider,
+                    R.string.frame_limit_slider_description,
+                    1,
+                    200,
+                    "%",
+                    IntSetting.FRAME_LIMIT.key,
+                    IntSetting.FRAME_LIMIT.defaultValue.toFloat()
+                )
+            )
+        }
+    }
+
+    @OptIn(ExperimentalStdlibApi::class)
+    private fun addSystemSettings(sl: ArrayList<SettingsItem>) {
+        settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_system))
+        sl.apply {
+            val usernameSetting = object : AbstractStringSetting {
+                override var string: String
+                    get() = SystemSaveGame.getUsername()
+                    set(value) = SystemSaveGame.setUsername(value)
+                override val key = null
+                override val section = null
+                override val isRuntimeEditable = false
+                override val valueAsString get() = string
+                override val defaultValue = "CITRA"
+            }
+            add(
+                StringInputSetting(
+                    usernameSetting,
+                    R.string.username,
+                    0,
+                    "CITRA",
+                    10
+                )
+            )
+            add(
+                SingleChoiceSetting(
+                    IntSetting.EMULATED_REGION,
+                    R.string.emulated_region,
+                    0,
+                    R.array.regionNames,
+                    R.array.regionValues,
+                    IntSetting.EMULATED_REGION.key,
+                    IntSetting.EMULATED_REGION.defaultValue
+                )
+            )
+
+            val systemLanguageSetting = object : AbstractIntSetting {
+                override var int: Int
+                    get() = SystemSaveGame.getSystemLanguage()
+                    set(value) = SystemSaveGame.setSystemLanguage(value)
+                override val key = null
+                override val section = null
+                override val isRuntimeEditable = false
+                override val valueAsString get() = int.toString()
+                override val defaultValue = 1
+            }
+            add(
+                SingleChoiceSetting(
+                    systemLanguageSetting,
+                    R.string.emulated_language,
+                    0,
+                    R.array.languageNames,
+                    R.array.languageValues
+                )
+            )
+
+            val systemCountrySetting = object : AbstractShortSetting {
+                override var short: Short
+                    get() = SystemSaveGame.getCountryCode()
+                    set(value) = SystemSaveGame.setCountryCode(value)
+                override val key = null
+                override val section = null
+                override val isRuntimeEditable = false
+                override val valueAsString = short.toString()
+                override val defaultValue: Short = 49
+            }
+            var index = -1
+            val countries = settingsActivity.resources.getStringArray(R.array.countries)
+                .mapNotNull {
+                    index++
+                    if (it.isNotEmpty()) it to index.toString() else null
+                }
+            add(
+                StringSingleChoiceSetting(
+                    systemCountrySetting,
+                    R.string.country,
+                    0,
+                    countries.map { it.first }.toTypedArray(),
+                    countries.map { it.second }.toTypedArray()
+                )
+            )
+
+            val playCoinSettings = object : AbstractIntSetting {
+                override var int: Int
+                    get() = SystemSaveGame.getPlayCoins()
+                    set(value) = SystemSaveGame.setPlayCoins(value)
+                override val key = null
+                override val section = null
+                override val isRuntimeEditable = false
+                override val valueAsString = int.toString()
+                override val defaultValue = 42
+            }
+            add(
+                SliderSetting(
+                    playCoinSettings,
+                    R.string.play_coins,
+                    0,
+                    0,
+                    300,
+                    ""
+                )
+            )
+            add(
+                RunnableSetting(
+                    R.string.console_id,
+                    0,
+                    false,
+                    { settingsAdapter.onClickRegenerateConsoleId() },
+                    { "0x${SystemSaveGame.getConsoleId().toHexString().uppercase()}" }
+                )
+            )
+
+            add(HeaderSetting(R.string.birthday))
+            val systemBirthdayMonthSetting = object : AbstractShortSetting {
+                override var short: Short
+                    get() = SystemSaveGame.getBirthday()[0]
+                    set(value) {
+                        val birthdayDay = SystemSaveGame.getBirthday()[1]
+                        val daysInNewMonth = BirthdayMonth.getMonthFromCode(value)?.days ?: 31
+                        if (daysInNewMonth < birthdayDay) {
+                            SystemSaveGame.setBirthday(value, 1)
+                            settingsAdapter.notifyDataSetChanged()
+                        } else {
+                            SystemSaveGame.setBirthday(value, birthdayDay)
+                        }
+                    }
+                override val key = null
+                override val section = null
+                override val isRuntimeEditable = false
+                override val valueAsString get() = short.toString()
+                override val defaultValue: Short = 3
+            }
+            add(
+                SingleChoiceSetting(
+                    systemBirthdayMonthSetting,
+                    R.string.birthday_month,
+                    0,
+                    R.array.months,
+                    R.array.monthValues
+                )
+            )
+
+            val systemBirthdayDaySetting = object : AbstractShortSetting {
+                override var short: Short
+                    get() = SystemSaveGame.getBirthday()[1]
+                    set(value) {
+                        val birthdayMonth = SystemSaveGame.getBirthday()[0]
+                        val daysInNewMonth =
+                            BirthdayMonth.getMonthFromCode(birthdayMonth)?.days ?: 31
+                        if (value > daysInNewMonth) {
+                            SystemSaveGame.setBirthday(birthdayMonth, 1)
+                        } else {
+                            SystemSaveGame.setBirthday(birthdayMonth, value)
+                        }
+                    }
+                override val key = null
+                override val section = null
+                override val isRuntimeEditable = false
+                override val valueAsString get() = short.toString()
+                override val defaultValue: Short = 25
+            }
+            val birthdayMonth = SystemSaveGame.getBirthday()[0]
+            val daysInMonth = BirthdayMonth.getMonthFromCode(birthdayMonth)?.days ?: 31
+            val dayArray = Array(daysInMonth) { "${it + 1}" }
+            add(
+                StringSingleChoiceSetting(
+                    systemBirthdayDaySetting,
+                    R.string.birthday_day,
+                    0,
+                    dayArray,
+                    dayArray
+                )
+            )
+
+            add(HeaderSetting(R.string.clock))
+            add(
+                SingleChoiceSetting(
+                    IntSetting.INIT_CLOCK,
+                    R.string.init_clock,
+                    R.string.init_clock_description,
+                    R.array.systemClockNames,
+                    R.array.systemClockValues,
+                    IntSetting.INIT_CLOCK.key,
+                    IntSetting.INIT_CLOCK.defaultValue
+                )
+            )
+            add(
+                DateTimeSetting(
+                    StringSetting.INIT_TIME,
+                    R.string.simulated_clock,
+                    R.string.init_time_description,
+                    StringSetting.INIT_TIME.key,
+                    StringSetting.INIT_TIME.defaultValue
+                )
+            )
+
+            add(HeaderSetting(R.string.plugin_loader))
+            add(
+                SwitchSetting(
+                    BooleanSetting.PLUGIN_LOADER,
+                    R.string.plugin_loader,
+                    R.string.plugin_loader_description,
+                    BooleanSetting.PLUGIN_LOADER.key,
+                    BooleanSetting.PLUGIN_LOADER.defaultValue
+                )
+            )
+            add(
+                SwitchSetting(
+                    BooleanSetting.ALLOW_PLUGIN_LOADER,
+                    R.string.allow_plugin_loader,
+                    R.string.allow_plugin_loader_description,
+                    BooleanSetting.ALLOW_PLUGIN_LOADER.key,
+                    BooleanSetting.ALLOW_PLUGIN_LOADER.defaultValue
+                )
+            )
+        }
+    }
+
+    private fun addCameraSettings(sl: ArrayList<SettingsItem>) {
+        settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.camera))
+
+        // Get the camera IDs
+        val cameraManager =
+            settingsActivity.getSystemService(Context.CAMERA_SERVICE) as CameraManager?
+        val supportedCameraNameList = ArrayList<String>()
+        val supportedCameraIdList = ArrayList<String>()
+        if (cameraManager != null) {
+            try {
+                for (id in cameraManager.cameraIdList) {
+                    val characteristics = cameraManager.getCameraCharacteristics(id)
+                    if (characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL) ==
+                        CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
+                    ) {
+                        continue  // Legacy cameras cannot be used with the NDK
+                    }
+                    supportedCameraIdList.add(id)
+                    val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
+                    var stringId: Int = R.string.camera_facing_external
+                    when (facing) {
+                        CameraCharacteristics.LENS_FACING_FRONT -> stringId =
+                            R.string.camera_facing_front
+
+                        CameraCharacteristics.LENS_FACING_BACK -> stringId =
+                            R.string.camera_facing_back
+
+                        CameraCharacteristics.LENS_FACING_EXTERNAL -> stringId =
+                            R.string.camera_facing_external
+                    }
+                    supportedCameraNameList.add(
+                        String.format("%1\$s (%2\$s)", id, settingsActivity.getString(stringId))
+                    )
+                }
+            } catch (e: CameraAccessException) {
+                Log.error("Couldn't retrieve camera list")
+                e.printStackTrace()
+            }
+        }
+
+        // Create the names and values for display
+        val cameraDeviceNameList =
+            settingsActivity.resources.getStringArray(R.array.cameraDeviceNames).toMutableList()
+        cameraDeviceNameList.addAll(supportedCameraNameList)
+        val cameraDeviceValueList =
+            settingsActivity.resources.getStringArray(R.array.cameraDeviceValues).toMutableList()
+        cameraDeviceValueList.addAll(supportedCameraIdList)
+
+        val haveCameraDevices = supportedCameraIdList.isNotEmpty()
+
+        val imageSourceNames =
+            settingsActivity.resources.getStringArray(R.array.cameraImageSourceNames)
+        val imageSourceValues =
+            settingsActivity.resources.getStringArray(R.array.cameraImageSourceValues)
+        if (!haveCameraDevices) {
+            // Remove the last entry (ndk / Device Camera)
+            imageSourceNames.copyOfRange(0, imageSourceNames.size - 1)
+            imageSourceValues.copyOfRange(0, imageSourceValues.size - 1)
+        }
+
+        sl.apply {
+            add(HeaderSetting(R.string.inner_camera))
+            add(
+                StringSingleChoiceSetting(
+                    StringSetting.CAMERA_INNER_NAME,
+                    R.string.image_source,
+                    R.string.image_source_description,
+                    imageSourceNames,
+                    imageSourceValues,
+                    StringSetting.CAMERA_INNER_NAME.key,
+                    StringSetting.CAMERA_INNER_NAME.defaultValue
+                )
+            )
+            if (haveCameraDevices) {
+                add(
+                    StringSingleChoiceSetting(
+                        StringSetting.CAMERA_INNER_CONFIG,
+                        R.string.camera_device,
+                        R.string.camera_device_description,
+                        cameraDeviceNameList.toTypedArray(),
+                        cameraDeviceValueList.toTypedArray(),
+                        StringSetting.CAMERA_INNER_CONFIG.key,
+                        StringSetting.CAMERA_INNER_CONFIG.defaultValue
+                    )
+                )
+            }
+            add(
+                SingleChoiceSetting(
+                    IntSetting.CAMERA_INNER_FLIP,
+                    R.string.image_flip,
+                    0,
+                    R.array.cameraFlipNames,
+                    R.array.cameraDeviceValues,
+                    IntSetting.CAMERA_INNER_FLIP.key,
+                    IntSetting.CAMERA_INNER_FLIP.defaultValue
+                )
+            )
+
+            add(HeaderSetting(R.string.outer_left_camera))
+            add(
+                StringSingleChoiceSetting(
+                    StringSetting.CAMERA_OUTER_LEFT_NAME,
+                    R.string.image_source,
+                    R.string.image_source_description,
+                    imageSourceNames,
+                    imageSourceValues,
+                    StringSetting.CAMERA_OUTER_LEFT_NAME.key,
+                    StringSetting.CAMERA_OUTER_LEFT_NAME.defaultValue
+                )
+            )
+            if (haveCameraDevices) {
+                add(
+                    StringSingleChoiceSetting(
+                        StringSetting.CAMERA_OUTER_LEFT_CONFIG,
+                        R.string.camera_device,
+                        R.string.camera_device_description,
+                        cameraDeviceNameList.toTypedArray(),
+                        cameraDeviceValueList.toTypedArray(),
+                        StringSetting.CAMERA_OUTER_LEFT_CONFIG.key,
+                        StringSetting.CAMERA_OUTER_LEFT_CONFIG.defaultValue
+                    )
+                )
+            }
+            add(
+                SingleChoiceSetting(
+                    IntSetting.CAMERA_OUTER_LEFT_FLIP,
+                    R.string.image_flip,
+                    0,
+                    R.array.cameraFlipNames,
+                    R.array.cameraDeviceValues,
+                    IntSetting.CAMERA_OUTER_LEFT_FLIP.key,
+                    IntSetting.CAMERA_OUTER_LEFT_FLIP.defaultValue
+                )
+            )
+
+            add(HeaderSetting(R.string.outer_right_camera))
+            add(
+                StringSingleChoiceSetting(
+                    StringSetting.CAMERA_OUTER_RIGHT_NAME,
+                    R.string.image_source,
+                    R.string.image_source_description,
+                    imageSourceNames,
+                    imageSourceValues,
+                    StringSetting.CAMERA_OUTER_RIGHT_NAME.key,
+                    StringSetting.CAMERA_OUTER_RIGHT_NAME.defaultValue
+                )
+            )
+            if (haveCameraDevices) {
+                add(
+                    StringSingleChoiceSetting(
+                        StringSetting.CAMERA_OUTER_RIGHT_CONFIG,
+                        R.string.camera_device,
+                        R.string.camera_device_description,
+                        cameraDeviceNameList.toTypedArray(),
+                        cameraDeviceValueList.toTypedArray(),
+                        StringSetting.CAMERA_OUTER_RIGHT_CONFIG.key,
+                        StringSetting.CAMERA_OUTER_RIGHT_CONFIG.defaultValue
+                    )
+                )
+            }
+            add(
+                SingleChoiceSetting(
+                    IntSetting.CAMERA_OUTER_RIGHT_FLIP,
+                    R.string.image_flip,
+                    0,
+                    R.array.cameraFlipNames,
+                    R.array.cameraDeviceValues,
+                    IntSetting.CAMERA_OUTER_RIGHT_FLIP.key,
+                    IntSetting.CAMERA_OUTER_RIGHT_FLIP.defaultValue
+                )
+            )
+        }
+    }
+
+    private fun addControlsSettings(sl: ArrayList<SettingsItem>) {
+        settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_controls))
+        sl.apply {
+            add(HeaderSetting(R.string.generic_buttons))
+            Settings.buttonKeys.forEachIndexed { i: Int, key: String ->
+                val button = getInputObject(key)
+                add(InputBindingSetting(button, Settings.buttonTitles[i]))
+            }
+
+            add(HeaderSetting(R.string.controller_circlepad))
+            Settings.circlePadKeys.forEachIndexed { i: Int, key: String ->
+                val button = getInputObject(key)
+                add(InputBindingSetting(button, Settings.axisTitles[i]))
+            }
+
+            add(HeaderSetting(R.string.controller_c))
+            Settings.cStickKeys.forEachIndexed { i: Int, key: String ->
+                val button = getInputObject(key)
+                add(InputBindingSetting(button, Settings.axisTitles[i]))
+            }
+
+            add(HeaderSetting(R.string.controller_dpad))
+            Settings.dPadKeys.forEachIndexed { i: Int, key: String ->
+                val button = getInputObject(key)
+                add(InputBindingSetting(button, Settings.axisTitles[i]))
+            }
+
+            add(HeaderSetting(R.string.controller_triggers))
+            Settings.triggerKeys.forEachIndexed { i: Int, key: String ->
+                val button = getInputObject(key)
+                add(InputBindingSetting(button, Settings.triggerTitles[i]))
+            }
+        }
+    }
+
+    private fun getInputObject(key: String): AbstractStringSetting {
+        return object : AbstractStringSetting {
+            override var string: String
+                get() = preferences.getString(key, "")!!
+                set(value) {
+                    preferences.edit()
+                        .putString(key, value)
+                        .apply()
+                }
+            override val key = key
+            override val section = Settings.SECTION_CONTROLS
+            override val isRuntimeEditable = true
+            override val valueAsString = preferences.getString(key, "")!!
+            override val defaultValue = ""
+        }
+    }
+
+    private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) {
+        settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_graphics))
+        sl.apply {
+            add(HeaderSetting(R.string.renderer))
+            add(
+                SingleChoiceSetting(
+                    IntSetting.GRAPHICS_API,
+                    R.string.graphics_api,
+                    0,
+                    R.array.graphicsApiNames,
+                    R.array.graphicsApiValues,
+                    IntSetting.GRAPHICS_API.key,
+                    IntSetting.GRAPHICS_API.defaultValue
+                )
+            )
+            add(
+                SwitchSetting(
+                    BooleanSetting.SPIRV_SHADER_GEN,
+                    R.string.spirv_shader_gen,
+                    R.string.spirv_shader_gen_description,
+                    BooleanSetting.SPIRV_SHADER_GEN.key,
+                    BooleanSetting.SPIRV_SHADER_GEN.defaultValue,
+                )
+            )
+            add(
+                SwitchSetting(
+                    BooleanSetting.ASYNC_SHADERS,
+                    R.string.async_shaders,
+                    R.string.async_shaders_description,
+                    BooleanSetting.ASYNC_SHADERS.key,
+                    BooleanSetting.ASYNC_SHADERS.defaultValue
+                )
+            )
+            add(
+                SliderSetting(
+                    IntSetting.RESOLUTION_FACTOR,
+                    R.string.internal_resolution,
+                    R.string.internal_resolution_description,
+                    1,
+                    10,
+                    "x",
+                    IntSetting.GRAPHICS_API.key,
+                    IntSetting.GRAPHICS_API.defaultValue.toFloat()
+                )
+            )
+            add(
+                SwitchSetting(
+                    IntSetting.LINEAR_FILTERING,
+                    R.string.linear_filtering,
+                    R.string.linear_filtering_description,
+                    IntSetting.LINEAR_FILTERING.key,
+                    IntSetting.LINEAR_FILTERING.defaultValue
+                )
+            )
+            add(
+                SwitchSetting(
+                    IntSetting.SHADERS_ACCURATE_MUL,
+                    R.string.shaders_accurate_mul,
+                    R.string.shaders_accurate_mul_description,
+                    IntSetting.SHADERS_ACCURATE_MUL.key,
+                    IntSetting.SHADERS_ACCURATE_MUL.defaultValue
+                )
+            )
+            add(
+                SwitchSetting(
+                    IntSetting.DISK_SHADER_CACHE,
+                    R.string.use_disk_shader_cache,
+                    R.string.use_disk_shader_cache_description,
+                    IntSetting.DISK_SHADER_CACHE.key,
+                    IntSetting.DISK_SHADER_CACHE.defaultValue
+                )
+            )
+            add(
+                SingleChoiceSetting(
+                    IntSetting.TEXTURE_FILTER,
+                    R.string.texture_filter_name,
+                    R.string.texture_filter_description,
+                    R.array.textureFilterNames,
+                    R.array.textureFilterValues,
+                    IntSetting.TEXTURE_FILTER.key,
+                    IntSetting.TEXTURE_FILTER.defaultValue
+                )
+            )
+
+            add(HeaderSetting(R.string.stereoscopy))
+            add(
+                SingleChoiceSetting(
+                    IntSetting.STEREOSCOPIC_3D_MODE,
+                    R.string.render3d,
+                    0,
+                    R.array.render3dModes,
+                    R.array.render3dValues,
+                    IntSetting.STEREOSCOPIC_3D_MODE.key,
+                    IntSetting.STEREOSCOPIC_3D_MODE.defaultValue
+                )
+            )
+            add(
+                SliderSetting(
+                    IntSetting.STEREOSCOPIC_3D_DEPTH,
+                    R.string.factor3d,
+                    R.string.factor3d_description,
+                    0,
+                    100,
+                    "%",
+                    IntSetting.STEREOSCOPIC_3D_DEPTH.key,
+                    IntSetting.STEREOSCOPIC_3D_DEPTH.defaultValue.toFloat()
+                )
+            )
+
+            add(HeaderSetting(R.string.cardboard_vr))
+            add(
+                SliderSetting(
+                    IntSetting.CARDBOARD_SCREEN_SIZE,
+                    R.string.cardboard_screen_size,
+                    R.string.cardboard_screen_size_description,
+                    30,
+                    100,
+                    "%",
+                    IntSetting.CARDBOARD_SCREEN_SIZE.key,
+                    IntSetting.CARDBOARD_SCREEN_SIZE.defaultValue.toFloat()
+                )
+            )
+            add(
+                SliderSetting(
+                    IntSetting.CARDBOARD_X_SHIFT,
+                    R.string.cardboard_x_shift,
+                    R.string.cardboard_x_shift_description,
+                    -100,
+                    100,
+                    "%",
+                    IntSetting.CARDBOARD_X_SHIFT.key,
+                    IntSetting.CARDBOARD_X_SHIFT.defaultValue.toFloat()
+                )
+            )
+            add(
+                SliderSetting(
+                    IntSetting.CARDBOARD_Y_SHIFT,
+                    R.string.cardboard_y_shift,
+                    R.string.cardboard_y_shift_description,
+                    -100,
+                    100,
+                    "%",
+                    IntSetting.CARDBOARD_Y_SHIFT.key,
+                    IntSetting.CARDBOARD_Y_SHIFT.defaultValue.toFloat()
+                )
+            )
+
+            add(HeaderSetting(R.string.utility))
+            add(
+                SwitchSetting(
+                    IntSetting.DUMP_TEXTURES,
+                    R.string.dump_textures,
+                    R.string.dump_textures_description,
+                    IntSetting.DUMP_TEXTURES.key,
+                    IntSetting.DUMP_TEXTURES.defaultValue
+                )
+            )
+            add(
+                SwitchSetting(
+                    IntSetting.CUSTOM_TEXTURES,
+                    R.string.custom_textures,
+                    R.string.custom_textures_description,
+                    IntSetting.CUSTOM_TEXTURES.key,
+                    IntSetting.CUSTOM_TEXTURES.defaultValue
+                )
+            )
+            add(
+                SwitchSetting(
+                    IntSetting.ASYNC_CUSTOM_LOADING,
+                    R.string.async_custom_loading,
+                    R.string.async_custom_loading_description,
+                    IntSetting.ASYNC_CUSTOM_LOADING.key,
+                    IntSetting.ASYNC_CUSTOM_LOADING.defaultValue
+                )
+            )
+
+            // Disabled until custom texture implementation gets rewrite, current one overloads RAM
+            // and crashes Citra.
+            // add(
+            //     SwitchSetting(
+            //         BooleanSetting.PRELOAD_TEXTURES,
+            //         R.string.preload_textures,
+            //         R.string.preload_textures_description,
+            //         BooleanSetting.PRELOAD_TEXTURES.key,
+            //         BooleanSetting.PRELOAD_TEXTURES.defaultValue
+            //     )
+            // )
+        }
+    }
+
+    private fun addAudioSettings(sl: ArrayList<SettingsItem>) {
+        settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_audio))
+        sl.apply {
+            add(
+                SliderSetting(
+                    ScaledFloatSetting.AUDIO_VOLUME,
+                    R.string.audio_volume,
+                    0,
+                    0,
+                    100,
+                    "%",
+                    ScaledFloatSetting.AUDIO_VOLUME.key,
+                    ScaledFloatSetting.AUDIO_VOLUME.defaultValue
+                )
+            )
+            add(
+                SwitchSetting(
+                    IntSetting.ENABLE_AUDIO_STRETCHING,
+                    R.string.audio_stretch,
+                    R.string.audio_stretch_description,
+                    IntSetting.ENABLE_AUDIO_STRETCHING.key,
+                    IntSetting.ENABLE_AUDIO_STRETCHING.defaultValue
+                )
+            )
+            add(
+                SingleChoiceSetting(
+                    IntSetting.AUDIO_INPUT_TYPE,
+                    R.string.audio_input_type,
+                    0,
+                    R.array.audioInputTypeNames,
+                    R.array.audioInputTypeValues,
+                    IntSetting.AUDIO_INPUT_TYPE.key,
+                    IntSetting.AUDIO_INPUT_TYPE.defaultValue
+                )
+            )
+
+            val soundOutputModeSetting = object : AbstractIntSetting {
+                override var int: Int
+                    get() = SystemSaveGame.getSoundOutputMode()
+                    set(value) = SystemSaveGame.setSoundOutputMode(value)
+                override val key = null
+                override val section = null
+                override val isRuntimeEditable = false
+                override val valueAsString = int.toString()
+                override val defaultValue = 2
+            }
+            add(
+                SingleChoiceSetting(
+                    soundOutputModeSetting,
+                    R.string.sound_output_mode,
+                    0,
+                    R.array.soundOutputModes,
+                    R.array.soundOutputModeValues
+                )
+            )
+        }
+    }
+
+    private fun addDebugSettings(sl: ArrayList<SettingsItem>) {
+        settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_debug))
+        sl.apply {
+            add(HeaderSetting(R.string.debug_warning))
+            add(
+                SwitchSetting(
+                    IntSetting.NEW_3DS,
+                    R.string.new_3ds,
+                    0,
+                    IntSetting.NEW_3DS.key,
+                    IntSetting.NEW_3DS.defaultValue
+                )
+            )
+            add(
+                SliderSetting(
+                    IntSetting.CPU_CLOCK_SPEED,
+                    R.string.cpu_clock_speed,
+                    0,
+                    25,
+                    400,
+                    "%",
+                    IntSetting.CPU_CLOCK_SPEED.key,
+                    IntSetting.CPU_CLOCK_SPEED.defaultValue.toFloat()
+                )
+            )
+            add(
+                SwitchSetting(
+                    IntSetting.CPU_JIT,
+                    R.string.cpu_jit,
+                    R.string.cpu_jit_description,
+                    IntSetting.CPU_JIT.key,
+                    IntSetting.CPU_JIT.defaultValue
+                )
+            )
+            add(
+                SwitchSetting(
+                    IntSetting.HW_SHADER,
+                    R.string.hw_shaders,
+                    R.string.hw_shaders_description,
+                    IntSetting.HW_SHADER.key,
+                    IntSetting.HW_SHADER.defaultValue
+                )
+            )
+            add(
+                SwitchSetting(
+                    IntSetting.VSYNC,
+                    R.string.vsync,
+                    R.string.vsync_description,
+                    IntSetting.VSYNC.key,
+                    IntSetting.VSYNC.defaultValue
+                )
+            )
+            add(
+                SwitchSetting(
+                    IntSetting.DEBUG_RENDERER,
+                    R.string.renderer_debug,
+                    R.string.renderer_debug_description,
+                    IntSetting.DEBUG_RENDERER.key,
+                    IntSetting.DEBUG_RENDERER.defaultValue
+                )
+            )
+        }
+    }
+
+    private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
+        settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_theme))
+        sl.apply {
+            val theme: AbstractBooleanSetting = object : AbstractBooleanSetting {
+                override var boolean: Boolean
+                    get() = preferences.getBoolean(Settings.PREF_MATERIAL_YOU, false)
+                    set(value) {
+                        preferences.edit()
+                            .putBoolean(Settings.PREF_MATERIAL_YOU, value)
+                            .apply()
+                        settingsActivity.recreate()
+                    }
+                override val key: String? = null
+                override val section: String? = null
+                override val isRuntimeEditable: Boolean = false
+                override val valueAsString: String
+                    get() = preferences.getBoolean(Settings.PREF_MATERIAL_YOU, false).toString()
+                override val defaultValue = false
+            }
+
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                add(
+                    SwitchSetting(
+                        theme,
+                        R.string.material_you,
+                        R.string.material_you_description
+                    )
+                )
+            }
+
+            val themeMode: AbstractIntSetting = object : AbstractIntSetting {
+                override var int: Int
+                    get() = preferences.getInt(Settings.PREF_THEME_MODE, -1)
+                    set(value) {
+                        preferences.edit()
+                            .putInt(Settings.PREF_THEME_MODE, value)
+                            .apply()
+                        ThemeUtil.setThemeMode(settingsActivity)
+                        settingsActivity.recreate()
+                    }
+                override val key: String? = null
+                override val section: String? = null
+                override val isRuntimeEditable: Boolean = false
+                override val valueAsString: String
+                    get() = preferences.getInt(Settings.PREF_THEME_MODE, -1).toString()
+                override val defaultValue: Any = -1
+            }
+
+            add(
+                SingleChoiceSetting(
+                    themeMode,
+                    R.string.change_theme_mode,
+                    0,
+                    R.array.themeModeEntries,
+                    R.array.themeModeValues
+                )
+            )
+
+            val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting {
+                override var boolean: Boolean
+                    get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
+                    set(value) {
+                        preferences.edit()
+                            .putBoolean(Settings.PREF_BLACK_BACKGROUNDS, value)
+                            .apply()
+                        settingsActivity.recreate()
+                    }
+                override val key: String? = null
+                override val section: String? = null
+                override val isRuntimeEditable: Boolean = false
+                override val valueAsString: String
+                    get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
+                        .toString()
+                override val defaultValue: Any = false
+            }
+
+            add(
+                SwitchSetting(
+                    blackBackgrounds,
+                    R.string.use_black_backgrounds,
+                    R.string.use_black_backgrounds_description
+                )
+            )
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.java
deleted file mode 100644
index c36eb55a7c..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.java
+++ /dev/null
@@ -1,78 +0,0 @@
-package org.citra.citra_emu.features.settings.ui;
-
-import androidx.fragment.app.FragmentActivity;
-
-import org.citra.citra_emu.features.settings.model.Setting;
-import org.citra.citra_emu.features.settings.model.Settings;
-import org.citra.citra_emu.features.settings.model.view.SettingsItem;
-
-import java.util.ArrayList;
-
-/**
- * Abstraction for a screen showing a list of settings. Instances of
- * this type of view will each display a layer of the setting hierarchy.
- */
-public interface SettingsFragmentView {
-    /**
-     * Called by the containing Activity to notify the Fragment that an
-     * asynchronous load operation completed.
-     *
-     * @param settings The (possibly null) result of the ini load operation.
-     */
-    void onSettingsFileLoaded(Settings settings);
-
-    /**
-     * Pass a settings HashMap to the containing activity, so that it can
-     * share the HashMap with other SettingsFragments; useful so that rotations
-     * do not require an additional load operation.
-     *
-     * @param settings An ArrayList containing all the settings HashMaps.
-     */
-    void passSettingsToActivity(Settings settings);
-
-    /**
-     * Pass an ArrayList to the View so that it can be displayed on screen.
-     *
-     * @param settingsList The result of converting the HashMap to an ArrayList
-     */
-    void showSettingsList(ArrayList<SettingsItem> settingsList);
-
-    /**
-     * Called by the containing Activity when an asynchronous load operation fails.
-     * Instructs the Fragment to load the settings screen with defaults selected.
-     */
-    void loadDefaultSettings();
-
-    /**
-     * @return The Fragment's containing activity.
-     */
-    FragmentActivity getActivity();
-
-    /**
-     * Tell the Fragment to tell the containing Activity to show a new
-     * Fragment containing a submenu of settings.
-     *
-     * @param menuKey Identifier for the settings group that should be shown.
-     */
-    void loadSubMenu(String menuKey);
-
-    /**
-     * Tell the Fragment to tell the containing activity to display a toast message.
-     *
-     * @param message Text to be shown in the Toast
-     * @param is_long Whether this should be a long Toast or short one.
-     */
-    void showToastMessage(String message, boolean is_long);
-
-    /**
-     * Have the fragment add a setting to the HashMap.
-     *
-     * @param setting The (possibly previously missing) new setting.
-     */
-    void putSetting(Setting setting);
-
-    /**
-     * Have the fragment tell the containing Activity that a setting was modified.
-     */
-    void onSettingChanged();
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.kt
new file mode 100644
index 0000000000..e1bb25230d
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/SettingsFragmentView.kt
@@ -0,0 +1,59 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.ui
+
+import org.citra.citra_emu.features.settings.model.AbstractSetting
+import org.citra.citra_emu.features.settings.model.view.SettingsItem
+
+/**
+ * Abstraction for a screen showing a list of settings. Instances of
+ * this type of view will each display a layer of the setting hierarchy.
+ */
+interface SettingsFragmentView {
+    /**
+     * Pass an ArrayList to the View so that it can be displayed on screen.
+     *
+     * @param settingsList The result of converting the HashMap to an ArrayList
+     */
+    fun showSettingsList(settingsList: ArrayList<SettingsItem>)
+
+    /**
+     * Instructs the Fragment to load the settings screen.
+     */
+    fun loadSettingsList()
+
+    /**
+     * @return The Fragment's containing activity.
+     */
+    val activityView: SettingsActivityView?
+
+    /**
+     * Tell the Fragment to tell the containing Activity to show a new
+     * Fragment containing a submenu of settings.
+     *
+     * @param menuKey Identifier for the settings group that should be shown.
+     */
+    fun loadSubMenu(menuKey: String)
+
+    /**
+     * Tell the Fragment to tell the containing activity to display a toast message.
+     *
+     * @param message Text to be shown in the Toast
+     * @param is_long Whether this should be a long Toast or short one.
+     */
+    fun showToastMessage(message: String?, is_long: Boolean)
+
+    /**
+     * Have the fragment add a setting to the HashMap.
+     *
+     * @param setting The (possibly previously missing) new setting.
+     */
+    fun putSetting(setting: AbstractSetting)
+
+    /**
+     * Have the fragment tell the containing Activity that a setting was modified.
+     */
+    fun onSettingChanged()
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java
deleted file mode 100644
index d914f7d0bc..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/CheckBoxSettingViewHolder.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package org.citra.citra_emu.features.settings.ui.viewholder;
-
-import android.view.View;
-import android.widget.CheckBox;
-import android.widget.TextView;
-
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.settings.model.view.CheckBoxSetting;
-import org.citra.citra_emu.features.settings.model.view.SettingsItem;
-import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
-
-public final class CheckBoxSettingViewHolder extends SettingViewHolder {
-    private CheckBoxSetting mItem;
-
-    private TextView mTextSettingName;
-    private TextView mTextSettingDescription;
-
-    private CheckBox mCheckbox;
-
-    public CheckBoxSettingViewHolder(View itemView, SettingsAdapter adapter) {
-        super(itemView, adapter);
-    }
-
-    @Override
-    protected void findViews(View root) {
-        mTextSettingName = root.findViewById(R.id.text_setting_name);
-        mTextSettingDescription = root.findViewById(R.id.text_setting_description);
-        mCheckbox = root.findViewById(R.id.checkbox);
-    }
-
-    @Override
-    public void bind(SettingsItem item) {
-        mItem = (CheckBoxSetting) item;
-
-        mTextSettingName.setText(item.getNameId());
-
-        if (item.getDescriptionId() > 0) {
-            mTextSettingDescription.setText(item.getDescriptionId());
-            mTextSettingDescription.setVisibility(View.VISIBLE);
-        } else {
-            mTextSettingDescription.setText("");
-            mTextSettingDescription.setVisibility(View.GONE);
-        }
-
-        mCheckbox.setChecked(mItem.isChecked());
-    }
-
-    @Override
-    public void onClick(View clicked) {
-        mCheckbox.toggle();
-
-        getAdapter().onBooleanClick(mItem, getAdapterPosition(), mCheckbox.isChecked());
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.java
deleted file mode 100644
index 09ea93010e..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package org.citra.citra_emu.features.settings.ui.viewholder;
-
-import android.view.View;
-import android.widget.TextView;
-
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.settings.model.view.DateTimeSetting;
-import org.citra.citra_emu.features.settings.model.view.SettingsItem;
-import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
-import org.citra.citra_emu.utils.Log;
-
-public final class DateTimeViewHolder extends SettingViewHolder {
-    private DateTimeSetting mItem;
-
-    private TextView mTextSettingName;
-    private TextView mTextSettingDescription;
-
-    public DateTimeViewHolder(View itemView, SettingsAdapter adapter) {
-        super(itemView, adapter);
-    }
-
-    @Override
-    protected void findViews(View root) {
-        mTextSettingName = root.findViewById(R.id.text_setting_name);
-        Log.error("test " + mTextSettingName);
-        mTextSettingDescription = root.findViewById(R.id.text_setting_description);
-        Log.error("test " + mTextSettingDescription);
-    }
-
-    @Override
-    public void bind(SettingsItem item) {
-        mItem = (DateTimeSetting) item;
-        mTextSettingName.setText(item.getNameId());
-
-        if (item.getDescriptionId() > 0) {
-            mTextSettingDescription.setText(item.getDescriptionId());
-            mTextSettingDescription.setVisibility(View.VISIBLE);
-        } else {
-            mTextSettingDescription.setVisibility(View.GONE);
-        }
-    }
-
-    @Override
-    public void onClick(View clicked) {
-        getAdapter().onDateTimeClick(mItem, getAdapterPosition());
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
new file mode 100644
index 0000000000..6e06e7a01b
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
@@ -0,0 +1,77 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.ui.viewholder
+
+import android.annotation.SuppressLint
+import android.view.View
+import org.citra.citra_emu.databinding.ListItemSettingBinding
+import java.time.Instant
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
+import org.citra.citra_emu.features.settings.model.view.DateTimeSetting
+import org.citra.citra_emu.features.settings.model.view.SettingsItem
+import org.citra.citra_emu.features.settings.ui.SettingsAdapter
+import java.text.SimpleDateFormat
+
+class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
+    SettingViewHolder(binding.root, adapter) {
+    private lateinit var setting: DateTimeSetting
+
+    @SuppressLint("SimpleDateFormat")
+    override fun bind(item: SettingsItem) {
+        setting = item as DateTimeSetting
+        binding.textSettingName.setText(item.nameId)
+        if (item.descriptionId != 0) {
+            binding.textSettingDescription.visibility = View.VISIBLE
+            binding.textSettingDescription.setText(item.descriptionId)
+        } else {
+            binding.textSettingDescription.visibility = View.GONE
+        }
+        binding.textSettingValue.visibility = View.VISIBLE
+        val epochTime = try {
+            setting.value.toLong()
+        } catch (e: NumberFormatException) {
+            val date = setting.value.substringBefore(" ")
+            val time = setting.value.substringAfter(" ")
+
+            val formatter = SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssZZZZ")
+            val gmt = formatter.parse("${date}T${time}+0000")
+            gmt!!.time / 1000
+        }
+        val instant = Instant.ofEpochMilli(epochTime * 1000)
+        val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
+        val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
+        binding.textSettingValue.text = dateFormatter.format(zonedTime)
+
+        if (setting.isEditable) {
+            binding.textSettingName.alpha = 1f
+            binding.textSettingDescription.alpha = 1f
+            binding.textSettingValue.alpha = 1f
+        } else {
+            binding.textSettingName.alpha = 0.5f
+            binding.textSettingDescription.alpha = 0.5f
+            binding.textSettingValue.alpha = 0.5f
+        }
+    }
+
+    override fun onClick(clicked: View) {
+        if (setting.isEditable) {
+            adapter.onDateTimeClick(setting, bindingAdapterPosition)
+        } else {
+            adapter.onClickDisabledSetting()
+        }
+    }
+
+    override fun onLongClick(clicked: View): Boolean {
+        if (setting.isEditable) {
+            return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
+        } else {
+            adapter.onClickDisabledSetting()
+        }
+        return false
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.java
deleted file mode 100644
index baf80ed76d..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package org.citra.citra_emu.features.settings.ui.viewholder;
-
-import android.view.View;
-import android.widget.TextView;
-
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.settings.model.view.SettingsItem;
-import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
-
-public final class HeaderViewHolder extends SettingViewHolder {
-    private TextView mHeaderName;
-
-    public HeaderViewHolder(View itemView, SettingsAdapter adapter) {
-        super(itemView, adapter);
-        itemView.setOnClickListener(null);
-    }
-
-    @Override
-    protected void findViews(View root) {
-        mHeaderName = root.findViewById(R.id.text_header_name);
-    }
-
-    @Override
-    public void bind(SettingsItem item) {
-        mHeaderName.setText(item.getNameId());
-    }
-
-    @Override
-    public void onClick(View clicked) {
-        // no-op
-    }
-}
\ No newline at end of file
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.kt
new file mode 100644
index 0000000000..617348c895
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/HeaderViewHolder.kt
@@ -0,0 +1,31 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.ui.viewholder
+
+import android.view.View
+import org.citra.citra_emu.databinding.ListItemSettingsHeaderBinding
+import org.citra.citra_emu.features.settings.model.view.SettingsItem
+import org.citra.citra_emu.features.settings.ui.SettingsAdapter
+
+class HeaderViewHolder(val binding: ListItemSettingsHeaderBinding, adapter: SettingsAdapter) :
+    SettingViewHolder(binding.root, adapter) {
+
+    init {
+        itemView.setOnClickListener(null)
+    }
+
+    override fun bind(item: SettingsItem) {
+        binding.textHeaderName.setText(item.nameId)
+    }
+
+    override fun onClick(clicked: View) {
+        // no-op
+    }
+
+    override fun onLongClick(clicked: View): Boolean {
+        // no-op
+        return true
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java
deleted file mode 100644
index 7d95c250a1..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package org.citra.citra_emu.features.settings.ui.viewholder;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.preference.PreferenceManager;
-import android.view.View;
-import android.widget.TextView;
-
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
-import org.citra.citra_emu.features.settings.model.view.SettingsItem;
-import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
-
-public final class InputBindingSettingViewHolder extends SettingViewHolder {
-    private InputBindingSetting mItem;
-
-    private TextView mTextSettingName;
-    private TextView mTextSettingDescription;
-
-    private Context mContext;
-
-    public InputBindingSettingViewHolder(View itemView, SettingsAdapter adapter, Context context) {
-        super(itemView, adapter);
-
-        mContext = context;
-    }
-
-    @Override
-    protected void findViews(View root) {
-        mTextSettingName = root.findViewById(R.id.text_setting_name);
-        mTextSettingDescription = root.findViewById(R.id.text_setting_description);
-    }
-
-    @Override
-    public void bind(SettingsItem item) {
-        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
-
-        mItem = (InputBindingSetting) item;
-
-        mTextSettingName.setText(item.getNameId());
-
-        String key = sharedPreferences.getString(mItem.getKey(), "");
-        if (key != null && !key.isEmpty()) {
-            mTextSettingDescription.setText(key);
-            mTextSettingDescription.setVisibility(View.VISIBLE);
-        } else {
-            mTextSettingDescription.setVisibility(View.GONE);
-        }
-    }
-
-    @Override
-    public void onClick(View clicked) {
-        getAdapter().onInputBindingClick(mItem, getAdapterPosition());
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt
new file mode 100644
index 0000000000..9d2dc15af3
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/InputBindingSettingViewHolder.kt
@@ -0,0 +1,60 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.ui.viewholder
+
+import android.view.View
+import androidx.preference.PreferenceManager
+import org.citra.citra_emu.CitraApplication
+import org.citra.citra_emu.databinding.ListItemSettingBinding
+import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
+import org.citra.citra_emu.features.settings.model.view.SettingsItem
+import org.citra.citra_emu.features.settings.ui.SettingsAdapter
+
+class InputBindingSettingViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
+    SettingViewHolder(binding.root, adapter) {
+    private lateinit var setting: InputBindingSetting
+
+    override fun bind(item: SettingsItem) {
+        val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
+        setting = item as InputBindingSetting
+        binding.textSettingName.setText(item.nameId)
+        val uiString = preferences.getString(setting.abstractSetting.key, "")!!
+        if (uiString.isNotEmpty()) {
+            binding.textSettingDescription.visibility = View.GONE
+            binding.textSettingValue.visibility = View.VISIBLE
+            binding.textSettingValue.text = uiString
+        } else {
+            binding.textSettingDescription.visibility = View.GONE
+            binding.textSettingValue.visibility = View.GONE
+        }
+
+        if (setting.isEditable) {
+            binding.textSettingName.alpha = 1f
+            binding.textSettingDescription.alpha = 1f
+            binding.textSettingValue.alpha = 1f
+        } else {
+            binding.textSettingName.alpha = 0.5f
+            binding.textSettingDescription.alpha = 0.5f
+            binding.textSettingValue.alpha = 0.5f
+        }
+    }
+
+    override fun onClick(clicked: View) {
+        if (setting.isEditable) {
+            adapter.onInputBindingClick(setting, bindingAdapterPosition)
+        } else {
+            adapter.onClickDisabledSetting()
+        }
+    }
+
+    override fun onLongClick(clicked: View): Boolean {
+        if (setting.isEditable) {
+            adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
+        } else {
+            adapter.onClickDisabledSetting()
+        }
+        return false
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt
new file mode 100644
index 0000000000..ac0c60a906
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/RunnableViewHolder.kt
@@ -0,0 +1,58 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.ui.viewholder
+
+import android.view.View
+import org.citra.citra_emu.NativeLibrary
+import org.citra.citra_emu.databinding.ListItemSettingBinding
+import org.citra.citra_emu.features.settings.model.view.RunnableSetting
+import org.citra.citra_emu.features.settings.model.view.SettingsItem
+import org.citra.citra_emu.features.settings.ui.SettingsAdapter
+
+class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
+    SettingViewHolder(binding.root, adapter) {
+    private lateinit var setting: RunnableSetting
+
+    override fun bind(item: SettingsItem) {
+        setting = item as RunnableSetting
+        binding.textSettingName.setText(item.nameId)
+        if (item.descriptionId != 0) {
+            binding.textSettingDescription.setText(item.descriptionId)
+            binding.textSettingDescription.visibility = View.VISIBLE
+        } else {
+            binding.textSettingDescription.visibility = View.GONE
+        }
+
+        if (setting.value != null) {
+            binding.textSettingValue.visibility = View.VISIBLE
+            binding.textSettingValue.text = setting.value!!.invoke()
+        } else {
+            binding.textSettingValue.visibility = View.GONE
+        }
+
+        if (setting.isEditable) {
+            binding.textSettingName.alpha = 1f
+            binding.textSettingDescription.alpha = 1f
+            binding.textSettingValue.alpha = 1f
+        } else {
+            binding.textSettingName.alpha = 0.5f
+            binding.textSettingDescription.alpha = 0.5f
+            binding.textSettingValue.alpha = 0.5f
+        }
+    }
+
+    override fun onClick(clicked: View) {
+        if (!setting.isRuntimeRunnable && !NativeLibrary.isRunning()) {
+            setting.runnable.invoke()
+        } else {
+            adapter.onClickDisabledSetting()
+        }
+    }
+
+    override fun onLongClick(clicked: View): Boolean {
+        // no-op
+        return true
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.java
deleted file mode 100644
index 2643ea1214..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package org.citra.citra_emu.features.settings.ui.viewholder;
-
-import android.view.View;
-
-import androidx.recyclerview.widget.RecyclerView;
-
-import org.citra.citra_emu.features.settings.model.view.SettingsItem;
-import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
-
-public abstract class SettingViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
-    private SettingsAdapter mAdapter;
-
-    public SettingViewHolder(View itemView, SettingsAdapter adapter) {
-        super(itemView);
-
-        mAdapter = adapter;
-
-        itemView.setOnClickListener(this);
-
-        findViews(itemView);
-    }
-
-    protected SettingsAdapter getAdapter() {
-        return mAdapter;
-    }
-
-    /**
-     * Gets handles to all this ViewHolder's child views using their XML-defined identifiers.
-     *
-     * @param root The newly inflated top-level view.
-     */
-    protected abstract void findViews(View root);
-
-    /**
-     * Called by the adapter to set this ViewHolder's child views to display the list item
-     * it must now represent.
-     *
-     * @param item The list item that should be represented by this ViewHolder.
-     */
-    public abstract void bind(SettingsItem item);
-
-    /**
-     * Called when this ViewHolder's view is clicked on. Implementations should usually pass
-     * this event up to the adapter.
-     *
-     * @param clicked The view that was clicked on.
-     */
-    public abstract void onClick(View clicked);
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt
new file mode 100644
index 0000000000..5b4d39cf43
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SettingViewHolder.kt
@@ -0,0 +1,37 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.ui.viewholder
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+import org.citra.citra_emu.features.settings.model.view.SettingsItem
+import org.citra.citra_emu.features.settings.ui.SettingsAdapter
+
+abstract class SettingViewHolder(itemView: View, protected val adapter: SettingsAdapter) :
+    RecyclerView.ViewHolder(itemView), View.OnClickListener, View.OnLongClickListener {
+
+    init {
+        itemView.setOnClickListener(this)
+        itemView.setOnLongClickListener(this)
+    }
+
+    /**
+     * Called by the adapter to set this ViewHolder's child views to display the list item
+     * it must now represent.
+     *
+     * @param item The list item that should be represented by this ViewHolder.
+     */
+    abstract fun bind(item: SettingsItem)
+
+    /**
+     * Called when this ViewHolder's view is clicked on. Implementations should usually pass
+     * this event up to the adapter.
+     *
+     * @param clicked The view that was clicked on.
+     */
+    abstract override fun onClick(clicked: View)
+
+    abstract override fun onLongClick(clicked: View): Boolean
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java
deleted file mode 100644
index f735b77523..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package org.citra.citra_emu.features.settings.ui.viewholder;
-
-import android.content.res.Resources;
-import android.view.View;
-import android.widget.TextView;
-
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.settings.model.view.SettingsItem;
-import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting;
-import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting;
-import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
-
-public final class SingleChoiceViewHolder extends SettingViewHolder {
-    private SettingsItem mItem;
-
-    private TextView mTextSettingName;
-    private TextView mTextSettingDescription;
-
-    public SingleChoiceViewHolder(View itemView, SettingsAdapter adapter) {
-        super(itemView, adapter);
-    }
-
-    @Override
-    protected void findViews(View root) {
-        mTextSettingName = root.findViewById(R.id.text_setting_name);
-        mTextSettingDescription = root.findViewById(R.id.text_setting_description);
-    }
-
-    @Override
-    public void bind(SettingsItem item) {
-        mItem = item;
-
-        mTextSettingName.setText(item.getNameId());
-        mTextSettingDescription.setVisibility(View.VISIBLE);
-        if (item.getDescriptionId() > 0) {
-            mTextSettingDescription.setText(item.getDescriptionId());
-        } else if (item instanceof SingleChoiceSetting) {
-            SingleChoiceSetting setting = (SingleChoiceSetting) item;
-            int selected = setting.getSelectedValue();
-            Resources resMgr = mTextSettingDescription.getContext().getResources();
-            String[] choices = resMgr.getStringArray(setting.getChoicesId());
-            int[] values = resMgr.getIntArray(setting.getValuesId());
-            for (int i = 0; i < values.length; ++i) {
-                if (values[i] == selected) {
-                    mTextSettingDescription.setText(choices[i]);
-                }
-            }
-        } else {
-            mTextSettingDescription.setVisibility(View.GONE);
-        }
-    }
-
-    @Override
-    public void onClick(View clicked) {
-        int position = getAdapterPosition();
-        if (mItem instanceof SingleChoiceSetting) {
-            getAdapter().onSingleChoiceClick((SingleChoiceSetting) mItem, position);
-        } else if (mItem instanceof StringSingleChoiceSetting) {
-            getAdapter().onStringSingleChoiceClick((StringSingleChoiceSetting) mItem, position);
-        }
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
new file mode 100644
index 0000000000..e5e6d5df6d
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
@@ -0,0 +1,94 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.ui.viewholder
+
+import android.view.View
+import org.citra.citra_emu.databinding.ListItemSettingBinding
+import org.citra.citra_emu.features.settings.model.view.SettingsItem
+import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting
+import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting
+import org.citra.citra_emu.features.settings.ui.SettingsAdapter
+
+class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
+    SettingViewHolder(binding.root, adapter) {
+    private lateinit var setting: SettingsItem
+
+    override fun bind(item: SettingsItem) {
+        setting = item
+        binding.textSettingName.setText(item.nameId)
+        if (item.descriptionId != 0) {
+            binding.textSettingDescription.visibility = View.VISIBLE
+            binding.textSettingDescription.setText(item.descriptionId)
+        } else {
+            binding.textSettingDescription.visibility = View.GONE
+        }
+        binding.textSettingValue.visibility = View.VISIBLE
+        binding.textSettingValue.text = getTextSetting()
+
+        if (setting.isEditable) {
+            binding.textSettingName.alpha = 1f
+            binding.textSettingDescription.alpha = 1f
+            binding.textSettingValue.alpha = 1f
+        } else {
+            binding.textSettingName.alpha = 0.5f
+            binding.textSettingDescription.alpha = 0.5f
+            binding.textSettingValue.alpha = 0.5f
+        }
+    }
+
+    private fun getTextSetting(): String {
+        when (val item = setting) {
+            is SingleChoiceSetting -> {
+                val resMgr = binding.textSettingDescription.context.resources
+                val values = resMgr.getIntArray(item.valuesId)
+                values.forEachIndexed { i: Int, value: Int ->
+                    if (value == (setting as SingleChoiceSetting).selectedValue) {
+                        return resMgr.getStringArray(item.choicesId)[i]
+                    }
+                }
+                return ""
+            }
+
+            is StringSingleChoiceSetting -> {
+                item.values?.forEachIndexed { i: Int, value: String ->
+                    if (value == item.selectedValue) {
+                        return item.choices[i]
+                    }
+                }
+                return ""
+            }
+
+            else -> return ""
+        }
+    }
+
+    override fun onClick(clicked: View) {
+        if (!setting.isEditable) {
+            adapter.onClickDisabledSetting()
+            return
+        }
+
+        if (setting is SingleChoiceSetting) {
+            adapter.onSingleChoiceClick(
+                (setting as SingleChoiceSetting),
+                bindingAdapterPosition
+            )
+        } else if (setting is StringSingleChoiceSetting) {
+            adapter.onStringSingleChoiceClick(
+                (setting as StringSingleChoiceSetting),
+                bindingAdapterPosition
+            )
+        }
+    }
+
+    override fun onLongClick(clicked: View): Boolean {
+        if (setting.isEditable) {
+            return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
+        } else {
+            adapter.onClickDisabledSetting()
+        }
+        return false
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.java
deleted file mode 100644
index ce503bc543..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package org.citra.citra_emu.features.settings.ui.viewholder;
-
-import android.view.View;
-import android.widget.TextView;
-
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.settings.model.view.SettingsItem;
-import org.citra.citra_emu.features.settings.model.view.SliderSetting;
-import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
-
-public final class SliderViewHolder extends SettingViewHolder {
-    private SliderSetting mItem;
-
-    private TextView mTextSettingName;
-    private TextView mTextSettingDescription;
-
-    public SliderViewHolder(View itemView, SettingsAdapter adapter) {
-        super(itemView, adapter);
-    }
-
-    @Override
-    protected void findViews(View root) {
-        mTextSettingName = root.findViewById(R.id.text_setting_name);
-        mTextSettingDescription = root.findViewById(R.id.text_setting_description);
-    }
-
-    @Override
-    public void bind(SettingsItem item) {
-        mItem = (SliderSetting) item;
-
-        mTextSettingName.setText(item.getNameId());
-
-        if (item.getDescriptionId() > 0) {
-            mTextSettingDescription.setText(item.getDescriptionId());
-            mTextSettingDescription.setVisibility(View.VISIBLE);
-        } else {
-            mTextSettingDescription.setVisibility(View.GONE);
-        }
-    }
-
-    @Override
-    public void onClick(View clicked) {
-        getAdapter().onSliderClick(mItem, getAdapterPosition());
-    }
-}
-
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt
new file mode 100644
index 0000000000..e5817adc78
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SliderViewHolder.kt
@@ -0,0 +1,65 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.ui.viewholder
+
+import android.view.View
+import org.citra.citra_emu.databinding.ListItemSettingBinding
+import org.citra.citra_emu.features.settings.model.AbstractFloatSetting
+import org.citra.citra_emu.features.settings.model.AbstractIntSetting
+import org.citra.citra_emu.features.settings.model.FloatSetting
+import org.citra.citra_emu.features.settings.model.ScaledFloatSetting
+import org.citra.citra_emu.features.settings.model.view.SettingsItem
+import org.citra.citra_emu.features.settings.model.view.SliderSetting
+import org.citra.citra_emu.features.settings.ui.SettingsAdapter
+
+class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
+    SettingViewHolder(binding.root, adapter) {
+    private lateinit var setting: SliderSetting
+
+    override fun bind(item: SettingsItem) {
+        setting = item as SliderSetting
+        binding.textSettingName.setText(item.nameId)
+        if (item.descriptionId != 0) {
+            binding.textSettingDescription.visibility = View.VISIBLE
+            binding.textSettingDescription.setText(item.descriptionId)
+        } else {
+            binding.textSettingDescription.visibility = View.GONE
+        }
+        binding.textSettingValue.visibility = View.VISIBLE
+        binding.textSettingValue.text = when (setting.setting) {
+            is ScaledFloatSetting ->
+                "${(setting.setting as ScaledFloatSetting).float.toInt()}${setting.units}"
+            is FloatSetting -> "${(setting.setting as AbstractFloatSetting).float}${setting.units}"
+            else -> "${(setting.setting as AbstractIntSetting).int}${setting.units}"
+        }
+
+        if (setting.isEditable) {
+            binding.textSettingName.alpha = 1f
+            binding.textSettingDescription.alpha = 1f
+            binding.textSettingValue.alpha = 1f
+        } else {
+            binding.textSettingName.alpha = 0.5f
+            binding.textSettingDescription.alpha = 0.5f
+            binding.textSettingValue.alpha = 0.5f
+        }
+    }
+
+    override fun onClick(clicked: View) {
+        if (setting.isEditable) {
+            adapter.onSliderClick(setting, bindingAdapterPosition)
+        } else {
+            adapter.onClickDisabledSetting()
+        }
+    }
+
+    override fun onLongClick(clicked: View): Boolean {
+        if (setting.isEditable) {
+            return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
+        } else {
+            adapter.onClickDisabledSetting()
+        }
+        return false
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt
new file mode 100644
index 0000000000..b3140f39eb
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/StringInputViewHolder.kt
@@ -0,0 +1,46 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.ui.viewholder
+
+import android.view.View
+import org.citra.citra_emu.databinding.ListItemSettingBinding
+import org.citra.citra_emu.features.settings.model.view.SettingsItem
+import org.citra.citra_emu.features.settings.model.view.StringInputSetting
+import org.citra.citra_emu.features.settings.ui.SettingsAdapter
+
+class StringInputViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
+    SettingViewHolder(binding.root, adapter) {
+    private lateinit var setting: SettingsItem
+
+    override fun bind(item: SettingsItem) {
+        setting = item
+        binding.textSettingName.setText(item.nameId)
+        if (item.descriptionId != 0) {
+            binding.textSettingDescription.visibility = View.VISIBLE
+            binding.textSettingDescription.setText(item.descriptionId)
+        } else {
+            binding.textSettingDescription.visibility = View.GONE
+        }
+        binding.textSettingValue.visibility = View.VISIBLE
+        binding.textSettingValue.text = setting.setting?.valueAsString
+    }
+
+    override fun onClick(clicked: View) {
+        if (!setting.isEditable) {
+            adapter.onClickDisabledSetting()
+            return
+        }
+        adapter.onStringInputClick((setting as StringInputSetting), bindingAdapterPosition)
+    }
+
+    override fun onLongClick(clicked: View): Boolean {
+        if (setting.isEditable) {
+            return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
+        } else {
+            adapter.onClickDisabledSetting()
+        }
+        return false
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.java
deleted file mode 100644
index cb8c3e92a7..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.java
+++ /dev/null
@@ -1,45 +0,0 @@
-package org.citra.citra_emu.features.settings.ui.viewholder;
-
-import android.view.View;
-import android.widget.TextView;
-
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.settings.model.view.SettingsItem;
-import org.citra.citra_emu.features.settings.model.view.SubmenuSetting;
-import org.citra.citra_emu.features.settings.ui.SettingsAdapter;
-
-public final class SubmenuViewHolder extends SettingViewHolder {
-    private SubmenuSetting mItem;
-
-    private TextView mTextSettingName;
-    private TextView mTextSettingDescription;
-
-    public SubmenuViewHolder(View itemView, SettingsAdapter adapter) {
-        super(itemView, adapter);
-    }
-
-    @Override
-    protected void findViews(View root) {
-        mTextSettingName = root.findViewById(R.id.text_setting_name);
-        mTextSettingDescription = root.findViewById(R.id.text_setting_description);
-    }
-
-    @Override
-    public void bind(SettingsItem item) {
-        mItem = (SubmenuSetting) item;
-
-        mTextSettingName.setText(item.getNameId());
-
-        if (item.getDescriptionId() > 0) {
-            mTextSettingDescription.setText(item.getDescriptionId());
-            mTextSettingDescription.setVisibility(View.VISIBLE);
-        } else {
-            mTextSettingDescription.setVisibility(View.GONE);
-        }
-    }
-
-    @Override
-    public void onClick(View clicked) {
-        getAdapter().onSubmenuClick(mItem);
-    }
-}
\ No newline at end of file
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt
new file mode 100644
index 0000000000..3098abbff5
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt
@@ -0,0 +1,36 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.ui.viewholder
+
+import android.view.View
+import org.citra.citra_emu.databinding.ListItemSettingBinding
+import org.citra.citra_emu.features.settings.model.view.SettingsItem
+import org.citra.citra_emu.features.settings.model.view.SubmenuSetting
+import org.citra.citra_emu.features.settings.ui.SettingsAdapter
+
+class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
+    SettingViewHolder(binding.root, adapter) {
+    private lateinit var item: SubmenuSetting
+
+    override fun bind(item: SettingsItem) {
+        this.item = item as SubmenuSetting
+        binding.textSettingName.setText(item.nameId)
+        if (item.descriptionId != 0) {
+            binding.textSettingDescription.setText(item.descriptionId)
+            binding.textSettingDescription.visibility = View.VISIBLE
+        } else {
+            binding.textSettingDescription.visibility = View.GONE
+        }
+    }
+
+    override fun onClick(clicked: View) {
+        adapter.onSubmenuClick(item)
+    }
+
+    override fun onLongClick(clicked: View): Boolean {
+        // no-op
+        return true
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
new file mode 100644
index 0000000000..bd5ecc2eb4
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
@@ -0,0 +1,62 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.ui.viewholder
+
+import android.view.View
+import android.widget.CompoundButton
+import org.citra.citra_emu.databinding.ListItemSettingSwitchBinding
+import org.citra.citra_emu.features.settings.model.view.SettingsItem
+import org.citra.citra_emu.features.settings.model.view.SwitchSetting
+import org.citra.citra_emu.features.settings.ui.SettingsAdapter
+
+class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) :
+    SettingViewHolder(binding.root, adapter) {
+
+    private lateinit var setting: SwitchSetting
+
+    override fun bind(item: SettingsItem) {
+        setting = item as SwitchSetting
+        binding.textSettingName.setText(item.nameId)
+        if (item.descriptionId != 0) {
+            binding.textSettingDescription.setText(item.descriptionId)
+            binding.textSettingDescription.visibility = View.VISIBLE
+        } else {
+            binding.textSettingDescription.text = ""
+            binding.textSettingDescription.visibility = View.GONE
+        }
+
+        binding.switchWidget.setOnCheckedChangeListener(null)
+        binding.switchWidget.isChecked = setting.isChecked
+        binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
+            adapter.onBooleanClick(item, bindingAdapterPosition, binding.switchWidget.isChecked)
+        }
+
+        binding.switchWidget.isEnabled = setting.isEditable
+        if (setting.isEditable) {
+            binding.textSettingName.alpha = 1f
+            binding.textSettingDescription.alpha = 1f
+        } else {
+            binding.textSettingName.alpha = 0.5f
+            binding.textSettingDescription.alpha = 0.5f
+        }
+    }
+
+    override fun onClick(clicked: View) {
+        if (setting.isEditable) {
+            binding.switchWidget.toggle()
+        } else {
+            adapter.onClickDisabledSetting()
+        }
+    }
+
+    override fun onLongClick(clicked: View): Boolean {
+        if (setting.isEditable) {
+            return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
+        } else {
+            adapter.onClickDisabledSetting()
+        }
+        return false
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java
deleted file mode 100644
index 4590100cd4..0000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.java
+++ /dev/null
@@ -1,344 +0,0 @@
-package org.citra.citra_emu.features.settings.utils;
-
-import android.content.Context;
-import android.net.Uri;
-
-import androidx.annotation.NonNull;
-import androidx.documentfile.provider.DocumentFile;
-
-import org.citra.citra_emu.CitraApplication;
-import org.citra.citra_emu.NativeLibrary;
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.settings.model.FloatSetting;
-import org.citra.citra_emu.features.settings.model.IntSetting;
-import org.citra.citra_emu.features.settings.model.Setting;
-import org.citra.citra_emu.features.settings.model.SettingSection;
-import org.citra.citra_emu.features.settings.model.Settings;
-import org.citra.citra_emu.features.settings.model.StringSetting;
-import org.citra.citra_emu.features.settings.ui.SettingsActivityView;
-import org.citra.citra_emu.utils.BiMap;
-import org.citra.citra_emu.utils.DirectoryInitialization;
-import org.citra.citra_emu.utils.Log;
-import org.ini4j.Wini;
-
-import java.io.BufferedReader;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.util.HashMap;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.TreeSet;
-
-/**
- * Contains static methods for interacting with .ini files in which settings are stored.
- */
-public final class SettingsFile {
-    public static final String FILE_NAME_CONFIG = "config";
-
-    public static final String KEY_CPU_JIT = "use_cpu_jit";
-
-    public static final String KEY_DESIGN = "design";
-
-
-    public static final String KEY_GRAPHICS_API = "graphics_api";
-    public static final String KEY_SPIRV_SHADER_GEN = "spirv_shader_gen";
-    public static final String KEY_ASYNC_SHADERS = "async_shader_compilation";
-    public static final String KEY_RENDERER_DEBUG = "renderer_debug";
-    public static final String KEY_HW_SHADER = "use_hw_shader";
-    public static final String KEY_SHADERS_ACCURATE_MUL = "shaders_accurate_mul";
-    public static final String KEY_USE_SHADER_JIT = "use_shader_jit";
-    public static final String KEY_USE_DISK_SHADER_CACHE = "use_disk_shader_cache";
-    public static final String KEY_USE_VSYNC = "use_vsync_new";
-    public static final String KEY_RESOLUTION_FACTOR = "resolution_factor";
-    public static final String KEY_FRAME_LIMIT_ENABLED = "use_frame_limit";
-    public static final String KEY_FRAME_LIMIT = "frame_limit";
-    public static final String KEY_BACKGROUND_RED = "bg_red";
-    public static final String KEY_BACKGROUND_BLUE = "bg_blue";
-    public static final String KEY_BACKGROUND_GREEN = "bg_green";
-    public static final String KEY_RENDER_3D = "render_3d";
-    public static final String KEY_FACTOR_3D = "factor_3d";
-    public static final String KEY_PP_SHADER_NAME = "pp_shader_name";
-    public static final String KEY_FILTER_MODE = "filter_mode";
-    public static final String KEY_TEXTURE_FILTER_NAME = "texture_filter_name";
-    public static final String KEY_USE_ASYNCHRONOUS_GPU_EMULATION = "use_asynchronous_gpu_emulation";
-
-    public static final String KEY_LAYOUT_OPTION = "layout_option";
-    public static final String KEY_SWAP_SCREEN = "swap_screen";
-    public static final String KEY_CARDBOARD_SCREEN_SIZE = "cardboard_screen_size";
-    public static final String KEY_CARDBOARD_X_SHIFT = "cardboard_x_shift";
-    public static final String KEY_CARDBOARD_Y_SHIFT = "cardboard_y_shift";
-
-    public static final String KEY_DUMP_TEXTURES = "dump_textures";
-    public static final String KEY_CUSTOM_TEXTURES = "custom_textures";
-    public static final String KEY_PRELOAD_TEXTURES = "preload_textures";
-    public static final String KEY_ASYNC_CUSTOM_LOADING = "async_custom_loading";
-
-    public static final String KEY_AUDIO_OUTPUT_TYPE = "output_type";
-    public static final String KEY_ENABLE_AUDIO_STRETCHING = "enable_audio_stretching";
-    public static final String KEY_VOLUME = "volume";
-    public static final String KEY_AUDIO_INPUT_TYPE = "input_type";
-
-    public static final String KEY_USE_VIRTUAL_SD = "use_virtual_sd";
-
-    public static final String KEY_IS_NEW_3DS = "is_new_3ds";
-    public static final String KEY_REGION_VALUE = "region_value";
-    public static final String KEY_LANGUAGE = "language";
-    public static final String KEY_PLUGIN_LOADER = "plugin_loader";
-    public static final String KEY_ALLOW_PLUGIN_LOADER = "allow_plugin_loader";
-
-    public static final String KEY_INIT_CLOCK = "init_clock";
-    public static final String KEY_INIT_TIME = "init_time";
-
-    public static final String KEY_BUTTON_A = "button_a";
-    public static final String KEY_BUTTON_B = "button_b";
-    public static final String KEY_BUTTON_X = "button_x";
-    public static final String KEY_BUTTON_Y = "button_y";
-    public static final String KEY_BUTTON_SELECT = "button_select";
-    public static final String KEY_BUTTON_START = "button_start";
-    public static final String KEY_BUTTON_UP = "button_up";
-    public static final String KEY_BUTTON_DOWN = "button_down";
-    public static final String KEY_BUTTON_LEFT = "button_left";
-    public static final String KEY_BUTTON_RIGHT = "button_right";
-    public static final String KEY_BUTTON_L = "button_l";
-    public static final String KEY_BUTTON_R = "button_r";
-    public static final String KEY_BUTTON_ZL = "button_zl";
-    public static final String KEY_BUTTON_ZR = "button_zr";
-    public static final String KEY_CIRCLEPAD_AXIS_VERTICAL = "circlepad_axis_vertical";
-    public static final String KEY_CIRCLEPAD_AXIS_HORIZONTAL = "circlepad_axis_horizontal";
-    public static final String KEY_CSTICK_AXIS_VERTICAL = "cstick_axis_vertical";
-    public static final String KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal";
-    public static final String KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical";
-    public static final String KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal";
-    public static final String KEY_CIRCLEPAD_UP = "circlepad_up";
-    public static final String KEY_CIRCLEPAD_DOWN = "circlepad_down";
-    public static final String KEY_CIRCLEPAD_LEFT = "circlepad_left";
-    public static final String KEY_CIRCLEPAD_RIGHT = "circlepad_right";
-    public static final String KEY_CSTICK_UP = "cstick_up";
-    public static final String KEY_CSTICK_DOWN = "cstick_down";
-    public static final String KEY_CSTICK_LEFT = "cstick_left";
-    public static final String KEY_CSTICK_RIGHT = "cstick_right";
-
-    public static final String KEY_CAMERA_OUTER_RIGHT_NAME = "camera_outer_right_name";
-    public static final String KEY_CAMERA_OUTER_RIGHT_CONFIG = "camera_outer_right_config";
-    public static final String KEY_CAMERA_OUTER_RIGHT_FLIP = "camera_outer_right_flip";
-    public static final String KEY_CAMERA_OUTER_LEFT_NAME = "camera_outer_left_name";
-    public static final String KEY_CAMERA_OUTER_LEFT_CONFIG = "camera_outer_left_config";
-    public static final String KEY_CAMERA_OUTER_LEFT_FLIP = "camera_outer_left_flip";
-    public static final String KEY_CAMERA_INNER_NAME = "camera_inner_name";
-    public static final String KEY_CAMERA_INNER_CONFIG = "camera_inner_config";
-    public static final String KEY_CAMERA_INNER_FLIP = "camera_inner_flip";
-
-    public static final String KEY_LOG_FILTER = "log_filter";
-
-    private static BiMap<String, String> sectionsMap = new BiMap<>();
-
-    static {
-        //TODO: Add members to sectionsMap when game-specific settings are added
-    }
-
-
-    private SettingsFile() {
-    }
-
-    /**
-     * Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves
-     * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
-     * failed.
-     *
-     * @param ini          The ini file to load the settings from
-     * @param isCustomGame
-     * @param view         The current view.
-     * @return An Observable that emits a HashMap of the file's contents, then completes.
-     */
-    static HashMap<String, SettingSection> readFile(final DocumentFile ini, boolean isCustomGame, SettingsActivityView view) {
-        HashMap<String, SettingSection> sections = new Settings.SettingsSectionMap();
-
-        BufferedReader reader = null;
-
-        try {
-            Context context = CitraApplication.Companion.getAppContext();
-            InputStream inputStream = context.getContentResolver().openInputStream(ini.getUri());
-            reader = new BufferedReader(new InputStreamReader(inputStream));
-
-            SettingSection current = null;
-            for (String line; (line = reader.readLine()) != null; ) {
-                if (line.startsWith("[") && line.endsWith("]")) {
-                    current = sectionFromLine(line, isCustomGame);
-                    sections.put(current.getName(), current);
-                } else if ((current != null)) {
-                    Setting setting = settingFromLine(current, line);
-                    if (setting != null) {
-                        current.putSetting(setting);
-                    }
-                }
-            }
-        } catch (FileNotFoundException e) {
-            Log.error("[SettingsFile] File not found: " + ini.getUri() + e.getMessage());
-            if (view != null)
-                view.onSettingsFileNotFound();
-        } catch (IOException e) {
-            Log.error("[SettingsFile] Error reading from: " + ini.getUri() + e.getMessage());
-            if (view != null)
-                view.onSettingsFileNotFound();
-        } finally {
-            if (reader != null) {
-                try {
-                    reader.close();
-                } catch (IOException e) {
-                    Log.error("[SettingsFile] Error closing: " + ini.getUri() + e.getMessage());
-                }
-            }
-        }
-
-        return sections;
-    }
-
-    public static HashMap<String, SettingSection> readFile(final String fileName, SettingsActivityView view) {
-        return readFile(getSettingsFile(fileName), false, view);
-    }
-
-    /**
-     * Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves
-     * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
-     * failed.
-     *
-     * @param gameId the id of the game to load it's settings.
-     * @param view   The current view.
-     */
-    public static HashMap<String, SettingSection> readCustomGameSettings(final String gameId, SettingsActivityView view) {
-        return readFile(getCustomGameSettingsFile(gameId), true, view);
-    }
-
-    /**
-     * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error
-     * telling why it failed.
-     *
-     * @param fileName The target filename without a path or extension.
-     * @param sections The HashMap containing the Settings we want to serialize.
-     * @param view     The current view.
-     */
-    public static void saveFile(final String fileName, TreeMap<String, SettingSection> sections,
-                                SettingsActivityView view) {
-        DocumentFile ini = getSettingsFile(fileName);
-
-        try {
-            Context context = CitraApplication.Companion.getAppContext();
-            InputStream inputStream = context.getContentResolver().openInputStream(ini.getUri());
-            Wini writer = new Wini(inputStream);
-
-            Set<String> keySet = sections.keySet();
-            for (String key : keySet) {
-                SettingSection section = sections.get(key);
-                writeSection(writer, section);
-            }
-            inputStream.close();
-            OutputStream outputStream = context.getContentResolver().openOutputStream(ini.getUri(), "wt");
-            writer.store(outputStream);
-            outputStream.flush();
-            outputStream.close();
-        } catch (IOException e) {
-            Log.error("[SettingsFile] File not found: " + fileName + ".ini: " + e.getMessage());
-            view.showToastMessage(CitraApplication.Companion.getAppContext().getString(R.string.error_saving, fileName, e.getMessage()), false);
-        }
-    }
-
-    private static String mapSectionNameFromIni(String generalSectionName) {
-        if (sectionsMap.getForward(generalSectionName) != null) {
-            return sectionsMap.getForward(generalSectionName);
-        }
-
-        return generalSectionName;
-    }
-
-    private static String mapSectionNameToIni(String generalSectionName) {
-        if (sectionsMap.getBackward(generalSectionName) != null) {
-            return sectionsMap.getBackward(generalSectionName);
-        }
-
-        return generalSectionName;
-    }
-
-    public static DocumentFile getSettingsFile(String fileName) {
-        DocumentFile root = DocumentFile.fromTreeUri(CitraApplication.Companion.getAppContext(), Uri.parse(DirectoryInitialization.INSTANCE.getUserDirectory()));
-        DocumentFile configDirectory = root.findFile("config");
-        return configDirectory.findFile(fileName + ".ini");
-    }
-
-    private static DocumentFile getCustomGameSettingsFile(String gameId) {
-        DocumentFile root = DocumentFile.fromTreeUri(CitraApplication.Companion.getAppContext(), Uri.parse(DirectoryInitialization.INSTANCE.getUserDirectory()));
-        DocumentFile configDirectory = root.findFile("GameSettings");
-        return configDirectory.findFile(gameId + ".ini");
-    }
-
-    private static SettingSection sectionFromLine(String line, boolean isCustomGame) {
-        String sectionName = line.substring(1, line.length() - 1);
-        if (isCustomGame) {
-            sectionName = mapSectionNameToIni(sectionName);
-        }
-        return new SettingSection(sectionName);
-    }
-
-    /**
-     * For a line of text, determines what type of data is being represented, and returns
-     * a Setting object containing this data.
-     *
-     * @param current The section currently being parsed by the consuming method.
-     * @param line    The line of text being parsed.
-     * @return A typed Setting containing the key/value contained in the line.
-     */
-    private static Setting settingFromLine(SettingSection current, String line) {
-        String[] splitLine = line.split("=");
-
-        if (splitLine.length != 2) {
-            Log.warning("Skipping invalid config line \"" + line + "\"");
-            return null;
-        }
-
-        String key = splitLine[0].trim();
-        String value = splitLine[1].trim();
-
-        if (value.isEmpty()) {
-            Log.warning("Skipping null value in config line \"" + line + "\"");
-            return null;
-        }
-
-        try {
-            int valueAsInt = Integer.parseInt(value);
-
-            return new IntSetting(key, current.getName(), valueAsInt);
-        } catch (NumberFormatException ex) {
-        }
-
-        try {
-            float valueAsFloat = Float.parseFloat(value);
-
-            return new FloatSetting(key, current.getName(), valueAsFloat);
-        } catch (NumberFormatException ex) {
-        }
-
-        return new StringSetting(key, current.getName(), value);
-    }
-
-    /**
-     * Writes the contents of a Section HashMap to disk.
-     *
-     * @param parser  A Wini pointed at a file on disk.
-     * @param section A section containing settings to be written to the file.
-     */
-    private static void writeSection(Wini parser, SettingSection section) {
-        // Write the section header.
-        String header = section.getName();
-
-        // Write this section's values.
-        HashMap<String, Setting> settings = section.getSettings();
-        Set<String> keySet = settings.keySet();
-
-        for (String key : keySet) {
-            Setting setting = settings.get(key);
-            parser.put(header, setting.getKey(), setting.getValueAsString());
-        }
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt
new file mode 100644
index 0000000000..83b5da972c
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/utils/SettingsFile.kt
@@ -0,0 +1,258 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.features.settings.utils
+
+import android.content.Context
+import android.net.Uri
+import androidx.documentfile.provider.DocumentFile
+import org.citra.citra_emu.CitraApplication
+import org.citra.citra_emu.NativeLibrary
+import org.citra.citra_emu.R
+import org.citra.citra_emu.features.settings.model.AbstractSetting
+import org.citra.citra_emu.features.settings.model.BooleanSetting
+import org.citra.citra_emu.features.settings.model.FloatSetting
+import org.citra.citra_emu.features.settings.model.IntSetting
+import org.citra.citra_emu.features.settings.model.ScaledFloatSetting
+import org.citra.citra_emu.features.settings.model.SettingSection
+import org.citra.citra_emu.features.settings.model.Settings.SettingsSectionMap
+import org.citra.citra_emu.features.settings.model.StringSetting
+import org.citra.citra_emu.features.settings.ui.SettingsActivityView
+import org.citra.citra_emu.utils.BiMap
+import org.citra.citra_emu.utils.DirectoryInitialization.userDirectory
+import org.citra.citra_emu.utils.Log
+import org.ini4j.Wini
+import java.io.*
+import java.lang.NumberFormatException
+import java.util.*
+
+
+/**
+ * Contains static methods for interacting with .ini files in which settings are stored.
+ */
+object SettingsFile {
+    const val FILE_NAME_CONFIG = "config"
+
+    private var sectionsMap = BiMap<String?, String?>()
+
+    /**
+     * Reads a given .ini file from disk and returns it as a HashMap of Settings, themselves
+     * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
+     * failed.
+     *
+     * @param ini          The ini file to load the settings from
+     * @param isCustomGame
+     * @param view         The current view.
+     * @return An Observable that emits a HashMap of the file's contents, then completes.
+     */
+    fun readFile(
+        ini: DocumentFile,
+        isCustomGame: Boolean,
+        view: SettingsActivityView?
+    ): HashMap<String, SettingSection?> {
+        val sections: HashMap<String, SettingSection?> = SettingsSectionMap()
+        var reader: BufferedReader? = null
+        try {
+            val context: Context = CitraApplication.appContext
+            val inputStream = context.contentResolver.openInputStream(ini.uri)
+            reader = BufferedReader(InputStreamReader(inputStream))
+            var current: SettingSection? = null
+            var line: String?
+            while (reader.readLine().also { line = it } != null) {
+                if (line!!.startsWith("[") && line!!.endsWith("]")) {
+                    current = sectionFromLine(line!!, isCustomGame)
+                    sections[current.name] = current
+                } else if (current != null) {
+                    val setting = settingFromLine(line!!)
+                    if (setting != null) {
+                        current.putSetting(setting)
+                    }
+                }
+            }
+        } catch (e: FileNotFoundException) {
+            Log.error("[SettingsFile] File not found: " + ini.uri + e.message)
+            view?.onSettingsFileNotFound()
+        } catch (e: IOException) {
+            Log.error("[SettingsFile] Error reading from: " + ini.uri + e.message)
+            view?.onSettingsFileNotFound()
+        } finally {
+            if (reader != null) {
+                try {
+                    reader.close()
+                } catch (e: IOException) {
+                    Log.error("[SettingsFile] Error closing: " + ini.uri + e.message)
+                }
+            }
+        }
+        return sections
+    }
+
+    fun readFile(fileName: String, view: SettingsActivityView?): HashMap<String, SettingSection?> {
+        return readFile(getSettingsFile(fileName), false, view)
+    }
+
+    fun readFile(fileName: String): HashMap<String, SettingSection?> = readFile(fileName, null)
+
+    /**
+     * Reads a given .ini file from disk and returns it as a HashMap of SettingSections, themselves
+     * effectively a HashMap of key/value settings. If unsuccessful, outputs an error telling why it
+     * failed.
+     *
+     * @param gameId the id of the game to load it's settings.
+     * @param view   The current view.
+     */
+    fun readCustomGameSettings(
+        gameId: String,
+        view: SettingsActivityView?
+    ): HashMap<String, SettingSection?> {
+        return readFile(getCustomGameSettingsFile(gameId), true, view)
+    }
+
+    /**
+     * Saves a Settings HashMap to a given .ini file on disk. If unsuccessful, outputs an error
+     * telling why it failed.
+     *
+     * @param fileName The target filename without a path or extension.
+     * @param sections The HashMap containing the Settings we want to serialize.
+     * @param view     The current view.
+     */
+    fun saveFile(
+        fileName: String,
+        sections: TreeMap<String, SettingSection?>,
+        view: SettingsActivityView
+    ) {
+        val ini = getSettingsFile(fileName)
+        try {
+            val context: Context = CitraApplication.appContext
+            val inputStream = context.contentResolver.openInputStream(ini.uri)
+            val writer = Wini(inputStream)
+            val keySet: Set<String> = sections.keys
+            for (key in keySet) {
+                val section = sections[key]
+                writeSection(writer, section!!)
+            }
+            inputStream!!.close()
+            val outputStream = context.contentResolver.openOutputStream(ini.uri, "wt")
+            writer.store(outputStream)
+            outputStream!!.flush()
+            outputStream.close()
+        } catch (e: Exception) {
+            Log.error("[SettingsFile] File not found: $fileName.ini: ${e.message}")
+            view.showToastMessage(
+                CitraApplication.appContext
+                    .getString(R.string.error_saving, fileName, e.message), false
+            )
+        }
+    }
+
+    private fun mapSectionNameFromIni(generalSectionName: String): String? {
+        return if (sectionsMap.getForward(generalSectionName) != null) {
+            sectionsMap.getForward(generalSectionName)
+        } else {
+            generalSectionName
+        }
+    }
+
+    private fun mapSectionNameToIni(generalSectionName: String): String {
+        return if (sectionsMap.getBackward(generalSectionName) != null) {
+            sectionsMap.getBackward(generalSectionName).toString()
+        } else {
+            generalSectionName
+        }
+    }
+
+    fun getSettingsFile(fileName: String): DocumentFile {
+        val root = DocumentFile.fromTreeUri(CitraApplication.appContext, Uri.parse(userDirectory))
+        val configDirectory = root!!.findFile("config")
+        return configDirectory!!.findFile("$fileName.ini")!!
+    }
+
+    private fun getCustomGameSettingsFile(gameId: String): DocumentFile {
+        val root = DocumentFile.fromTreeUri(CitraApplication.appContext, Uri.parse(userDirectory))
+        val configDirectory = root!!.findFile("GameSettings")
+        return configDirectory!!.findFile("$gameId.ini")!!
+    }
+
+    private fun sectionFromLine(line: String, isCustomGame: Boolean): SettingSection {
+        var sectionName: String = line.substring(1, line.length - 1)
+        if (isCustomGame) {
+            sectionName = mapSectionNameToIni(sectionName)
+        }
+        return SettingSection(sectionName)
+    }
+
+    /**
+     * For a line of text, determines what type of data is being represented, and returns
+     * a Setting object containing this data.
+     *
+     * @param line    The line of text being parsed.
+     * @return A typed Setting containing the key/value contained in the line.
+     */
+    private fun settingFromLine(line: String): AbstractSetting? {
+        val splitLine = line.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
+        if (splitLine.size != 2) {
+            return null
+        }
+        val key = splitLine[0].trim { it <= ' ' }
+        val value = splitLine[1].trim { it <= ' ' }
+        if (value.isEmpty()) {
+            return null
+        }
+
+        val booleanSetting = BooleanSetting.from(key)
+        if (booleanSetting != null) {
+            booleanSetting.boolean = value.toBoolean()
+            return booleanSetting
+        }
+
+        val intSetting = IntSetting.from(key)
+        if (intSetting != null) {
+            try {
+                intSetting.int = value.toInt()
+            } catch (e: NumberFormatException) {
+                intSetting.int = if (value.toBoolean()) 1 else 0
+            }
+            return intSetting
+        }
+
+        val scaledFloatSetting = ScaledFloatSetting.from(key)
+        if (scaledFloatSetting != null) {
+            scaledFloatSetting.float = value.toFloat() * scaledFloatSetting.scale
+            return scaledFloatSetting
+        }
+
+        val floatSetting = FloatSetting.from(key)
+        if (floatSetting != null) {
+            floatSetting.float = value.toFloat()
+            return floatSetting
+        }
+
+        val stringSetting = StringSetting.from(key)
+        if (stringSetting != null) {
+            stringSetting.string = value
+            return stringSetting
+        }
+
+        return null
+    }
+
+    /**
+     * Writes the contents of a Section HashMap to disk.
+     *
+     * @param parser  A Wini pointed at a file on disk.
+     * @param section A section containing settings to be written to the file.
+     */
+    private fun writeSection(parser: Wini, section: SettingSection) {
+        // Write the section header.
+        val header = section.name
+
+        // Write this section's values.
+        val settings = section.settings
+        val keySet: Set<String> = settings.keys
+        for (key in keySet) {
+            val setting = settings[key]
+            parser.put(header, setting!!.key, setting.valueAsString)
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt
index 05379d8d6b..b19fdd7b9b 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/HomeSettingsFragment.kt
@@ -28,6 +28,7 @@ import org.citra.citra_emu.CitraApplication
 import org.citra.citra_emu.R
 import org.citra.citra_emu.adapters.HomeSettingAdapter
 import org.citra.citra_emu.databinding.FragmentHomeSettingsBinding
+import org.citra.citra_emu.features.settings.model.Settings
 import org.citra.citra_emu.features.settings.ui.SettingsActivity
 import org.citra.citra_emu.features.settings.utils.SettingsFile
 import org.citra.citra_emu.model.HomeSetting
@@ -124,6 +125,12 @@ class HomeSettingsFragment : Fragment() {
                 { getGamesDirectory.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) },
                 details = homeViewModel.gamesDir
             ),
+            HomeSetting(
+                R.string.preferences_theme,
+                R.string.theme_and_color_description,
+                R.drawable.ic_palette,
+                { SettingsActivity.launch(requireContext(), Settings.SECTION_THEME, "") }
+            ),
             HomeSetting(
                 R.string.about,
                 R.string.about_description,
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt
new file mode 100644
index 0000000000..cf42bed129
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/MotionBottomSheetDialogFragment.kt
@@ -0,0 +1,208 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.fragments
+
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.InputDevice
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewGroup
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import org.citra.citra_emu.R
+import org.citra.citra_emu.databinding.DialogInputBinding
+import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
+import org.citra.citra_emu.utils.Log
+import kotlin.math.abs
+
+class MotionBottomSheetDialogFragment : BottomSheetDialogFragment() {
+    private var _binding: DialogInputBinding? = null
+    private val binding get() = _binding!!
+
+    private var setting: InputBindingSetting? = null
+    private var onCancel: (() -> Unit)? = null
+    private var onDismiss: (() -> Unit)? = null
+
+    private val previousValues = ArrayList<Float>()
+    private var prevDeviceId = 0
+    private var waitingForEvent = true
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        if (setting == null) {
+            dismiss()
+        }
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        _binding = DialogInputBinding.inflate(inflater)
+        return binding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        BottomSheetBehavior.from<View>(view.parent as View).state =
+            BottomSheetBehavior.STATE_EXPANDED
+
+        isCancelable = false
+        view.requestFocus()
+        view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
+        if (setting!!.isButtonMappingSupported()) {
+            dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) }
+        }
+        if (setting!!.isAxisMappingSupported()) {
+            binding.root.setOnGenericMotionListener { _, event -> onMotionEvent(event) }
+        }
+
+        val inputTypeId = when {
+            setting!!.isCirclePad() -> R.string.controller_circlepad
+            setting!!.isCStick() -> R.string.controller_c
+            setting!!.isDPad() -> R.string.controller_dpad
+            setting!!.isTrigger() -> R.string.controller_trigger
+            else -> R.string.button
+        }
+        binding.textTitle.text =
+            String.format(
+                getString(R.string.input_dialog_title),
+                getString(inputTypeId),
+                getString(setting!!.nameId)
+            )
+
+        var messageResId: Int = R.string.input_dialog_description
+        if (setting!!.isAxisMappingSupported() && !setting!!.isTrigger()) {
+            // Use specialized message for axis left/right or up/down
+            messageResId = if (setting!!.isHorizontalOrientation()) {
+                R.string.input_binding_description_horizontal_axis
+            } else {
+                R.string.input_binding_description_vertical_axis
+            }
+        }
+        binding.textMessage.text = getString(messageResId)
+
+        binding.buttonClear.setOnClickListener {
+            setting?.removeOldMapping()
+            dismiss()
+        }
+        binding.buttonCancel.setOnClickListener {
+            onCancel?.invoke()
+            dismiss()
+        }
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        _binding = null
+    }
+
+    override fun onDismiss(dialog: DialogInterface) {
+        super.onDismiss(dialog)
+        onDismiss?.invoke()
+    }
+
+    private fun onKeyEvent(event: KeyEvent): Boolean {
+        Log.debug("[MotionBottomSheetDialogFragment] Received key event: " + event.action)
+        return when (event.action) {
+            KeyEvent.ACTION_UP -> {
+                setting?.onKeyInput(event)
+                dismiss()
+                // Even if we ignore the key, we still consume it. Thus return true regardless.
+                true
+            }
+
+            else -> false
+        }
+    }
+
+    private fun onMotionEvent(event: MotionEvent): Boolean {
+        Log.debug("[MotionBottomSheetDialogFragment] Received motion event: " + event.action)
+        if (event.source and InputDevice.SOURCE_CLASS_JOYSTICK == 0) return false
+        if (event.action != MotionEvent.ACTION_MOVE) return false
+
+        val input = event.device
+
+        val motionRanges = input.motionRanges
+
+        if (input.id != prevDeviceId) {
+            previousValues.clear()
+        }
+        prevDeviceId = input.id
+        val firstEvent = previousValues.isEmpty()
+
+        var numMovedAxis = 0
+        var axisMoveValue = 0.0f
+        var lastMovedRange: InputDevice.MotionRange? = null
+        var lastMovedDir = '?'
+        if (waitingForEvent) {
+            for (i in motionRanges.indices) {
+                val range = motionRanges[i]
+                val axis = range.axis
+                val origValue = event.getAxisValue(axis)
+                if (firstEvent) {
+                    previousValues.add(origValue)
+                } else {
+                    val previousValue = previousValues[i]
+
+                    // Only handle the axes that are not neutral (more than 0.5)
+                    // but ignore any axis that has a constant value (e.g. always 1)
+                    if (abs(origValue) > 0.5f && origValue != previousValue) {
+                        // It is common to have multiple axes with the same physical input. For example,
+                        // shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE.
+                        // To handle this, we ignore an axis motion that's the exact same as a motion
+                        // we already saw. This way, we ignore axes with two names, but catch the case
+                        // where a joystick is moved in two directions.
+                        // ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html
+                        if (origValue != axisMoveValue) {
+                            axisMoveValue = origValue
+                            numMovedAxis++
+                            lastMovedRange = range
+                            lastMovedDir = if (origValue < 0.0f) '-' else '+'
+                        }
+                    } else if (abs(origValue) < 0.25f && abs(previousValue) > 0.75f) {
+                        // Special case for d-pads (axis value jumps between 0 and 1 without any values
+                        // in between). Without this, the user would need to press the d-pad twice
+                        // due to the first press being caught by the "if (firstEvent)" case further up.
+                        numMovedAxis++
+                        lastMovedRange = range
+                        lastMovedDir = if (previousValue < 0.0f) '-' else '+'
+                    }
+                }
+                previousValues[i] = origValue
+            }
+
+            // If only one axis moved, that's the winner.
+            if (numMovedAxis == 1) {
+                waitingForEvent = false
+                setting?.onMotionInput(input, lastMovedRange!!, lastMovedDir)
+                dismiss()
+            }
+        }
+        return true
+    }
+
+    companion object {
+        const val TAG = "MotionBottomSheetDialogFragment"
+
+        fun newInstance(
+            setting: InputBindingSetting,
+            onCancel: () -> Unit,
+            onDismiss: () -> Unit
+        ): MotionBottomSheetDialogFragment {
+            val dialog = MotionBottomSheetDialogFragment()
+            dialog.apply {
+                this.setting = setting
+                this.onCancel = onCancel
+                this.onDismiss = onDismiss
+            }
+            return dialog
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/ResetSettingsDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/ResetSettingsDialogFragment.kt
new file mode 100644
index 0000000000..d4e4dec657
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/ResetSettingsDialogFragment.kt
@@ -0,0 +1,31 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.fragments
+
+import android.app.Dialog
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.citra.citra_emu.R
+import org.citra.citra_emu.features.settings.ui.SettingsActivity
+
+class ResetSettingsDialogFragment : DialogFragment() {
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        val settingsActivity = requireActivity() as SettingsActivity
+
+        return MaterialAlertDialogBuilder(requireContext())
+            .setTitle(R.string.reset_all_settings)
+            .setMessage(R.string.reset_all_settings_description)
+            .setPositiveButton(android.R.string.ok) { _, _ ->
+                settingsActivity.onSettingsReset()
+            }
+            .setNegativeButton(android.R.string.cancel, null)
+            .show()
+    }
+
+    companion object {
+        const val TAG = "ResetSettingsDialogFragment"
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt
index 3a9f8167cb..219f769fee 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/SystemFilesFragment.kt
@@ -25,7 +25,6 @@ import androidx.navigation.findNavController
 import androidx.preference.PreferenceManager
 import com.google.android.material.textfield.MaterialAutoCompleteTextView
 import com.google.android.material.transition.MaterialSharedAxis
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.launch
 import org.citra.citra_emu.CitraApplication
 import org.citra.citra_emu.NativeLibrary
@@ -33,6 +32,7 @@ import org.citra.citra_emu.R
 import org.citra.citra_emu.activities.EmulationActivity
 import org.citra.citra_emu.databinding.FragmentSystemFilesBinding
 import org.citra.citra_emu.features.settings.model.Settings
+import org.citra.citra_emu.utils.SystemSaveGame
 import org.citra.citra_emu.viewmodel.GamesViewModel
 import org.citra.citra_emu.viewmodel.HomeViewModel
 import org.citra.citra_emu.viewmodel.SystemFilesViewModel
@@ -74,7 +74,7 @@ class SystemFilesFragment : Fragment() {
         super.onCreate(savedInstanceState)
         enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
         returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
-        NativeLibrary.loadSystemConfig()
+        SystemSaveGame.load()
     }
 
     override fun onCreateView(
@@ -149,15 +149,15 @@ class SystemFilesFragment : Fragment() {
 
     override fun onPause() {
         super.onPause()
-        NativeLibrary.saveSystemConfig()
+        SystemSaveGame.save()
     }
 
     private fun reloadUi() {
         val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
 
-        binding.switchRunSystemSetup.isChecked = NativeLibrary.getIsSystemSetupNeeded()
+        binding.switchRunSystemSetup.isChecked = SystemSaveGame.getIsSystemSetupNeeded()
         binding.switchRunSystemSetup.setOnCheckedChangeListener { _, isChecked ->
-            NativeLibrary.setSystemSetupNeeded(isChecked)
+            SystemSaveGame.setSystemSetupNeeded(isChecked)
         }
 
         val showHomeApps = preferences.getBoolean(Settings.PREF_SHOW_HOME_APPS, false)
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt
index cb198f31e3..6010851ea2 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/MainActivity.kt
@@ -41,6 +41,7 @@ import org.citra.citra_emu.activities.EmulationActivity
 import org.citra.citra_emu.contracts.OpenFileResultContract
 import org.citra.citra_emu.databinding.ActivityMainBinding
 import org.citra.citra_emu.features.settings.model.Settings
+import org.citra.citra_emu.features.settings.model.SettingsViewModel
 import org.citra.citra_emu.features.settings.ui.SettingsActivity
 import org.citra.citra_emu.features.settings.utils.SettingsFile
 import org.citra.citra_emu.fragments.SelectUserDirectoryDialogFragment
@@ -54,11 +55,14 @@ import org.citra.citra_emu.utils.ThemeUtil
 import org.citra.citra_emu.viewmodel.GamesViewModel
 import org.citra.citra_emu.viewmodel.HomeViewModel
 
-class MainActivity : AppCompatActivity() {
+class MainActivity : AppCompatActivity(), ThemeProvider {
     private lateinit var binding: ActivityMainBinding
 
     private val homeViewModel: HomeViewModel by viewModels()
     private val gamesViewModel: GamesViewModel by viewModels()
+    private val settingsViewModel: SettingsViewModel by viewModels()
+
+    override var themeId: Int = 0
 
     override fun onCreate(savedInstanceState: Bundle?) {
         val splashScreen = installSplashScreen()
@@ -67,6 +71,11 @@ class MainActivity : AppCompatActivity() {
                     PermissionsHandler.hasWriteAccess(this)
         }
 
+        if (PermissionsHandler.hasWriteAccess(applicationContext) &&
+            DirectoryInitialization.areCitraDirectoriesReady()) {
+            settingsViewModel.settings.loadSettings()
+        }
+
         ThemeUtil.setTheme(this)
         super.onCreate(savedInstanceState)
 
@@ -155,6 +164,8 @@ class MainActivity : AppCompatActivity() {
 
     override fun onResume() {
         checkUserPermissions()
+
+        ThemeUtil.setCorrectTheme(this)
         super.onResume()
     }
 
@@ -163,6 +174,11 @@ class MainActivity : AppCompatActivity() {
         super.onDestroy()
     }
 
+    override fun setTheme(resId: Int) {
+        super.setTheme(resId)
+        themeId = resId
+    }
+
     private fun checkUserPermissions() {
         val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
             .getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/main/ThemeProvider.kt b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/ThemeProvider.kt
new file mode 100644
index 0000000000..87bca2ce26
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/main/ThemeProvider.kt
@@ -0,0 +1,12 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.ui.main
+
+interface ThemeProvider {
+    /**
+     * Provides theme ID by overriding an activity's 'setTheme' method and returning that result
+     */
+    var themeId: Int
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/SystemSaveGame.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/SystemSaveGame.kt
new file mode 100644
index 0000000000..f451762d7f
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/SystemSaveGame.kt
@@ -0,0 +1,67 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.utils
+
+object SystemSaveGame {
+    external fun save()
+
+    external fun load()
+
+    external fun getIsSystemSetupNeeded(): Boolean
+
+    external fun setSystemSetupNeeded(needed: Boolean)
+
+    external fun getUsername(): String
+
+    external fun setUsername(username: String)
+
+    /**
+     * Returns birthday as an array with the month as the first element and the
+     * day as the second element
+     */
+    external fun getBirthday(): ShortArray
+
+    external fun setBirthday(month: Short, day: Short)
+
+    external fun getSystemLanguage(): Int
+
+    external fun setSystemLanguage(language: Int)
+
+    external fun getSoundOutputMode(): Int
+
+    external fun setSoundOutputMode(mode: Int)
+
+    external fun getCountryCode(): Short
+
+    external fun setCountryCode(countryCode: Short)
+
+    external fun getPlayCoins(): Int
+
+    external fun setPlayCoins(coins: Int)
+
+    external fun getConsoleId(): Long
+
+    external fun regenerateConsoleId()
+}
+
+enum class BirthdayMonth(val code: Int, val days: Int) {
+    JANUARY(1, 31),
+    FEBRUARY(2, 29),
+    MARCH(3, 31),
+    APRIL(4, 30),
+    MAY(5, 31),
+    JUNE(6, 30),
+    JULY(7, 31),
+    AUGUST(8, 31),
+    SEPTEMBER(9, 30),
+    OCTOBER(10, 31),
+    NOVEMBER(11, 30),
+    DECEMBER(12, 31);
+
+    companion object {
+        fun getMonthFromCode(code: Short): BirthdayMonth? =
+            entries.firstOrNull { it.code == code.toInt() }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/ThemeUtil.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/ThemeUtil.kt
index ce3d24ceb4..69758a322b 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/utils/ThemeUtil.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/ThemeUtil.kt
@@ -16,6 +16,7 @@ import androidx.preference.PreferenceManager
 import org.citra.citra_emu.CitraApplication
 import org.citra.citra_emu.R
 import org.citra.citra_emu.features.settings.model.Settings
+import org.citra.citra_emu.ui.main.ThemeProvider
 import kotlin.math.roundToInt
 
 object ThemeUtil {
@@ -26,6 +27,20 @@ object ThemeUtil {
 
     fun setTheme(activity: AppCompatActivity) {
         setThemeMode(activity)
+        if (preferences.getBoolean(Settings.PREF_MATERIAL_YOU, false)) {
+            activity.setTheme(R.style.Theme_Citra_Main_MaterialYou)
+        } else {
+            activity.setTheme(R.style.Theme_Citra_Main)
+        }
+
+        // Using a specific night mode check because this could apply incorrectly when using the
+        // light app mode, dark system mode, and black backgrounds. Launching the settings activity
+        // will then show light mode colors/navigation bars but with black backgrounds.
+        if (preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) &&
+            isNightMode(activity)
+        ) {
+            activity.setTheme(R.style.ThemeOverlay_Citra_Dark)
+        }
     }
 
     fun setThemeMode(activity: AppCompatActivity) {
@@ -64,6 +79,14 @@ object ThemeUtil {
         windowController.isAppearanceLightNavigationBars = false
     }
 
+    fun setCorrectTheme(activity: AppCompatActivity) {
+        val currentTheme = (activity as ThemeProvider).themeId
+        setTheme(activity)
+        if (currentTheme != (activity as ThemeProvider).themeId) {
+            activity.recreate()
+        }
+    }
+
     @ColorInt
     fun getColorWithOpacity(@ColorInt color: Int, alphaFactor: Float): Int {
         return Color.argb(
diff --git a/src/android/app/src/main/jni/CMakeLists.txt b/src/android/app/src/main/jni/CMakeLists.txt
index bc0a6b94ac..624826e0f0 100644
--- a/src/android/app/src/main/jni/CMakeLists.txt
+++ b/src/android/app/src/main/jni/CMakeLists.txt
@@ -31,6 +31,7 @@ add_library(citra-android SHARED
     native.cpp
     ndk_motion.cpp
     ndk_motion.h
+    system_save_game.cpp
 )
 
 target_link_libraries(citra-android PRIVATE audio_core citra_common citra_core input_common network)
diff --git a/src/android/app/src/main/jni/config.cpp b/src/android/app/src/main/jni/config.cpp
index e0808b7f39..574b824a2a 100644
--- a/src/android/app/src/main/jni/config.cpp
+++ b/src/android/app/src/main/jni/config.cpp
@@ -75,13 +75,6 @@ static const std::array<int, Settings::NativeAnalog::NumAnalogs> default_analogs
     InputManager::N3DS_STICK_C,
 }};
 
-void Config::UpdateCFG() {
-    std::shared_ptr<Service::CFG::Module> cfg = std::make_shared<Service::CFG::Module>();
-    cfg->SetSystemLanguage(static_cast<Service::CFG::SystemLanguage>(
-        sdl2_config->GetInteger("System", "language", Service::CFG::SystemLanguage::LANGUAGE_EN)));
-    cfg->UpdateConfigNANDSavegame();
-}
-
 template <>
 void Config::ReadSetting(const std::string& group, Settings::Setting<std::string>& setting) {
     std::string setting_value = sdl2_config->Get(group, setting.GetLabel(), setting.GetDefault());
@@ -215,24 +208,11 @@ void Config::ReadValues() {
     ReadSetting("System", Settings::values.region_value);
     ReadSetting("System", Settings::values.init_clock);
     {
-        std::tm t;
-        t.tm_sec = 1;
-        t.tm_min = 0;
-        t.tm_hour = 0;
-        t.tm_mday = 1;
-        t.tm_mon = 0;
-        t.tm_year = 100;
-        t.tm_isdst = 0;
-        std::istringstream string_stream(
-            sdl2_config->GetString("System", "init_time", "2000-01-01 00:00:01"));
-        string_stream >> std::get_time(&t, "%Y-%m-%d %H:%M:%S");
-        if (string_stream.fail()) {
-            LOG_ERROR(Config, "Failed To parse init_time. Using 2000-01-01 00:00:01");
+        std::string time = sdl2_config->GetString("System", "init_time", "946681277");
+        try {
+            Settings::values.init_time = std::stoll(time);
+        } catch (...) {
         }
-        Settings::values.init_time =
-            std::chrono::duration_cast<std::chrono::seconds>(
-                std::chrono::system_clock::from_time_t(std::mktime(&t)).time_since_epoch())
-                .count();
     }
     ReadSetting("System", Settings::values.plugin_loader_enabled);
     ReadSetting("System", Settings::values.allow_plugin_loader);
@@ -286,9 +266,6 @@ void Config::ReadValues() {
         sdl2_config->GetString("WebService", "web_api_url", "https://api.citra-emu.org");
     NetSettings::values.citra_username = sdl2_config->GetString("WebService", "citra_username", "");
     NetSettings::values.citra_token = sdl2_config->GetString("WebService", "citra_token", "");
-
-    // Update CFG file based on settings
-    UpdateCFG();
 }
 
 void Config::Reload() {
diff --git a/src/android/app/src/main/jni/config.h b/src/android/app/src/main/jni/config.h
index 162225728a..c2b0abcea0 100644
--- a/src/android/app/src/main/jni/config.h
+++ b/src/android/app/src/main/jni/config.h
@@ -17,7 +17,6 @@ private:
 
     bool LoadINI(const std::string& default_contents = "", bool retry = true);
     void ReadValues();
-    void UpdateCFG();
 
 public:
     Config();
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index dbd845a840..f67d29e9cd 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -64,7 +64,6 @@ ANativeWindow* s_surf;
 
 std::shared_ptr<Common::DynamicLibrary> vulkan_library{};
 std::unique_ptr<EmuWindow_Android> window;
-std::shared_ptr<Service::CFG::Module> cfg;
 
 std::atomic<bool> stop_run{true};
 std::atomic<bool> pause_emulation{false};
@@ -732,29 +731,4 @@ void Java_org_citra_citra_1emu_NativeLibrary_logDeviceInfo([[maybe_unused]] JNIE
     LOG_INFO(Frontend, "Host OS: Android API level {}", android_get_device_api_level());
 }
 
-void Java_org_citra_citra_1emu_NativeLibrary_loadSystemConfig([[maybe_unused]] JNIEnv* env,
-                                                              [[maybe_unused]] jobject obj) {
-    if (Core::System::GetInstance().IsPoweredOn()) {
-        cfg = Service::CFG::GetModule(Core::System::GetInstance());
-    } else {
-        cfg = std::make_shared<Service::CFG::Module>();
-    }
-}
-
-void Java_org_citra_citra_1emu_NativeLibrary_saveSystemConfig([[maybe_unused]] JNIEnv* env,
-                                                              [[maybe_unused]] jobject obj) {
-    cfg->UpdateConfigNANDSavegame();
-}
-
-void Java_org_citra_citra_1emu_NativeLibrary_setSystemSetupNeeded([[maybe_unused]] JNIEnv* env,
-                                                                  [[maybe_unused]] jobject obj,
-                                                                  jboolean needed) {
-    cfg->SetSystemSetupNeeded(needed);
-}
-
-jboolean Java_org_citra_citra_1emu_NativeLibrary_getIsSystemSetupNeeded(
-    [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
-    return cfg->IsSystemSetupNeeded();
-}
-
 } // extern "C"
diff --git a/src/android/app/src/main/jni/system_save_game.cpp b/src/android/app/src/main/jni/system_save_game.cpp
new file mode 100644
index 0000000000..003ad1b638
--- /dev/null
+++ b/src/android/app/src/main/jni/system_save_game.cpp
@@ -0,0 +1,122 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <common/string_util.h>
+#include <core/core.h>
+#include <core/hle/service/cfg/cfg.h>
+#include <core/hle/service/ptm/ptm.h>
+#include "android_common/android_common.h"
+
+std::shared_ptr<Service::CFG::Module> cfg;
+
+extern "C" {
+
+void Java_org_citra_citra_1emu_utils_SystemSaveGame_save([[maybe_unused]] JNIEnv* env,
+                                                         [[maybe_unused]] jobject obj) {
+    cfg->UpdateConfigNANDSavegame();
+}
+
+void Java_org_citra_citra_1emu_utils_SystemSaveGame_load([[maybe_unused]] JNIEnv* env,
+                                                         [[maybe_unused]] jobject obj) {
+    if (Core::System::GetInstance().IsPoweredOn()) {
+        cfg = Service::CFG::GetModule(Core::System::GetInstance());
+    } else {
+        cfg = std::make_shared<Service::CFG::Module>();
+    }
+}
+
+jboolean Java_org_citra_citra_1emu_utils_SystemSaveGame_getIsSystemSetupNeeded(
+    [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
+    return cfg->IsSystemSetupNeeded();
+}
+
+void Java_org_citra_citra_1emu_utils_SystemSaveGame_setSystemSetupNeeded(
+    [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj, jboolean needed) {
+    cfg->SetSystemSetupNeeded(needed);
+}
+
+jstring Java_org_citra_citra_1emu_utils_SystemSaveGame_getUsername([[maybe_unused]] JNIEnv* env,
+                                                                   [[maybe_unused]] jobject obj) {
+    return ToJString(env, Common::UTF16ToUTF8(cfg->GetUsername()));
+}
+
+void Java_org_citra_citra_1emu_utils_SystemSaveGame_setUsername([[maybe_unused]] JNIEnv* env,
+                                                                [[maybe_unused]] jobject obj,
+                                                                jstring username) {
+    cfg->SetUsername(Common::UTF8ToUTF16(GetJString(env, username)));
+}
+
+jshortArray Java_org_citra_citra_1emu_utils_SystemSaveGame_getBirthday(
+    [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
+    jshortArray jbirthdayArray = env->NewShortArray(2);
+    auto birthday = cfg->GetBirthday();
+    jshort birthdayArray[2]{static_cast<jshort>(get<0>(birthday)),
+                            static_cast<jshort>(get<1>(birthday))};
+    env->SetShortArrayRegion(jbirthdayArray, 0, 2, birthdayArray);
+    return jbirthdayArray;
+}
+
+void Java_org_citra_citra_1emu_utils_SystemSaveGame_setBirthday([[maybe_unused]] JNIEnv* env,
+                                                                [[maybe_unused]] jobject obj,
+                                                                jshort jmonth, jshort jday) {
+    cfg->SetBirthday(static_cast<u8>(jmonth), static_cast<u8>(jday));
+}
+
+jint Java_org_citra_citra_1emu_utils_SystemSaveGame_getSystemLanguage(
+    [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
+    return cfg->GetSystemLanguage();
+}
+
+void Java_org_citra_citra_1emu_utils_SystemSaveGame_setSystemLanguage([[maybe_unused]] JNIEnv* env,
+                                                                      [[maybe_unused]] jobject obj,
+                                                                      jint jsystemLanguage) {
+    cfg->SetSystemLanguage(static_cast<Service::CFG::SystemLanguage>(jsystemLanguage));
+}
+
+jint Java_org_citra_citra_1emu_utils_SystemSaveGame_getSoundOutputMode(
+    [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
+    return cfg->GetSoundOutputMode();
+}
+
+void Java_org_citra_citra_1emu_utils_SystemSaveGame_setSoundOutputMode([[maybe_unused]] JNIEnv* env,
+                                                                       [[maybe_unused]] jobject obj,
+                                                                       jint jmode) {
+    cfg->SetSoundOutputMode(static_cast<Service::CFG::SoundOutputMode>(jmode));
+}
+
+jshort Java_org_citra_citra_1emu_utils_SystemSaveGame_getCountryCode([[maybe_unused]] JNIEnv* env,
+                                                                     [[maybe_unused]] jobject obj) {
+    return cfg->GetCountryCode();
+}
+
+void Java_org_citra_citra_1emu_utils_SystemSaveGame_setCountryCode([[maybe_unused]] JNIEnv* env,
+                                                                   [[maybe_unused]] jobject obj,
+                                                                   jshort jmode) {
+    cfg->SetCountryCode(static_cast<u8>(jmode));
+}
+
+jint Java_org_citra_citra_1emu_utils_SystemSaveGame_getPlayCoins([[maybe_unused]] JNIEnv* env,
+                                                                 [[maybe_unused]] jobject obj) {
+    return Service::PTM::Module::GetPlayCoins();
+}
+
+void Java_org_citra_citra_1emu_utils_SystemSaveGame_setPlayCoins([[maybe_unused]] JNIEnv* env,
+                                                                 [[maybe_unused]] jobject obj,
+                                                                 jint jcoins) {
+    Service::PTM::Module::SetPlayCoins(static_cast<u16>(jcoins));
+}
+
+jlong Java_org_citra_citra_1emu_utils_SystemSaveGame_getConsoleId([[maybe_unused]] JNIEnv* env,
+                                                                  [[maybe_unused]] jobject obj) {
+    return cfg->GetConsoleUniqueId();
+}
+
+void Java_org_citra_citra_1emu_utils_SystemSaveGame_regenerateConsoleId(
+    [[maybe_unused]] JNIEnv* env, [[maybe_unused]] jobject obj) {
+    const auto [random_number, console_id] = cfg->GenerateConsoleUniqueId();
+    cfg->SetConsoleUniqueId(random_number, console_id);
+    cfg->UpdateConfigNANDSavegame();
+}
+
+} // extern "C"
diff --git a/src/android/app/src/main/res/drawable/ic_palette.xml b/src/android/app/src/main/res/drawable/ic_palette.xml
new file mode 100644
index 0000000000..43daec1ff4
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_palette.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="?attr/colorControlNormal"
+        android:pathData="M12,2C6.49,2 2,6.49 2,12s4.49,10 10,10c1.38,0 2.5,-1.12 2.5,-2.5c0,-0.61 -0.23,-1.2 -0.64,-1.67c-0.08,-0.1 -0.13,-0.21 -0.13,-0.33c0,-0.28 0.22,-0.5 0.5,-0.5H16c3.31,0 6,-2.69 6,-6C22,6.04 17.51,2 12,2zM17.5,13c-0.83,0 -1.5,-0.67 -1.5,-1.5c0,-0.83 0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5C19,12.33 18.33,13 17.5,13zM14.5,9C13.67,9 13,8.33 13,7.5C13,6.67 13.67,6 14.5,6S16,6.67 16,7.5C16,8.33 15.33,9 14.5,9zM5,11.5C5,10.67 5.67,10 6.5,10S8,10.67 8,11.5C8,12.33 7.33,13 6.5,13S5,12.33 5,11.5zM11,7.5C11,8.33 10.33,9 9.5,9S8,8.33 8,7.5C8,6.67 8.67,6 9.5,6S11,6.67 11,7.5z" />
+</vector>
diff --git a/src/android/app/src/main/res/layout/activity_settings.xml b/src/android/app/src/main/res/layout/activity_settings.xml
index c57abfd0a6..14ae83b041 100644
--- a/src/android/app/src/main/res/layout/activity_settings.xml
+++ b/src/android/app/src/main/res/layout/activity_settings.xml
@@ -1,20 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
 <androidx.coordinatorlayout.widget.CoordinatorLayout
+    android:id="@+id/coordinator_main"
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/coordinator_settings"
     android:layout_width="match_parent"
-    android:layout_height="match_parent">
+    android:layout_height="match_parent"
+    android:background="?attr/colorSurface">
 
     <com.google.android.material.appbar.AppBarLayout
         android:id="@+id/appbar_settings"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:fitsSystemWindows="true">
+        android:fitsSystemWindows="true"
+        app:elevation="0dp">
 
-        <com.google.android.material.appbar.MaterialToolbar
-            android:id="@+id/toolbar_settings"
+        <com.google.android.material.appbar.CollapsingToolbarLayout
+            style="?attr/collapsingToolbarLayoutMediumStyle"
+            android:id="@+id/toolbar_settings_layout"
             android:layout_width="match_parent"
-            android:layout_height="?attr/actionBarSize" />
+            android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
+            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
+
+            <com.google.android.material.appbar.MaterialToolbar
+                android:id="@+id/toolbar_settings"
+                android:layout_width="match_parent"
+                android:layout_height="?attr/actionBarSize"
+                app:layout_collapseMode="pin" />
+
+        </com.google.android.material.appbar.CollapsingToolbarLayout>
 
     </com.google.android.material.appbar.AppBarLayout>
 
@@ -22,6 +35,16 @@
         android:id="@+id/frame_content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
+        android:layout_marginHorizontal="12dp"
         app:layout_behavior="@string/appbar_scrolling_view_behavior" />
 
+    <View
+        android:id="@+id/navigation_bar_shade"
+        android:layout_width="match_parent"
+        android:layout_height="1px"
+        android:background="@android:color/transparent"
+        android:clickable="false"
+        android:focusable="false"
+        android:layout_gravity="bottom|center_horizontal" />
+
 </androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/src/android/app/src/main/res/layout/dialog_input.xml b/src/android/app/src/main/res/layout/dialog_input.xml
new file mode 100644
index 0000000000..3ea1e4d7b5
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_input.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingHorizontal="24dp"
+    android:paddingBottom="24dp"
+    android:defaultFocusHighlightEnabled="false"
+    android:focusable="true"
+    android:focusableInTouchMode="true"
+    android:focusedByDefault="true"
+    android:orientation="vertical">
+
+    <com.google.android.material.bottomsheet.BottomSheetDragHandleView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal" />
+
+    <com.google.android.material.textview.MaterialTextView
+        android:id="@+id/text_title"
+        style="@style/TextAppearance.Material3.HeadlineSmall"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:gravity="center"
+        android:text="@string/start" />
+
+    <com.google.android.material.textview.MaterialTextView
+        android:id="@+id/text_message"
+        style="@style/TextAppearance.Material3.BodyLarge"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
+        android:gravity="center"
+        tools:text="@string/input_binding_description" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="16dp"
+        android:orientation="horizontal">
+
+        <Button
+            android:id="@+id/button_cancel"
+            style="@style/Widget.Material3.Button.TextButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:layout_weight="1"
+            android:focusable="false"
+            android:text="@android:string/cancel" />
+
+        <Button
+            android:id="@+id/button_clear"
+            style="@style/Widget.Material3.Button.TextButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:layout_weight="1"
+            android:focusable="false"
+            android:text="@string/clear" />
+
+    </LinearLayout>
+
+</LinearLayout>
diff --git a/src/android/app/src/main/res/layout/dialog_software_keyboard.xml b/src/android/app/src/main/res/layout/dialog_software_keyboard.xml
new file mode 100644
index 0000000000..a6d315278e
--- /dev/null
+++ b/src/android/app/src/main/res/layout/dialog_software_keyboard.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.google.android.material.textfield.TextInputLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/edit_text"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingTop="16dp"
+    android:paddingHorizontal="24dp"
+    tools:hint="@string/cheats_name"
+    app:errorEnabled="true"
+    app:layout_constraintBottom_toTopOf="@id/edit_notes"
+    app:layout_constraintEnd_toEndOf="parent"
+    app:layout_constraintStart_toStartOf="parent"
+    app:layout_constraintTop_toTopOf="parent">
+
+    <com.google.android.material.textfield.TextInputEditText
+        android:id="@+id/edit_text_input"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:importantForAutofill="no"
+        android:inputType="text"
+        android:minHeight="48dp"
+        android:textAlignment="viewStart" />
+
+</com.google.android.material.textfield.TextInputLayout>
diff --git a/src/android/app/src/main/res/layout/list_item_setting.xml b/src/android/app/src/main/res/layout/list_item_setting.xml
index a2a07b3811..2aaaa4ba63 100644
--- a/src/android/app/src/main/res/layout/list_item_setting.xml
+++ b/src/android/app/src/main/res/layout/list_item_setting.xml
@@ -1,5 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
-<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
@@ -8,34 +10,43 @@
     android:focusable="true"
     android:gravity="center_vertical"
     android:minHeight="72dp"
-    android:paddingTop="@dimen/spacing_large"
-    android:paddingBottom="@dimen/spacing_large">
+    android:padding="@dimen/spacing_large">
 
-    <TextView
-        android:id="@+id/text_setting_name"
-        style="@style/TextAppearance.AppCompat.Headline"
-        android:layout_width="0dp"
+    <LinearLayout
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_alignParentStart="true"
-        android:layout_alignParentTop="true"
-        android:layout_alignParentEnd="true"
-        android:layout_marginStart="@dimen/spacing_large"
-        android:layout_marginEnd="@dimen/spacing_large"
-        android:textSize="16sp"
-        tools:text="Setting Name" />
+        android:orientation="vertical">
 
-    <TextView
-        android:id="@+id/text_setting_description"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_below="@+id/text_setting_name"
-        android:layout_alignStart="@+id/text_setting_name"
-        android:layout_alignParentStart="true"
-        android:layout_alignParentEnd="true"
-        android:layout_marginStart="@dimen/spacing_large"
-        android:layout_marginTop="@dimen/spacing_small"
-        android:layout_marginEnd="@dimen/spacing_large"
-        android:visibility="visible"
-        tools:text="@string/app_disclaimer" />
+        <com.google.android.material.textview.MaterialTextView
+            android:id="@+id/text_setting_name"
+            style="@style/TextAppearance.Material3.HeadlineMedium"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textAlignment="viewStart"
+            android:textSize="16sp"
+            app:lineHeight="22dp"
+            tools:text="Setting Name" />
 
-</RelativeLayout>
\ No newline at end of file
+        <com.google.android.material.textview.MaterialTextView
+            android:id="@+id/text_setting_description"
+            style="@style/TextAppearance.Material3.BodySmall"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@dimen/spacing_small"
+            android:textAlignment="viewStart"
+            tools:text="@string/app_disclaimer" />
+
+        <com.google.android.material.textview.MaterialTextView
+            android:id="@+id/text_setting_value"
+            style="@style/TextAppearance.Material3.LabelMedium"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@dimen/spacing_small"
+            android:textAlignment="viewStart"
+            android:textStyle="bold"
+            android:visibility="gone"
+            tools:text="1x" />
+
+    </LinearLayout>
+
+</RelativeLayout>
diff --git a/src/android/app/src/main/res/layout/list_item_setting_switch.xml b/src/android/app/src/main/res/layout/list_item_setting_switch.xml
new file mode 100644
index 0000000000..cbace0e7f7
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_setting_switch.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?android:attr/selectableItemBackground"
+    android:clickable="true"
+    android:focusable="true"
+    android:minHeight="72dp"
+    android:paddingVertical="@dimen/spacing_large"
+    android:paddingStart="@dimen/spacing_large"
+    android:paddingEnd="24dp">
+
+    <com.google.android.material.materialswitch.MaterialSwitch
+        android:id="@+id/switch_widget"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentEnd="true"
+        android:layout_centerVertical="true" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignParentTop="true"
+        android:layout_centerVertical="true"
+        android:layout_marginEnd="@dimen/spacing_large"
+        android:layout_toStartOf="@+id/switch_widget"
+        android:gravity="center_vertical"
+        android:orientation="vertical">
+
+        <com.google.android.material.textview.MaterialTextView
+            android:id="@+id/text_setting_name"
+            style="@style/TextAppearance.Material3.HeadlineMedium"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textAlignment="viewStart"
+            android:textSize="16sp"
+            app:lineHeight="22dp"
+            tools:text="@string/frame_limit_enable" />
+
+        <com.google.android.material.textview.MaterialTextView
+            android:id="@+id/text_setting_description"
+            style="@style/TextAppearance.Material3.BodySmall"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@dimen/spacing_small"
+            android:textAlignment="viewStart"
+            tools:text="@string/frame_limit_enable_description" />
+
+    </LinearLayout>
+
+</RelativeLayout>
diff --git a/src/android/app/src/main/res/layout/list_item_settings_header.xml b/src/android/app/src/main/res/layout/list_item_settings_header.xml
index aabd4cfc1a..e072d32546 100644
--- a/src/android/app/src/main/res/layout/list_item_settings_header.xml
+++ b/src/android/app/src/main/res/layout/list_item_settings_header.xml
@@ -1,19 +1,16 @@
 <?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/text_header_name"
+    style="@style/TextAppearance.Material3.TitleSmall"
     android:layout_width="match_parent"
-    android:layout_height="48dp">
-
-    <TextView
-        android:id="@+id/text_header_name"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_gravity="center_vertical"
-        android:layout_marginStart="@dimen/spacing_large"
-        android:layout_marginBottom="@dimen/spacing_small"
-        android:layout_marginTop="@dimen/spacing_small"
-        android:textColor="?attr/colorPrimary"
-        android:textStyle="bold"
-        tools:text="CPU Settings" />
-
-</FrameLayout>
+    android:layout_height="wrap_content"
+    android:layout_gravity="start|center_vertical"
+    android:paddingHorizontal="@dimen/spacing_large"
+    android:paddingVertical="16dp"
+    android:focusable="false"
+    android:clickable="false"
+    android:textAlignment="viewStart"
+    android:textColor="?attr/colorPrimary"
+    android:textStyle="bold"
+    tools:text="CPU Settings" />
diff --git a/src/android/app/src/main/res/layout/sysclock_datetime_picker.xml b/src/android/app/src/main/res/layout/sysclock_datetime_picker.xml
deleted file mode 100644
index d082f52833..0000000000
--- a/src/android/app/src/main/res/layout/sysclock_datetime_picker.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:orientation="vertical"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:padding="8dp"
-    android:gravity="center">
-
-    <DatePicker
-        android:id="@+id/date_picker"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:calendarViewShown="false"
-        android:datePickerMode="spinner"
-        android:spinnersShown="true" />
-
-    <TimePicker
-        android:id="@+id/time_picker"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:timePickerMode="spinner" />
-</LinearLayout>
diff --git a/src/android/app/src/main/res/values-night-v31/themes.xml b/src/android/app/src/main/res/values-night-v31/themes.xml
new file mode 100644
index 0000000000..c0b82149b1
--- /dev/null
+++ b/src/android/app/src/main/res/values-night-v31/themes.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <style name="Theme.Citra.Main.MaterialYou" parent="Theme.Citra.Main">
+        <item name="colorPrimary">@color/m3_sys_color_dynamic_dark_primary</item>
+        <item name="colorOnPrimary">@color/m3_sys_color_dynamic_dark_on_primary</item>
+        <item name="colorPrimaryContainer">@color/m3_sys_color_dynamic_dark_primary_container</item>
+        <item name="colorOnPrimaryContainer">@color/m3_sys_color_dynamic_dark_on_primary_container</item>
+        <item name="colorSecondary">@color/m3_sys_color_dynamic_dark_secondary</item>
+        <item name="colorOnSecondary">@color/m3_sys_color_dynamic_dark_on_secondary</item>
+        <item name="colorSecondaryContainer">@color/m3_sys_color_dynamic_dark_secondary_container</item>
+        <item name="colorOnSecondaryContainer">@color/m3_sys_color_dynamic_dark_on_secondary_container</item>
+        <item name="colorTertiary">@color/m3_sys_color_dynamic_dark_tertiary</item>
+        <item name="colorOnTertiary">@color/m3_sys_color_dynamic_dark_on_tertiary</item>
+        <item name="colorTertiaryContainer">@color/m3_sys_color_dynamic_dark_tertiary_container</item>
+        <item name="colorOnTertiaryContainer">@color/m3_sys_color_dynamic_dark_on_tertiary_container</item>
+        <item name="android:colorBackground">@color/m3_sys_color_dynamic_dark_background</item>
+        <item name="colorOnBackground">@color/m3_sys_color_dynamic_dark_on_background</item>
+        <item name="colorSurface">@color/m3_sys_color_dynamic_dark_surface</item>
+        <item name="colorOnSurface">@color/m3_sys_color_dynamic_dark_on_surface</item>
+        <item name="colorSurfaceVariant">@color/m3_sys_color_dynamic_dark_surface_variant</item>
+        <item name="colorOnSurfaceVariant">@color/m3_sys_color_dynamic_dark_on_surface_variant</item>
+        <item name="colorOutline">@color/m3_sys_color_dynamic_dark_outline</item>
+        <item name="colorOnSurfaceInverse">@color/m3_sys_color_dynamic_dark_on_surface_variant</item>
+        <item name="colorSurfaceInverse">@color/m3_sys_color_dynamic_dark_surface_variant</item>
+        <item name="colorPrimaryInverse">@color/m3_sys_color_dynamic_dark_inverse_primary</item>
+    </style>
+
+</resources>
diff --git a/src/android/app/src/main/res/values-night/themes.xml b/src/android/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000000..8670410d46
--- /dev/null
+++ b/src/android/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <style name="ThemeOverlay.Citra.Dark" parent="">
+        <item name="colorSurface">@android:color/black</item>
+        <item name="android:colorBackground">@android:color/black</item>
+    </style>
+
+</resources>
diff --git a/src/android/app/src/main/res/values-v31/themes.xml b/src/android/app/src/main/res/values-v31/themes.xml
new file mode 100644
index 0000000000..54ce7c8eca
--- /dev/null
+++ b/src/android/app/src/main/res/values-v31/themes.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <style name="Theme.Citra.Main.MaterialYou" parent="Theme.Citra.Main">
+        <item name="colorPrimary">@color/m3_sys_color_dynamic_light_primary</item>
+        <item name="colorOnPrimary">@color/m3_sys_color_dynamic_light_on_primary</item>
+        <item name="colorPrimaryContainer">@color/m3_sys_color_dynamic_light_primary_container</item>
+        <item name="colorOnPrimaryContainer">@color/m3_sys_color_dynamic_light_on_primary_container</item>
+        <item name="colorSecondary">@color/m3_sys_color_dynamic_light_secondary</item>
+        <item name="colorOnSecondary">@color/m3_sys_color_dynamic_light_on_secondary</item>
+        <item name="colorSecondaryContainer">@color/m3_sys_color_dynamic_light_secondary_container</item>
+        <item name="colorOnSecondaryContainer">@color/m3_sys_color_dynamic_light_on_secondary_container</item>
+        <item name="colorTertiary">@color/m3_sys_color_dynamic_light_tertiary</item>
+        <item name="colorOnTertiary">@color/m3_sys_color_dynamic_light_on_tertiary</item>
+        <item name="colorTertiaryContainer">@color/m3_sys_color_dynamic_light_tertiary_container</item>
+        <item name="colorOnTertiaryContainer">@color/m3_sys_color_dynamic_light_on_tertiary_container</item>
+        <item name="android:colorBackground">@color/m3_sys_color_dynamic_light_background</item>
+        <item name="colorOnBackground">@color/m3_sys_color_dynamic_light_on_background</item>
+        <item name="colorSurface">@color/m3_sys_color_dynamic_light_surface</item>
+        <item name="colorOnSurface">@color/m3_sys_color_dynamic_light_on_surface</item>
+        <item name="colorSurfaceVariant">@color/m3_sys_color_dynamic_light_surface_variant</item>
+        <item name="colorOnSurfaceVariant">@color/m3_sys_color_dynamic_light_on_surface_variant</item>
+        <item name="colorOutline">@color/m3_sys_color_dynamic_light_outline</item>
+        <item name="colorOnSurfaceInverse">@color/m3_sys_color_dynamic_light_on_surface_variant</item>
+        <item name="colorSurfaceInverse">@color/m3_sys_color_dynamic_light_surface_variant</item>
+        <item name="colorPrimaryInverse">@color/m3_sys_color_dynamic_light_inverse_primary</item>
+    </style>
+
+</resources>
diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
index c7cdadd195..46c3e22821 100644
--- a/src/android/app/src/main/res/values/arrays.xml
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -1,10 +1,9 @@
 <?xml version="1.0" encoding="utf-8"?>
-
-<!-- All lists for ListPreference keys/values are placed here -->
 <resources>
+
     <string-array name="systemClockNames" translatable="true">
-        <item>Device clock</item>
-        <item>Simulated clock</item>
+        <item>@string/device_clock</item>
+        <item>@string/simulated_clock</item>
     </string-array>
 
     <integer-array name="systemClockValues" translatable="false">
@@ -12,39 +11,15 @@
         <item>1</item>
     </integer-array>
 
-    <string-array name="designNames" translatable="true">
-        <item>Light</item>
-        <item>Dark</item>
-        <item>System default</item>
-    </string-array>
-
-    <integer-array name="designValues" translatable="false">
-        <item>0</item>
-        <item>1</item>
-        <item>2</item>
-    </integer-array>
-
-    <!-- Pre-Android 10 does not support System Default -->
-    <string-array name="designNamesOld" translatable="true">
-        <item>Light</item>
-        <item>Dark</item>
-    </string-array>
-
-    <!-- Pre-Android 10 does not support System Default -->
-    <integer-array name="designValuesOld" translatable="false">
-        <item>0</item>
-        <item>1</item>
-    </integer-array>
-
     <string-array name="regionNames">
-        <item>Auto-select</item>
-        <item>Japan</item>
-        <item>USA</item>
-        <item>Europe</item>
-        <item>Australia</item>
-        <item>China</item>
-        <item>Korea</item>
-        <item>Taiwan</item>
+        <item>@string/auto_select</item>
+        <item>@string/system_region_jpn</item>
+        <item>@string/system_region_usa</item>
+        <item>@string/system_region_eur</item>
+        <item>@string/system_region_aus</item>
+        <item>@string/system_region_chn</item>
+        <item>@string/system_region_kor</item>
+        <item>@string/system_region_twn</item>
     </string-array>
 
     <integer-array name="regionValues">
@@ -59,18 +34,18 @@
     </integer-array>
 
     <string-array name="languageNames">
-        <item>Japanese (日本語)</item>
-        <item>English</item>
-        <item>French (français)</item>
-        <item>German (Deutsch)</item>
-        <item>Italian (italiano)</item>
-        <item>Spanish (español)</item>
-        <item>Simplified Chinese (简体中文)</item>
-        <item>Korean (한국어)</item>
-        <item>Dutch (Nederlands)</item>
-        <item>Portuguese (português)</item>
-        <item>Russian (Русский)</item>
-        <item>Traditional Chinese (正體中文)</item>
+        <item>@string/language_japanese</item>
+        <item>@string/language_english</item>
+        <item>@string/language_french</item>
+        <item>@string/language_german</item>
+        <item>@string/language_italian</item>
+        <item>@string/language_spanish</item>
+        <item>@string/language_simplified_chinese</item>
+        <item>@string/language_korean</item>
+        <item>@string/language_dutch</item>
+        <item>@string/language_portuguese</item>
+        <item>@string/language_russian</item>
+        <item>@string/language_traditional_chinese</item>
     </string-array>
 
     <integer-array name="languageValues">
@@ -89,25 +64,25 @@
     </integer-array>
 
     <string-array name="n3dsButtons">
-        <item>a</item>
-        <item>b</item>
-        <item>x</item>
-        <item>y</item>
-        <item>L</item>
-        <item>R</item>
-        <item>ZL</item>
-        <item>ZR</item>
-        <item>Start</item>
-        <item>Select</item>
-        <item>D-Pad</item>
-        <item>Circle Pad</item>
-        <item>C Stick</item>
+        <item>@string/button_a</item>
+        <item>@string/button_b</item>
+        <item>@string/button_x</item>
+        <item>@string/button_y</item>
+        <item>@string/button_l</item>
+        <item>@string/button_r</item>
+        <item>@string/button_zl</item>
+        <item>@string/button_zr</item>
+        <item>@string/button_start</item>
+        <item>@string/button_select</item>
+        <item>@string/controller_dpad</item>
+        <item>@string/controller_circlepad</item>
+        <item>@string/controller_c</item>
     </string-array>
 
     <string-array name="cameraImageSourceNames">
-        <item>Blank</item>
-        <item>Still Image</item>
-        <item>Device Camera</item>
+        <item>@string/blank</item>
+        <item>@string/still_image</item>
+        <item>@string/device_camera</item>
     </string-array>
 
     <string-array name="cameraImageSourceValues">
@@ -117,9 +92,9 @@
     </string-array>
 
     <string-array name="cameraDeviceNames">
-        <item>Default</item>
-        <item>Any Front Camera</item>
-        <item>Any Back Camera</item>
+        <item>@string/option_default</item>
+        <item>@string/any_front_camera</item>
+        <item>@string/any_back_camera</item>
     </string-array>
 
     <string-array name="cameraDeviceValues">
@@ -129,10 +104,10 @@
     </string-array>
 
     <string-array name="cameraFlipNames">
-        <item>None</item>
-        <item>Horizontal</item>
-        <item>Vertical</item>
-        <item>Reverse</item>
+        <item>@string/none</item>
+        <item>@string/horizontal</item>
+        <item>@string/vertical</item>
+        <item>@string/reverse</item>
     </string-array>
 
     <integer-array name="cameraFlipValues">
@@ -143,11 +118,11 @@
     </integer-array>
 
     <string-array name="audioInputTypeNames">
-        <item>Auto</item>
-        <item>None</item>
-        <item>Static Noise</item>
-        <item>Real Device (Cubeb)</item>
-        <item>Real Device (OpenAL)</item>
+        <item>@string/auto</item>
+        <item>@string/none</item>
+        <item>@string/static_noise</item>
+        <item>@string/real_cubeb</item>
+        <item>@string/real_openal</item>
     </string-array>
 
     <integer-array name="audioInputTypeValues">
@@ -159,12 +134,12 @@
     </integer-array>
 
     <string-array name="render3dModes">
-        <item>Off</item>
-        <item>Side by Side</item>
-        <item>Anaglyph</item>
-        <item>Interlaced</item>
-        <item>Reverse Interlaced</item>
-        <item>Cardboard VR</item>
+        <item>@string/off</item>
+        <item>@string/side_by_side</item>
+        <item>@string/anaglyph</item>
+        <item>@string/interlaced</item>
+        <item>@string/reverse_interlaced</item>
+        <item>@string/cardboard_vr</item>
     </string-array>
 
     <integer-array name="render3dValues">
@@ -177,8 +152,8 @@
     </integer-array>
 
     <string-array name="graphicsApiNames">
-        <item>OpenGL ES</item>
-        <item>Vulkan</item>
+        <item>@string/opengles</item>
+        <item>@string/vulkan</item>
     </string-array>
 
     <integer-array name="graphicsApiValues">
@@ -187,12 +162,12 @@
     </integer-array>
 
     <string-array name="textureFilterNames">
-        <item>None</item>
-        <item>Anime4K</item>
-        <item>Bicubic</item>
-        <item>Nearest Neighbor</item>
-        <item>ScaleForce</item>
-        <item>xBRZ</item>
+        <item>@string/none</item>
+        <item>@string/anime4k</item>
+        <item>@string/bicubic</item>
+        <item>@string/scaleforce</item>
+        <item>@string/xbrz</item>
+        <item>@string/mmpx</item>
     </string-array>
 
     <integer-array name="textureFilterValues">
@@ -204,6 +179,17 @@
         <item>5</item>
     </integer-array>
 
+    <string-array name="themeModeEntries">
+        <item>@string/theme_mode_follow_system</item>
+        <item>@string/theme_mode_light</item>
+        <item>@string/theme_mode_dark</item>
+    </string-array>
+    <integer-array name="themeModeValues">
+        <item>-1</item>
+        <item>1</item>
+        <item>2</item>
+    </integer-array>
+
     <string-array name="systemFileRegions">
         <item>@string/system_region_jpn</item>
         <item>@string/system_region_usa</item>
@@ -233,4 +219,235 @@
         <item>2</item>
         <item>4</item>
     </integer-array>
+
+    <string-array name="soundOutputModes">
+        <item>@string/mono</item>
+        <item>@string/stereo</item>
+        <item>@string/surround</item>
+    </string-array>
+    <integer-array name="soundOutputModeValues">
+        <item>0</item>
+        <item>1</item>
+        <item>2</item>
+    </integer-array>
+
+    <string-array name="countries">
+        <item></item>
+        <item>@string/japan</item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item>@string/anguilla</item>
+        <item>@string/antigua_and_barbuda</item> <!-- 0-9 -->
+        <item>@string/argentina</item>
+        <item>@string/aruba</item>
+        <item>@string/bahamas</item>
+        <item>@string/barbados</item>
+        <item>@string/belize</item>
+        <item>@string/bolivia</item>
+        <item>@string/brazil</item>
+        <item>@string/british_virgin_islands</item>
+        <item>@string/canada</item>
+        <item>@string/cayman_islands</item> <!-- 10-19 -->
+        <item>@string/chile</item>
+        <item>@string/colombia</item>
+        <item>@string/costa_rica</item>
+        <item>@string/dominica</item>
+        <item>@string/dominican_republic</item>
+        <item>@string/ecuador</item>
+        <item>@string/el_salvador</item>
+        <item>@string/french_guiana</item>
+        <item>@string/grenada</item>
+        <item>@string/guadeloupe</item> <!-- 20-29 -->
+        <item>@string/guatemala</item>
+        <item>@string/guyana</item>
+        <item>@string/haiti</item>
+        <item>@string/honduras</item>
+        <item>@string/jamaica</item>
+        <item>@string/matinique</item>
+        <item>@string/mexico</item>
+        <item>@string/monsterrat</item>
+        <item>@string/netherlands_antilles</item>
+        <item>@string/nicaragua</item> <!-- 30-39 -->
+        <item>@string/panama</item>
+        <item>@string/paraguay</item>
+        <item>@string/peru</item>
+        <item>@string/saint_kittis_and_nevis</item>
+        <item>@string/saint_lucia</item>
+        <item>@string/saint_vincent_and_the_grenadines</item>
+        <item>@string/suriname</item>
+        <item>@string/trinidad_and_tobago</item>
+        <item>@string/turks_and_caicos_islands</item>
+        <item>@string/united_states</item> <!-- 40-49 -->
+        <item>@string/uruguay</item>
+        <item>@string/us_virgin_islands</item>
+        <item>@string/venezuela</item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item> <!-- 50-59 -->
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item>@string/albania</item>
+        <item>@string/australia</item>
+        <item>@string/austria</item>
+        <item>@string/belgium</item>
+        <item>@string/bosnia_and_herzegovnia</item>
+        <item>@string/botswana</item> <!-- 60-69 -->
+        <item>@string/bulgaria</item>
+        <item>@string/croatia</item>
+        <item>@string/cyprus</item>
+        <item>@string/czech_republic</item>
+        <item>@string/denmark</item>
+        <item>@string/estonia</item>
+        <item>@string/finland</item>
+        <item>@string/france</item>
+        <item>@string/germany</item>
+        <item>@string/greece</item> <!-- 70-79 -->
+        <item>@string/hungary</item>
+        <item>@string/iceland</item>
+        <item>@string/ireland</item>
+        <item>@string/italy</item>
+        <item>@string/latvia</item>
+        <item>@string/lesotho</item>
+        <item>@string/liechtenstein</item>
+        <item>@string/lithuania</item>
+        <item>@string/luxembourg</item>
+        <item>@string/macedonia</item> <!-- 80-89 -->
+        <item>@string/malta</item>
+        <item>@string/montenegro</item>
+        <item>@string/mozambique</item>
+        <item>@string/namibia</item>
+        <item>@string/netherlands</item>
+        <item>@string/new_zealand</item>
+        <item>@string/norway</item>
+        <item>@string/poland</item>
+        <item>@string/portugal</item>
+        <item>@string/romania</item> <!-- 90-99 -->
+        <item>@string/russia</item>
+        <item>@string/serbia</item>
+        <item>@string/slovakia</item>
+        <item>@string/slovenia</item>
+        <item>@string/south_africa</item>
+        <item>@string/spain</item>
+        <item>@string/swaziland</item>
+        <item>@string/sweden</item>
+        <item>@string/switzerland</item>
+        <item>@string/turkey</item> <!-- 100-109 -->
+        <item>@string/united_kingdom</item>
+        <item>@string/zambia</item>
+        <item>@string/zimbabwe</item>
+        <item>@string/azerbaijan</item>
+        <item>@string/mauritania</item>
+        <item>@string/mali</item>
+        <item>@string/niger</item>
+        <item>@string/chad</item>
+        <item>@string/sudan</item>
+        <item>@string/eritrea</item> <!-- 110-119 -->
+        <item>@string/djibouti</item>
+        <item>@string/somalia</item>
+        <item>@string/andorra</item>
+        <item>@string/gibraltar</item>
+        <item>@string/guernsey</item>
+        <item>@string/isle_of_man</item>
+        <item>@string/jersey</item>
+        <item>@string/monaco</item>
+        <item>@string/taiwan</item>
+        <item></item> <!-- 120-129 -->
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item>@string/south_korea</item>
+        <item></item>
+        <item></item>
+        <item></item> <!-- 130-139 -->
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item>@string/hong_kong</item>
+        <item>@string/macau</item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item> <!-- 140-149 -->
+        <item></item>
+        <item></item>
+        <item>@string/indonesia</item>
+        <item>@string/singapore</item>
+        <item>@string/thailand</item>
+        <item>@string/philippines</item>
+        <item>@string/malaysia</item>
+        <item></item>
+        <item></item>
+        <item></item> <!-- 150-159 -->
+        <item>@string/china</item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item>@string/united_arab_emirates</item>
+        <item>@string/india</item> <!-- 160-169 -->
+        <item>@string/egypt</item>
+        <item>@string/oman</item>
+        <item>@string/qatar</item>
+        <item>@string/kuwait</item>
+        <item>@string/saudi_arabia</item>
+        <item>@string/syria</item>
+        <item>@string/bahrain</item>
+        <item>@string/jordan</item>
+        <item></item>
+        <item></item> <!-- 170-179 -->
+        <item></item>
+        <item></item>
+        <item></item>
+        <item></item>
+        <item>@string/san_marino</item>
+        <item>@string/vatican_city</item>
+        <item>@string/bermuda</item> <!-- 180-189 -->
+    </string-array>
+
+    <string-array name="months">
+        <item>@string/january</item>
+        <item>@string/february</item>
+        <item>@string/march</item>
+        <item>@string/april</item>
+        <item>@string/may</item>
+        <item>@string/june</item>
+        <item>@string/july</item>
+        <item>@string/august</item>
+        <item>@string/september</item>
+        <item>@string/october</item>
+        <item>@string/november</item>
+        <item>@string/december</item>
+    </string-array>
+    <integer-array name="monthValues">
+        <item>1</item>
+        <item>2</item>
+        <item>3</item>
+        <item>4</item>
+        <item>5</item>
+        <item>6</item>
+        <item>7</item>
+        <item>8</item>
+        <item>9</item>
+        <item>10</item>
+        <item>11</item>
+        <item>12</item>
+    </integer-array>
+
 </resources>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 9c646b0357..59558a055e 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -105,9 +105,12 @@
     <string name="controller_circlepad">Circle Pad</string>
     <string name="controller_c">C-Stick</string>
     <string name="controller_triggers">Triggers</string>
+    <string name="controller_trigger">Trigger</string>
     <string name="controller_dpad">D-Pad</string>
     <string name="controller_axis_vertical">Up/Down Axis</string>
     <string name="controller_axis_horizontal">Left/Right Axis</string>
+    <string name="input_dialog_title">Bind %1$s %2$s</string>
+    <string name="input_dialog_description">Press or move an input.</string>
     <string name="input_binding">Input Binding</string>
     <string name="input_binding_description">Press or move an input to bind it to %1$s.</string>
     <string name="input_binding_description_vertical_axis">Move your joystick up or down.</string>
@@ -116,6 +119,7 @@
     <string name="button_b" translatable="false">B</string>
     <string name="button_select" translatable="false">SELECT</string>
     <string name="button_start" translatable="false">START</string>
+    <string name="button_home">HOME</string>
     <string name="button_x" translatable="false">X</string>
     <string name="button_y" translatable="false">Y</string>
     <string name="button_l" translatable="false">L</string>
@@ -137,13 +141,6 @@
     <string name="how_to_get_keys"><![CDATA[<a href="https://citra-emu.org/wiki/aes-keys/">How to get keys?</a>]]></string>
     <string name="show_home_apps">Show HOME menu apps in games list</string>
     <string name="run_system_setup">Run System Setup when the HOME Menu is launched</string>
-    <string name="system_region_jpn">JPN</string>
-    <string name="system_region_usa">USA</string>
-    <string name="system_region_eur">EUR</string>
-    <string name="system_region_aus">AUS</string>
-    <string name="system_region_chn">CHN</string>
-    <string name="system_region_kor">KOR</string>
-    <string name="system_region_twn">TWN</string>
     <string name="system_type_minimal">Minimal</string>
     <string name="system_type_old_3ds">Old 3DS</string>
     <string name="system_type_new_3ds">New 3DS</string>
@@ -161,19 +158,31 @@
 
     <!-- Generic buttons (Shared with lots of stuff) -->
     <string name="generic_buttons">Buttons</string>
+    <string name="button">Button</string>
 
     <!-- Core settings strings -->
     <string name="cpu_jit">CPU JIT</string>
     <string name="cpu_jit_description">Uses the Just-in-Time (JIT) compiler for CPU emulation. When enabled, game performance will be significantly improved.</string>
     <string name="init_clock">Clock</string>
     <string name="init_clock_description">Set the emulated 3DS clock to either reflect that of your device or start at a simulated date and time.</string>
+    <string name="cpu_clock_speed">CPU Clock Speed</string>
 
     <!-- System settings strings -->
+    <string name="username">Username</string>
+    <string name="new_3ds">New 3DS Mode</string>
     <string name="clock">Clock</string>
     <string name="init_time">Offset Time</string>
     <string name="init_time_description">If the clock is set to \"Simulated clock\", this changes the fixed date and time to start at.</string>
     <string name="emulated_region">Region</string>
     <string name="emulated_language">Language</string>
+    <string name="birthday">Birthday</string>
+    <string name="birthday_month">Month</string>
+    <string name="birthday_day">Day</string>
+    <string name="country">Country</string>
+    <string name="play_coins">Play Coins</string>
+    <string name="console_id">Console ID</string>
+    <string name="regenerate_console_id">Regenerate Console ID</string>
+    <string name="regenerate_console_id_description">This will replace your current virtual 3DS with a new one. Your current virtual 3DS will not be recoverable. This might have unexpected effects in games. This might fail if you use an outdated config savegame. Continue?</string>
     <string name="plugin_loader">3GX Plugin Loader</string>
     <string name="plugin_loader_description">Loads 3GX plugins from the emulated SD card if they are available.</string>
     <string name="allow_plugin_loader">Allow Games to Change Plugin Loader State</string>
@@ -246,9 +255,11 @@
     <string name="async_custom_loading_description">Load custom textures asynchronously with background threads to reduce loading stutter.</string>
 
     <!-- Audio settings strings -->
+    <string name="audio_volume">Volume</string>
     <string name="audio_stretch">Audio Stretching</string>
     <string name="audio_stretch_description">Stretches audio to reduce stuttering. When enabled, increases audio latency and slightly reduces performance.</string>
     <string name="audio_input_type">Audio Input Device</string>
+    <string name="sound_output_mode">Sound Output Mode</string>
 
     <!-- Miscellaneous -->
     <string name="clear">Clear</string>
@@ -261,7 +272,7 @@
     <string name="back">Back</string>
     <string name="learn_more">Learn More</string>
     <string name="close">Close</string>
-    <string name="reset_to_default">Reset to default</string>
+    <string name="reset_to_default">Reset to Default</string>
     <string name="redump_games"><![CDATA[Please follow the guides to redump your <a href="https://citra-emu.org/wiki/dumping-game-cartridges/">game cartidges</a> or <a href="https://citra-emu.org/wiki/dumping-installed-titles/">installed titles</a>.]]></string>
     <string name="option_default">Default</string>
     <string name="none">None</string>
@@ -269,6 +280,15 @@
     <string name="off">Off</string>
     <string name="install">Install</string>
     <string name="delete">Delete</string>
+    <string name="reset_all_settings">Reset All Settings?</string>
+    <string name="reset_all_settings_description">All advanced settings will be reset to their default configuration. This can not be undone.</string>
+    <string name="settings_reset">Settings reset</string>
+    <string name="select_rtc_date">Select RTC date</string>
+    <string name="select_rtc_time">Select RTC time</string>
+    <string name="reset_setting_confirmation">Do you want to reset this setting back to its default value?</string>
+    <string name="setting_not_editable">You can\'t edit this now</string>
+    <string name="setting_not_editable_description">This option can\'t be changed while a game is running.</string>
+    <string name="auto_select">Auto-Select</string>
 
     <!-- Add Directory Screen-->
     <string name="select_game_folder">Select Game Folder</string>
@@ -286,6 +306,7 @@
     <string name="preferences_graphics">Graphics</string>
     <string name="preferences_audio">Audio</string>
     <string name="preferences_debug">Debug</string>
+    <string name="preferences_theme">Theme and Color</string>
 
     <!-- ROM loading errors -->
     <string name="loader_error_encrypted">Your ROM is Encrypted</string>
@@ -408,4 +429,235 @@
     <string name="cia_install_error_invalid">\"%s\" is not a valid CIA</string>
     <string name="cia_install_error_encrypted">\"%s\" must be decrypted before being used with Citra.\n A real 3DS is required</string>
     <string name="cia_install_error_unknown">An unknown error occurred while installing \"%s\".\n Please see the log for more details</string>
+
+    <!-- Theme Modes -->
+    <string name="change_theme_mode">Change Theme Mode</string>
+    <string name="theme_mode_follow_system">Follow System</string>
+    <string name="theme_mode_light">Light</string>
+    <string name="theme_mode_dark">Dark</string>
+    <string name="material_you">Material You</string>
+    <string name="material_you_description">Use system colors across the app</string>
+
+    <!-- Black backgrounds theme -->
+    <string name="use_black_backgrounds">Black Backgrounds</string>
+    <string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string>
+
+    <!-- Clock types -->
+    <string name="device_clock">Device Clock</string>
+    <string name="simulated_clock">Simulated Clock</string>
+
+    <!-- Region names -->
+    <string name="system_region_jpn">JPN</string>
+    <string name="system_region_usa">USA</string>
+    <string name="system_region_eur">EUR</string>
+    <string name="system_region_aus">AUS</string>
+    <string name="system_region_chn">CHN</string>
+    <string name="system_region_kor">KOR</string>
+    <string name="system_region_twn">TWN</string>
+
+    <!-- Language names -->
+    <string name="language_japanese">Japanese (日本語)</string>
+    <string name="language_english">English</string>
+    <string name="language_french">French (Français)</string>
+    <string name="language_german">German (Deutsch)</string>
+    <string name="language_italian">Italian (Italiano)</string>
+    <string name="language_spanish">Spanish (Español)</string>
+    <string name="language_simplified_chinese">Simplified Chinese (简体中文)</string>
+    <string name="language_korean">Korean (한국어)</string>
+    <string name="language_dutch">Dutch (Nederlands)</string>
+    <string name="language_portuguese">Portuguese (Português)</string>
+    <string name="language_russian">Russian (Русский)</string>
+    <string name="language_traditional_chinese">Traditional Chinese (正體中文)</string>
+
+    <!-- Camera image sources -->
+    <string name="blank">Blank</string>
+    <string name="still_image">Still Image</string>
+    <string name="device_camera">Device Camera</string>
+
+    <!-- Camera device names -->
+    <string name="any_front_camera">Any Front Camera</string>
+    <string name="any_back_camera">Any Back Camera</string>
+
+    <!-- Camera flip names -->
+    <string name="horizontal">Horizontal</string>
+    <string name="vertical">Vertical</string>
+    <string name="reverse">Reverse</string>
+
+    <!-- Audio input types -->
+    <string name="static_noise">Static Noise</string>
+    <string name="real_cubeb">Real Device (cubeb)</string>
+    <string name="real_openal">Real Device (OpenAL)</string>
+
+    <!-- Render 3D modes -->
+    <string name="side_by_side">Side by Side</string>
+    <string name="anaglyph">Anaglyph</string>
+    <string name="interlaced">Interlaced</string>
+    <string name="reverse_interlaced">Reverse Interlaced</string>
+
+    <!-- Graphics API names -->
+    <string name="opengles">OpenGLES</string>
+    <string name="vulkan">Vulkan</string>
+
+    <!-- Texture filter names -->
+    <string name="anime4k">Anime4K</string>
+    <string name="bicubic">Bicubic</string>
+    <string name="nearest_neighbor">Nearest Neighbor</string>
+    <string name="scaleforce">ScaleForce</string>
+    <string name="xbrz">xBRZ</string>
+    <string name="mmpx">MMPX</string>
+
+    <!-- Sound output modes -->
+    <string name="mono">Mono</string>
+    <string name="stereo">Stereo</string>
+    <string name="surround">Surround</string>
+
+    <!-- Countries -->
+    <string name="japan">Japan</string>
+    <string name="anguilla">Anguilla</string>
+    <string name="antigua_and_barbuda">Antigua and Barbuda</string>
+    <string name="argentina">Argentina</string>
+    <string name="aruba">Aruba</string>
+    <string name="bahamas">Bahamas</string>
+    <string name="barbados">Barbados</string>
+    <string name="belize">Belize</string>
+    <string name="bolivia">Bolivia</string>
+    <string name="brazil">Brazil</string>
+    <string name="british_virgin_islands">British Virgin Islands</string>
+    <string name="canada">Canada</string>
+    <string name="cayman_islands">Cayman Islands</string>
+    <string name="chile">Chile</string>
+    <string name="colombia">Colombia</string>
+    <string name="costa_rica">Costa Rica</string>
+    <string name="dominica">Dominica</string>
+    <string name="dominican_republic">Dominican Republic</string>
+    <string name="ecuador">Ecuador</string>
+    <string name="el_salvador">El Salvador</string>
+    <string name="french_guiana">French Guiana</string>
+    <string name="grenada">Grenada</string>
+    <string name="guadeloupe">Guadeloupe</string>
+    <string name="guatemala">Guatemala</string>
+    <string name="guyana">Guyana</string>
+    <string name="haiti">Haiti</string>
+    <string name="honduras">Honduras</string>
+    <string name="jamaica">Jamaica</string>
+    <string name="matinique">Martinique</string>
+    <string name="mexico">Mexico</string>
+    <string name="monsterrat">Montserrat</string>
+    <string name="netherlands_antilles">Netherlands Antilles</string>
+    <string name="nicaragua">Nicaragua</string>
+    <string name="panama">Panama</string>
+    <string name="paraguay">Paraguay</string>
+    <string name="peru">Peru</string>
+    <string name="saint_kittis_and_nevis">Saint Kitts and Nevis</string>
+    <string name="saint_lucia">Saint Lucia</string>
+    <string name="saint_vincent_and_the_grenadines">Saint Vincent and the Grenadines</string>
+    <string name="suriname">Suriname</string>
+    <string name="trinidad_and_tobago">Trinidad and Tobago</string>
+    <string name="turks_and_caicos_islands">Turks and Caicos Islands</string>
+    <string name="united_states">United States</string>
+    <string name="uruguay">Uruguay</string>
+    <string name="us_virgin_islands">US Virgin Islands</string>
+    <string name="venezuela">Venezuela</string>
+    <string name="albania">Albania</string>
+    <string name="australia">Australia</string>
+    <string name="austria">Austria</string>
+    <string name="belgium">Belgium</string>
+    <string name="bosnia_and_herzegovnia">Bosnia and Herzegovina</string>
+    <string name="botswana">Botswana</string>
+    <string name="bulgaria">Bulgaria</string>
+    <string name="croatia">Croatia</string>
+    <string name="cyprus">Cyprus</string>
+    <string name="czech_republic">Czech Republic</string>
+    <string name="denmark">Denmark</string>
+    <string name="estonia">Estonia</string>
+    <string name="finland">Finland</string>
+    <string name="france">France</string>
+    <string name="germany">Germany</string>
+    <string name="greece">Greece</string>
+    <string name="hungary">Hungary</string>
+    <string name="iceland">Iceland</string>
+    <string name="ireland">Ireland</string>
+    <string name="italy">Italy</string>
+    <string name="latvia">Latvia</string>
+    <string name="lesotho">Lesotho</string>
+    <string name="liechtenstein">Liechtenstein</string>
+    <string name="lithuania">Lithuania</string>
+    <string name="luxembourg">Luxembourg</string>
+    <string name="macedonia">Macedonia</string>
+    <string name="malta">Malta</string>
+    <string name="montenegro">Montenegro</string>
+    <string name="mozambique">Mozambique</string>
+    <string name="namibia">Namibia</string>
+    <string name="netherlands">Netherlands</string>
+    <string name="new_zealand">New Zealand</string>
+    <string name="norway">Norway</string>
+    <string name="poland">Poland</string>
+    <string name="portugal">Portugal</string>
+    <string name="romania">Romania</string>
+    <string name="russia">Russia</string>
+    <string name="serbia">Serbia</string>
+    <string name="slovakia">Slovakia</string>
+    <string name="slovenia">Slovenia</string>
+    <string name="south_africa">South Africa</string>
+    <string name="spain">Spain</string>
+    <string name="swaziland">Swaziland</string>
+    <string name="sweden">Sweden</string>
+    <string name="switzerland">Switzerland</string>
+    <string name="turkey">Turkey</string>
+    <string name="united_kingdom">United Kingdom</string>
+    <string name="zambia">Zambia</string>
+    <string name="zimbabwe">Zimbabwe</string>
+    <string name="azerbaijan">Azerbaijan</string>
+    <string name="mauritania">Mauritania</string>
+    <string name="mali">Mali</string>
+    <string name="niger">Niger</string>
+    <string name="chad">Chad</string>
+    <string name="sudan">Sudan</string>
+    <string name="eritrea">Eritrea</string>
+    <string name="djibouti">Djibouti</string>
+    <string name="somalia">Somalia</string>
+    <string name="andorra">Andorra</string>
+    <string name="gibraltar">Gibraltar</string>
+    <string name="guernsey">Guernsey</string>
+    <string name="isle_of_man">Isle of Man</string>
+    <string name="jersey">Jersey</string>
+    <string name="monaco">Monaco</string>
+    <string name="taiwan">Taiwan</string>
+    <string name="south_korea">South Korea</string>
+    <string name="hong_kong">Hong Kong</string>
+    <string name="macau">Macau</string>
+    <string name="indonesia">Indonesia</string>
+    <string name="singapore">Singapore</string>
+    <string name="thailand">Thailand</string>
+    <string name="philippines">Philippines</string>
+    <string name="malaysia">Malaysia</string>
+    <string name="china">China</string>
+    <string name="united_arab_emirates">United Arab Emirates</string>
+    <string name="india">India</string>
+    <string name="egypt">Egypt</string>
+    <string name="oman">Oman</string>
+    <string name="qatar">Qatar</string>
+    <string name="kuwait">Kuwait</string>
+    <string name="saudi_arabia">Saudi Arabia</string>
+    <string name="syria">Syria</string>
+    <string name="bahrain">Bahrain</string>
+    <string name="jordan">Jordan</string>
+    <string name="san_marino">San Marino</string>
+    <string name="vatican_city">Vatican City</string>
+    <string name="bermuda">Bermuda</string>
+
+    <!-- Months -->
+    <string name="january">January</string>
+    <string name="february">February</string>
+    <string name="march">March</string>
+    <string name="april">April</string>
+    <string name="may">May</string>
+    <string name="june">June</string>
+    <string name="july">July</string>
+    <string name="august">August</string>
+    <string name="september">September</string>
+    <string name="october">October</string>
+    <string name="november">November</string>
+    <string name="december">December</string>
+
 </resources>