From 762ddfd07bd0c24c5fd99c202535d71410f9cc88 Mon Sep 17 00:00:00 2001
From: Charles Lombardo <clombardo169@gmail.com>
Date: Sun, 17 Dec 2023 20:32:30 -0500
Subject: [PATCH] Android UI Overhaul Part 4/4 (#7235)

* android: Rework cheats

Reworks cheats to use the navigation component, kotlin, and a tweaked layout for a better tuned look.

* android: Convert remaining files to kotlin and add overlay home button

* android: Remove Picasso dependency

* android: Fix home option layout centering

* android: Adjust logo size in-app
---
 src/android/app/build.gradle.kts              |    4 -
 .../citra/citra_emu/adapters/GameAdapter.kt   |    6 +-
 .../citra/citra_emu/applets/MiiSelector.java  |  129 --
 .../citra/citra_emu/applets/MiiSelector.kt    |   47 +
 .../citra_emu/applets/SoftwareKeyboard.java   |  279 -----
 .../citra_emu/applets/SoftwareKeyboard.kt     |  152 +++
 .../contracts/OpenFileResultContract.java     |   24 -
 .../contracts/OpenFileResultContract.kt       |   19 +
 .../features/cheats/model/Cheat.java          |   57 -
 .../citra_emu/features/cheats/model/Cheat.kt  |   48 +
 .../features/cheats/model/CheatEngine.java    |   28 -
 .../features/cheats/model/CheatEngine.kt      |   31 +
 .../cheats/model/CheatsViewModel.java         |  187 ---
 .../features/cheats/model/CheatsViewModel.kt  |  169 +++
 .../cheats/ui/CheatDetailsFragment.java       |  175 ---
 .../cheats/ui/CheatDetailsFragment.kt         |  193 +++
 .../features/cheats/ui/CheatListFragment.java |   71 --
 .../features/cheats/ui/CheatListFragment.kt   |  143 +++
 .../features/cheats/ui/CheatViewHolder.java   |   56 -
 .../features/cheats/ui/CheatsActivity.java    |  235 ----
 .../features/cheats/ui/CheatsActivity.kt      |   63 +
 .../features/cheats/ui/CheatsAdapter.java     |   72 --
 .../features/cheats/ui/CheatsAdapter.kt       |   69 ++
 .../features/cheats/ui/CheatsFragment.kt      |  244 ++++
 .../model/{view => }/AbstractShortSetting.kt  |    4 +-
 .../model/view/SingleChoiceSetting.kt         |    1 +
 .../model/view/StringSingleChoiceSetting.kt   |    1 +
 .../features/settings/ui/SettingsAdapter.kt   |    2 +-
 .../settings/ui/SettingsFragmentPresenter.kt  |    2 +-
 .../citra_emu/fragments/EmulationFragment.kt  |    3 -
 .../fragments/KeyboardDialogFragment.kt       |  115 ++
 .../fragments/MiiSelectorDialogFragment.kt    |   60 +
 .../citra/citra_emu/model/CheapDocument.java  |   36 -
 .../citra/citra_emu/model/CheapDocument.kt    |   17 +
 .../citra/citra_emu/overlay/InputOverlay.java |  766 ------------
 .../citra/citra_emu/overlay/InputOverlay.kt   | 1051 +++++++++++++++++
 .../overlay/InputOverlayDrawableButton.java   |  159 ---
 .../overlay/InputOverlayDrawableButton.kt     |  128 ++
 .../overlay/InputOverlayDrawableDpad.java     |  299 -----
 .../overlay/InputOverlayDrawableDpad.kt       |  262 ++++
 .../overlay/InputOverlayDrawableJoystick.java |  267 -----
 .../overlay/InputOverlayDrawableJoystick.kt   |  238 ++++
 .../citra_emu/ui/DividerItemDecoration.java   |  130 --
 .../ui/TwoPaneOnBackPressedCallback.java      |   46 -
 .../ui/TwoPaneOnBackPressedCallback.kt        |   40 +
 .../org/citra/citra_emu/utils/Action1.java    |    5 -
 .../java/org/citra/citra_emu/utils/BiMap.java |   22 -
 .../java/org/citra/citra_emu/utils/BiMap.kt   |   22 +
 .../citra_emu/utils/CiaInstallWorker.java     |  153 ---
 .../citra/citra_emu/utils/CiaInstallWorker.kt |  168 +++
 .../citra_emu/utils/FileBrowserHelper.java    |   50 -
 .../citra_emu/utils/FileBrowserHelper.kt      |   44 +
 .../citra/citra_emu/utils/InsetsHelper.java   |   33 -
 .../org/citra/citra_emu/utils/InsetsHelper.kt |   25 +
 .../java/org/citra/citra_emu/utils/Log.java   |   42 -
 .../java/org/citra/citra_emu/utils/Log.kt     |   37 +
 .../citra/citra_emu/utils/PicassoUtils.java   |   27 -
 .../citra_emu/viewholders/GameViewHolder.java |   46 -
 .../app/src/main/jni/applets/mii_selector.cpp |    9 +-
 .../app/src/main/jni/applets/swkbd.cpp        |   10 +-
 .../app/src/main/res/drawable/button_home.xml |   16 +
 .../main/res/drawable/button_home_pressed.xml |   16 +
 .../main/res/layout-ldrtl/list_item_cheat.xml |   34 +-
 .../src/main/res/layout/activity_cheats.xml   |   66 +-
 .../src/main/res/layout/card_home_option.xml  |   12 +-
 .../src/main/res/layout/fragment_about.xml    |    4 +-
 .../res/layout/fragment_cheat_details.xml     |  286 ++---
 .../main/res/layout/fragment_cheat_list.xml   |   36 +-
 .../src/main/res/layout/fragment_cheats.xml   |   26 +
 .../res/layout/fragment_home_settings.xml     |    6 +-
 .../src/main/res/layout/list_item_cheat.xml   |   28 +-
 .../main/res/navigation/cheats_navigation.xml |   19 +
 .../main/res/navigation/home_navigation.xml   |   14 +
 .../app/src/main/res/values/arrays.xml        |    1 +
 .../app/src/main/res/values/integers.xml      |    6 +-
 .../app/src/main/res/values/strings.xml       |    1 +
 76 files changed, 3738 insertions(+), 3654 deletions(-)
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/applets/SoftwareKeyboard.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/applets/SoftwareKeyboard.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/contracts/OpenFileResultContract.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/contracts/OpenFileResultContract.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/Cheat.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/Cheat.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatViewHolder.java
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsFragment.kt
 rename src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/{view => }/AbstractShortSetting.kt (62%)
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/fragments/KeyboardDialogFragment.kt
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/fragments/MiiSelectorDialogFragment.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/model/CheapDocument.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/model/CheapDocument.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/ui/DividerItemDecoration.java
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/ui/TwoPaneOnBackPressedCallback.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/ui/TwoPaneOnBackPressedCallback.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/Action1.java
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/BiMap.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/BiMap.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/CiaInstallWorker.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/CiaInstallWorker.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/InsetsHelper.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/InsetsHelper.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/Log.java
 create mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/Log.kt
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoUtils.java
 delete mode 100644 src/android/app/src/main/java/org/citra/citra_emu/viewholders/GameViewHolder.java
 create mode 100644 src/android/app/src/main/res/drawable/button_home.xml
 create mode 100644 src/android/app/src/main/res/drawable/button_home_pressed.xml
 create mode 100644 src/android/app/src/main/res/layout/fragment_cheats.xml
 create mode 100644 src/android/app/src/main/res/navigation/cheats_navigation.xml

diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts
index cc24879e9..001dcc546 100644
--- a/src/android/app/build.gradle.kts
+++ b/src/android/app/build.gradle.kts
@@ -178,10 +178,6 @@ dependencies {
     implementation("com.google.android.material:material:1.9.0")
     implementation("androidx.core:core-splashscreen:1.0.1")
     implementation("androidx.work:work-runtime:2.8.1")
-
-    // For loading huge screenshots from the disk.
-    implementation("com.squareup.picasso:picasso:2.71828")
-
     implementation("org.ini4j:ini4j:0.5.4")
     implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
     implementation("androidx.navigation:navigation-fragment-ktx:2.7.5")
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt
index e84aacb1e..cc0a2b750 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/adapters/GameAdapter.kt
@@ -26,10 +26,9 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import org.citra.citra_emu.HomeNavigationDirections
 import org.citra.citra_emu.CitraApplication
 import org.citra.citra_emu.R
-import org.citra.citra_emu.activities.EmulationActivity
 import org.citra.citra_emu.adapters.GameAdapter.GameViewHolder
 import org.citra.citra_emu.databinding.CardGameBinding
-import org.citra.citra_emu.features.cheats.ui.CheatsActivity
+import org.citra.citra_emu.features.cheats.ui.CheatsFragmentDirections
 import org.citra.citra_emu.model.Game
 import org.citra.citra_emu.utils.GameIconUtils
 import org.citra.citra_emu.viewmodel.GamesViewModel
@@ -100,7 +99,8 @@ class GameAdapter(private val activity: AppCompatActivity) :
                 .setPositiveButton(android.R.string.ok, null)
                 .show()
         } else {
-            CheatsActivity.launch(view.context, holder.game.titleId)
+            val action = CheatsFragmentDirections.actionGlobalCheatsFragment(holder.game.titleId)
+            view.findNavController().navigate(action)
         }
         return true
     }
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.java b/src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.java
deleted file mode 100644
index 67f51bc6d..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.java
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright 2020 Citra Emulator Project
-// Licensed under GPLv2 or any later version
-// Refer to the license.txt file included.
-
-package org.citra.citra_emu.applets;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.DialogInterface;
-import android.os.Bundle;
-
-import org.citra.citra_emu.NativeLibrary;
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.activities.EmulationActivity;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Objects;
-
-import androidx.annotation.Keep;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-
-@Keep
-public final class MiiSelector {
-    @Keep
-    public static class MiiSelectorConfig implements java.io.Serializable {
-        public boolean enable_cancel_button;
-        public String title;
-        public long initially_selected_mii_index;
-        // List of Miis to display
-        public String[] mii_names;
-    }
-
-    public static class MiiSelectorData {
-        public long return_code;
-        public int index;
-
-        private MiiSelectorData(long return_code, int index) {
-            this.return_code = return_code;
-            this.index = index;
-        }
-    }
-
-    public static class MiiSelectorDialogFragment extends DialogFragment {
-        static MiiSelectorDialogFragment newInstance(MiiSelectorConfig config) {
-            MiiSelectorDialogFragment frag = new MiiSelectorDialogFragment();
-            Bundle args = new Bundle();
-            args.putSerializable("config", config);
-            frag.setArguments(args);
-            return frag;
-        }
-
-        @NonNull
-        @Override
-        public Dialog onCreateDialog(Bundle savedInstanceState) {
-            final Activity emulationActivity = Objects.requireNonNull(getActivity());
-
-            MiiSelectorConfig config =
-                    Objects.requireNonNull((MiiSelectorConfig) Objects.requireNonNull(getArguments())
-                            .getSerializable("config"));
-
-            // Note: we intentionally leave out the Standard Mii in the native code so that
-            // the string can get translated
-            ArrayList<String> list = new ArrayList<>();
-            list.add(emulationActivity.getString(R.string.standard_mii));
-            list.addAll(Arrays.asList(config.mii_names));
-
-            final int initialIndex = config.initially_selected_mii_index < list.size()
-                    ? (int) config.initially_selected_mii_index
-                    : 0;
-            data.index = initialIndex;
-            MaterialAlertDialogBuilder builder =
-                    new MaterialAlertDialogBuilder(emulationActivity)
-                            .setTitle(config.title.isEmpty()
-                                    ? emulationActivity.getString(R.string.mii_selector)
-                                    : config.title)
-                            .setSingleChoiceItems(list.toArray(new String[]{}), initialIndex,
-                                    (dialog, which) -> {
-                                        data.index = which;
-                                    })
-                            .setPositiveButton(android.R.string.ok, (dialog, which) -> {
-                                data.return_code = 0;
-                                synchronized (finishLock) {
-                                    finishLock.notifyAll();
-                                }
-                            });
-            if (config.enable_cancel_button) {
-                builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
-                    data.return_code = 1;
-                    synchronized (finishLock) {
-                        finishLock.notifyAll();
-                    }
-                });
-            }
-            setCancelable(false);
-            return builder.create();
-        }
-    }
-
-    private static MiiSelectorData data;
-    private static final Object finishLock = new Object();
-
-    private static void ExecuteImpl(MiiSelectorConfig config) {
-        final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
-
-        data = new MiiSelectorData(0, 0);
-
-        MiiSelectorDialogFragment fragment = MiiSelectorDialogFragment.newInstance(config);
-        fragment.show(emulationActivity.getSupportFragmentManager(), "mii_selector");
-    }
-
-    public static MiiSelectorData Execute(MiiSelectorConfig config) {
-        NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config));
-
-        synchronized (finishLock) {
-            try {
-                finishLock.wait();
-            } catch (Exception ignored) {
-            }
-        }
-
-        return data;
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.kt b/src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.kt
new file mode 100644
index 000000000..e7dbfbf66
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/applets/MiiSelector.kt
@@ -0,0 +1,47 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.applets
+
+import androidx.annotation.Keep
+import org.citra.citra_emu.NativeLibrary
+import org.citra.citra_emu.fragments.MiiSelectorDialogFragment
+import java.io.Serializable
+
+@Keep
+object MiiSelector {
+    lateinit var data: MiiSelectorData
+    val finishLock = Object()
+
+    private fun ExecuteImpl(config: MiiSelectorConfig) {
+        val emulationActivity = NativeLibrary.sEmulationActivity.get()
+        data = MiiSelectorData(0, 0)
+        val fragment = MiiSelectorDialogFragment.newInstance(config)
+        fragment.show(emulationActivity!!.supportFragmentManager, "mii_selector")
+    }
+
+    @JvmStatic
+    fun Execute(config: MiiSelectorConfig): MiiSelectorData {
+        NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { ExecuteImpl(config) }
+        synchronized(finishLock) {
+            try {
+                finishLock.wait()
+            } catch (ignored: Exception) {
+            }
+        }
+        return data
+    }
+
+    @Keep
+    class MiiSelectorConfig : Serializable {
+        var enableCancelButton = false
+        var title: String? = null
+        var initiallySelectedMiiIndex: Long = 0
+
+        // List of Miis to display
+        lateinit var miiNames: Array<String>
+    }
+
+    class MiiSelectorData (var returnCode: Long, var index: Int)
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/applets/SoftwareKeyboard.java b/src/android/app/src/main/java/org/citra/citra_emu/applets/SoftwareKeyboard.java
deleted file mode 100644
index 77b02a6f0..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/applets/SoftwareKeyboard.java
+++ /dev/null
@@ -1,279 +0,0 @@
-// Copyright 2020 Citra Emulator Project
-// Licensed under GPLv2 or any later version
-// Refer to the license.txt file included.
-
-package org.citra.citra_emu.applets;
-
-import android.app.Activity;
-import android.app.Dialog;
-import android.content.DialogInterface;
-import android.content.res.Resources;
-import android.os.Bundle;
-import android.text.InputFilter;
-import android.text.Spanned;
-import android.util.TypedValue;
-import android.view.ViewGroup;
-import android.widget.EditText;
-import android.widget.FrameLayout;
-
-import androidx.annotation.ColorInt;
-import androidx.annotation.Keep;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.appcompat.app.AlertDialog;
-import androidx.fragment.app.DialogFragment;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-
-import org.citra.citra_emu.CitraApplication;
-import org.citra.citra_emu.NativeLibrary;
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.activities.EmulationActivity;
-import org.citra.citra_emu.utils.Log;
-
-import java.util.Objects;
-
-@Keep
-public final class SoftwareKeyboard {
-    /// Corresponds to Frontend::ButtonConfig
-    private interface ButtonConfig {
-        int Single = 0; /// Ok button
-        int Dual = 1;   /// Cancel | Ok buttons
-        int Triple = 2; /// Cancel | I Forgot | Ok buttons
-        int None = 3;   /// No button (returned by swkbdInputText in special cases)
-    }
-
-    /// Corresponds to Frontend::ValidationError
-    public enum ValidationError {
-        None,
-        // Button Selection
-        ButtonOutOfRange,
-        // Configured Filters
-        MaxDigitsExceeded,
-        AtSignNotAllowed,
-        PercentNotAllowed,
-        BackslashNotAllowed,
-        ProfanityNotAllowed,
-        CallbackFailed,
-        // Allowed Input Type
-        FixedLengthRequired,
-        MaxLengthExceeded,
-        BlankInputNotAllowed,
-        EmptyInputNotAllowed,
-    }
-
-    @Keep
-    public static class KeyboardConfig implements java.io.Serializable {
-        public int button_config;
-        public int max_text_length;
-        public boolean multiline_mode; /// True if the keyboard accepts multiple lines of input
-        public String hint_text;       /// Displayed in the field as a hint before
-        @Nullable
-        public String[] button_text; /// Contains the button text that the caller provides
-    }
-
-    /// Corresponds to Frontend::KeyboardData
-    public static class KeyboardData {
-        public int button;
-        public String text;
-
-        private KeyboardData(int button, String text) {
-            this.button = button;
-            this.text = text;
-        }
-    }
-
-    private static class Filter implements InputFilter {
-        @Override
-        public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
-                                   int dstart, int dend) {
-            String text = new StringBuilder(dest)
-                    .replace(dstart, dend, source.subSequence(start, end).toString())
-                    .toString();
-            if (ValidateFilters(text) == ValidationError.None) {
-                return null; // Accept replacement
-            }
-            return dest.subSequence(dstart, dend); // Request the subsequence to be unchanged
-        }
-    }
-
-    public static class KeyboardDialogFragment extends DialogFragment {
-        static KeyboardDialogFragment newInstance(KeyboardConfig config) {
-            KeyboardDialogFragment frag = new KeyboardDialogFragment();
-            Bundle args = new Bundle();
-            args.putSerializable("config", config);
-            frag.setArguments(args);
-            return frag;
-        }
-
-        @NonNull
-        @Override
-        public Dialog onCreateDialog(Bundle savedInstanceState) {
-            final Activity emulationActivity = getActivity();
-            assert emulationActivity != null;
-
-            FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
-                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
-            params.leftMargin = params.rightMargin =
-                    CitraApplication.Companion.getAppContext().getResources().getDimensionPixelSize(
-                            R.dimen.dialog_margin);
-
-            KeyboardConfig config = Objects.requireNonNull(
-                    (KeyboardConfig) Objects.requireNonNull(getArguments()).getSerializable("config"));
-
-            // Set up the input
-            EditText editText = new EditText(CitraApplication.Companion.getAppContext());
-            editText.setHint(config.hint_text);
-            editText.setSingleLine(!config.multiline_mode);
-            editText.setLayoutParams(params);
-            editText.setFilters(new InputFilter[]{
-                    new Filter(), new InputFilter.LengthFilter(config.max_text_length)});
-
-            TypedValue typedValue = new TypedValue();
-            Resources.Theme theme = requireContext().getTheme();
-            theme.resolveAttribute(R.attr.colorOnSurface, typedValue, true);
-            @ColorInt int color = typedValue.data;
-            editText.setHintTextColor(color);
-            editText.setTextColor(color);
-
-            FrameLayout container = new FrameLayout(emulationActivity);
-            container.addView(editText);
-
-            MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
-                    .setTitle(R.string.software_keyboard)
-                    .setView(container);
-            setCancelable(false);
-
-            switch (config.button_config) {
-                case ButtonConfig.Triple: {
-                    final String text = config.button_text[1].isEmpty()
-                            ? emulationActivity.getString(R.string.i_forgot)
-                            : config.button_text[1];
-                    builder.setNeutralButton(text, null);
-                }
-                // fallthrough
-                case ButtonConfig.Dual: {
-                    final String text = config.button_text[0].isEmpty()
-                            ? emulationActivity.getString(android.R.string.cancel)
-                            : config.button_text[0];
-                    builder.setNegativeButton(text, null);
-                }
-                // fallthrough
-                case ButtonConfig.Single: {
-                    final String text = config.button_text[2].isEmpty()
-                            ? emulationActivity.getString(android.R.string.ok)
-                            : config.button_text[2];
-                    builder.setPositiveButton(text, null);
-                    break;
-                }
-            }
-
-            final AlertDialog dialog = builder.create();
-            dialog.create();
-            if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) {
-                dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> {
-                    data.button = config.button_config;
-                    data.text = editText.getText().toString();
-                    final ValidationError error = ValidateInput(data.text);
-                    if (error != ValidationError.None) {
-                        HandleValidationError(config, error);
-                        return;
-                    }
-
-                    dialog.dismiss();
-
-                    synchronized (finishLock) {
-                        finishLock.notifyAll();
-                    }
-                });
-            }
-            if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) {
-                dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> {
-                    data.button = 1;
-                    dialog.dismiss();
-                    synchronized (finishLock) {
-                        finishLock.notifyAll();
-                    }
-                });
-            }
-            if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) {
-                dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> {
-                    data.button = 0;
-                    dialog.dismiss();
-                    synchronized (finishLock) {
-                        finishLock.notifyAll();
-                    }
-                });
-            }
-
-            return dialog;
-        }
-    }
-
-    private static KeyboardData data;
-    private static final Object finishLock = new Object();
-
-    private static void ExecuteImpl(KeyboardConfig config) {
-        final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
-
-        data = new KeyboardData(0, "");
-
-        KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config);
-        fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard");
-    }
-
-    private static void HandleValidationError(KeyboardConfig config, ValidationError error) {
-        final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
-        String message = "";
-        switch (error) {
-            case FixedLengthRequired:
-                message =
-                        emulationActivity.getString(R.string.fixed_length_required, config.max_text_length);
-                break;
-            case MaxLengthExceeded:
-                message =
-                        emulationActivity.getString(R.string.max_length_exceeded, config.max_text_length);
-                break;
-            case BlankInputNotAllowed:
-                message = emulationActivity.getString(R.string.blank_input_not_allowed);
-                break;
-            case EmptyInputNotAllowed:
-                message = emulationActivity.getString(R.string.empty_input_not_allowed);
-                break;
-        }
-
-        new MaterialAlertDialogBuilder(emulationActivity)
-                .setTitle(R.string.software_keyboard)
-                .setMessage(message)
-                .setPositiveButton(android.R.string.ok, null)
-                .show();
-    }
-
-    public static KeyboardData Execute(KeyboardConfig config) {
-        if (config.button_config == ButtonConfig.None) {
-            Log.error("Unexpected button config None");
-            return new KeyboardData(0, "");
-        }
-
-        NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config));
-
-        synchronized (finishLock) {
-            try {
-                finishLock.wait();
-            } catch (Exception ignored) {
-            }
-        }
-
-        return data;
-    }
-
-    public static void ShowError(String error) {
-        NativeLibrary.displayAlertMsg(
-                CitraApplication.Companion.getAppContext().getResources().getString(R.string.software_keyboard),
-                error, false);
-    }
-
-    private static native ValidationError ValidateFilters(String text);
-
-    private static native ValidationError ValidateInput(String text);
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/applets/SoftwareKeyboard.kt b/src/android/app/src/main/java/org/citra/citra_emu/applets/SoftwareKeyboard.kt
new file mode 100644
index 000000000..f334366eb
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/applets/SoftwareKeyboard.kt
@@ -0,0 +1,152 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.applets
+
+import android.text.InputFilter
+import android.text.Spanned
+import androidx.annotation.Keep
+import org.citra.citra_emu.CitraApplication.Companion.appContext
+import org.citra.citra_emu.NativeLibrary
+import org.citra.citra_emu.R
+import org.citra.citra_emu.fragments.KeyboardDialogFragment
+import org.citra.citra_emu.fragments.MessageDialogFragment
+import org.citra.citra_emu.utils.Log
+import java.io.Serializable
+
+@Keep
+object SoftwareKeyboard {
+    lateinit var data: KeyboardData
+    val finishLock = Object()
+
+    private fun ExecuteImpl(config: KeyboardConfig) {
+        val emulationActivity = NativeLibrary.sEmulationActivity.get()
+        data = KeyboardData(0, "")
+        KeyboardDialogFragment.newInstance(config)
+            .show(emulationActivity!!.supportFragmentManager, KeyboardDialogFragment.TAG)
+    }
+
+    fun HandleValidationError(config: KeyboardConfig, error: ValidationError) {
+        val emulationActivity = NativeLibrary.sEmulationActivity.get()!!
+        val message: String = when (error) {
+            ValidationError.FixedLengthRequired -> emulationActivity.getString(
+                R.string.fixed_length_required,
+                config.maxTextLength
+            )
+
+            ValidationError.MaxLengthExceeded ->
+                emulationActivity.getString(R.string.max_length_exceeded, config.maxTextLength)
+
+            ValidationError.BlankInputNotAllowed ->
+                emulationActivity.getString(R.string.blank_input_not_allowed)
+
+            ValidationError.EmptyInputNotAllowed ->
+                emulationActivity.getString(R.string.empty_input_not_allowed)
+
+            else -> emulationActivity.getString(R.string.invalid_input)
+        }
+
+        MessageDialogFragment.newInstance(R.string.software_keyboard, message).show(
+            NativeLibrary.sEmulationActivity.get()!!.supportFragmentManager,
+            MessageDialogFragment.TAG
+        )
+    }
+
+    @JvmStatic
+    fun Execute(config: KeyboardConfig): KeyboardData {
+        if (config.buttonConfig == ButtonConfig.None) {
+            Log.error("Unexpected button config None")
+            return KeyboardData(0, "")
+        }
+        NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { ExecuteImpl(config) }
+        synchronized(finishLock) {
+            try {
+                finishLock.wait()
+            } catch (ignored: Exception) {
+            }
+        }
+        return data
+    }
+
+    @JvmStatic
+    fun ShowError(error: String) {
+        NativeLibrary.displayAlertMsg(
+            appContext.resources.getString(R.string.software_keyboard),
+            error,
+            false
+        )
+    }
+
+    private external fun ValidateFilters(text: String): ValidationError
+    external fun ValidateInput(text: String): ValidationError
+
+    /// Corresponds to Frontend::ButtonConfig
+    interface ButtonConfig {
+        companion object {
+            const val Single = 0 /// Ok button
+            const val Dual = 1 /// Cancel | Ok buttons
+            const val Triple = 2 /// Cancel | I Forgot | Ok buttons
+            const val None = 3 /// No button (returned by swkbdInputText in special cases)
+        }
+    }
+
+    /// Corresponds to Frontend::ValidationError
+    enum class ValidationError {
+        None,
+
+        // Button Selection
+        ButtonOutOfRange,
+
+        // Configured Filters
+        MaxDigitsExceeded,
+        AtSignNotAllowed,
+        PercentNotAllowed,
+        BackslashNotAllowed,
+        ProfanityNotAllowed,
+        CallbackFailed,
+
+        // Allowed Input Type
+        FixedLengthRequired,
+        MaxLengthExceeded,
+        BlankInputNotAllowed,
+        EmptyInputNotAllowed
+    }
+
+    @Keep
+    class KeyboardConfig : Serializable {
+        var buttonConfig = 0
+        var maxTextLength = 0
+
+        // True if the keyboard accepts multiple lines of input
+        var multilineMode = false
+
+        // Displayed in the field as a hint before
+        var hintText: String? = null
+
+        // Contains the button text that the caller provides
+        lateinit var buttonText: Array<String>
+    }
+
+    /// Corresponds to Frontend::KeyboardData
+    class KeyboardData(var button: Int, var text: String)
+    class Filter : InputFilter {
+        override fun filter(
+            source: CharSequence,
+            start: Int,
+            end: Int,
+            dest: Spanned,
+            dstart: Int,
+            dend: Int
+        ): CharSequence? {
+            val text = StringBuilder(dest)
+                .replace(dstart, dend, source.subSequence(start, end).toString())
+                .toString()
+            return if (ValidateFilters(text) == ValidationError.None) {
+                null // Accept replacement
+            } else {
+                dest.subSequence(dstart, dend) // Request the subsequence to be unchanged
+            }
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/contracts/OpenFileResultContract.java b/src/android/app/src/main/java/org/citra/citra_emu/contracts/OpenFileResultContract.java
deleted file mode 100644
index cc29088ce..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/contracts/OpenFileResultContract.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.citra.citra_emu.contracts;
-
-import android.content.Context;
-import android.content.Intent;
-import android.util.Pair;
-
-import androidx.activity.result.contract.ActivityResultContract;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-public class OpenFileResultContract extends ActivityResultContract<Boolean, Intent> {
-    @NonNull
-    @Override
-    public Intent createIntent(@NonNull Context context, Boolean allowMultiple) {
-        return new Intent(Intent.ACTION_OPEN_DOCUMENT)
-            .setType("application/octet-stream")
-            .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
-    }
-
-    @Override
-    public Intent parseResult(int i, @Nullable Intent intent) {
-        return intent;
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/contracts/OpenFileResultContract.kt b/src/android/app/src/main/java/org/citra/citra_emu/contracts/OpenFileResultContract.kt
new file mode 100644
index 000000000..401518093
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/contracts/OpenFileResultContract.kt
@@ -0,0 +1,19 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.contracts
+
+import android.content.Context
+import android.content.Intent
+import androidx.activity.result.contract.ActivityResultContract
+
+class OpenFileResultContract : ActivityResultContract<Boolean?, Intent?>() {
+    override fun createIntent(context: Context, input: Boolean?): Intent {
+        return Intent(Intent.ACTION_OPEN_DOCUMENT)
+            .setType("application/octet-stream")
+            .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, input)
+    }
+
+    override fun parseResult(resultCode: Int, intent: Intent?): Intent? = intent
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/Cheat.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/Cheat.java
deleted file mode 100644
index 93b026364..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/Cheat.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package org.citra.citra_emu.features.cheats.model;
-
-import androidx.annotation.Keep;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-public class Cheat {
-    @Keep
-    private final long mPointer;
-
-    private Runnable mEnabledChangedCallback = null;
-
-    @Keep
-    private Cheat(long pointer) {
-        mPointer = pointer;
-    }
-
-    @Override
-    protected native void finalize();
-
-    @NonNull
-    public native String getName();
-
-    @NonNull
-    public native String getNotes();
-
-    @NonNull
-    public native String getCode();
-
-    public native boolean getEnabled();
-
-    public void setEnabled(boolean enabled) {
-        setEnabledImpl(enabled);
-        onEnabledChanged();
-    }
-
-    private native void setEnabledImpl(boolean enabled);
-
-    public void setEnabledChangedCallback(@Nullable Runnable callback) {
-        mEnabledChangedCallback = callback;
-    }
-
-    private void onEnabledChanged() {
-        if (mEnabledChangedCallback != null) {
-            mEnabledChangedCallback.run();
-        }
-    }
-
-    /**
-     * If the code is valid, returns 0. Otherwise, returns the 1-based index
-     * for the line containing the error.
-     */
-    public static native int isValidGatewayCode(@NonNull String code);
-
-    public static native Cheat createGatewayCode(@NonNull String name, @NonNull String notes,
-                                                 @NonNull String code);
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/Cheat.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/Cheat.kt
new file mode 100644
index 000000000..36a508f54
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/Cheat.kt
@@ -0,0 +1,48 @@
+// 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.cheats.model
+
+import androidx.annotation.Keep
+
+@Keep
+class Cheat(@field:Keep private val mPointer: Long) {
+    private var enabledChangedCallback: Runnable? = null
+    protected external fun finalize()
+
+    external fun getName(): String
+
+    external fun getNotes(): String
+
+    external fun getCode(): String
+
+    external fun getEnabled(): Boolean
+
+    fun setEnabled(enabled: Boolean) {
+        setEnabledImpl(enabled)
+        onEnabledChanged()
+    }
+
+    private external fun setEnabledImpl(enabled: Boolean)
+
+    fun setEnabledChangedCallback(callback: Runnable) {
+        enabledChangedCallback = callback
+    }
+
+    private fun onEnabledChanged() {
+        enabledChangedCallback?.run()
+    }
+
+    companion object {
+        /**
+         * If the code is valid, returns 0. Otherwise, returns the 1-based index
+         * for the line containing the error.
+         */
+        @JvmStatic
+        external fun isValidGatewayCode(code: String): Int
+
+        @JvmStatic
+        external fun createGatewayCode(name: String, notes: String, code: String): Cheat
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.java
deleted file mode 100644
index a1e88a3d3..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.citra.citra_emu.features.cheats.model;
-
-import androidx.annotation.Keep;
-
-public class CheatEngine {
-    @Keep
-    private final long mPointer;
-
-    @Keep
-    public CheatEngine(long titleId) {
-        mPointer = initialize(titleId);
-    }
-
-    private static native long initialize(long titleId);
-
-    @Override
-    protected native void finalize();
-
-    public native Cheat[] getCheats();
-
-    public native void addCheat(Cheat cheat);
-
-    public native void removeCheat(int index);
-
-    public native void updateCheat(int index, Cheat newCheat);
-
-    public native void saveCheatFile();
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.kt
new file mode 100644
index 000000000..8cd10678b
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatEngine.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.cheats.model
+
+import androidx.annotation.Keep
+
+@Keep
+class CheatEngine(titleId: Long) {
+    @Keep
+    private val mPointer: Long
+
+    init {
+        mPointer = initialize(titleId)
+    }
+
+    protected external fun finalize()
+
+    external fun getCheats(): Array<Cheat>
+
+    external fun addCheat(cheat: Cheat?)
+    external fun removeCheat(index: Int)
+    external fun updateCheat(index: Int, newCheat: Cheat?)
+    external fun saveCheatFile()
+
+    companion object {
+        @JvmStatic
+        private external fun initialize(titleId: Long): Long
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.java
deleted file mode 100644
index dbeb34c21..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.java
+++ /dev/null
@@ -1,187 +0,0 @@
-package org.citra.citra_emu.features.cheats.model;
-
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
-import androidx.lifecycle.ViewModel;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-
-public class CheatsViewModel extends ViewModel {
-    private int mSelectedCheatPosition = -1;
-    private final MutableLiveData<Cheat> mSelectedCheat = new MutableLiveData<>(null);
-    private final MutableLiveData<Boolean> mIsAdding = new MutableLiveData<>(false);
-    private final MutableLiveData<Boolean> mIsEditing = new MutableLiveData<>(false);
-
-    private final MutableLiveData<Integer> mCheatAddedEvent = new MutableLiveData<>(null);
-    private final MutableLiveData<Integer> mCheatChangedEvent = new MutableLiveData<>(null);
-    private final MutableLiveData<Integer> mCheatDeletedEvent = new MutableLiveData<>(null);
-    private final MutableLiveData<Boolean> mOpenDetailsViewEvent = new MutableLiveData<>(false);
-
-    private CheatEngine mCheatEngine;
-    private Cheat[] mCheats;
-    private boolean mCheatsNeedSaving = false;
-
-    public void initialize(long titleId) {
-        mCheatEngine = new CheatEngine(titleId);
-        load();
-    }
-
-    private void load() {
-        mCheats = mCheatEngine.getCheats();
-
-        for (int i = 0; i < mCheats.length; i++) {
-            int position = i;
-            mCheats[i].setEnabledChangedCallback(() -> {
-                mCheatsNeedSaving = true;
-                notifyCheatUpdated(position);
-            });
-        }
-    }
-
-    public void saveIfNeeded() {
-        if (mCheatsNeedSaving) {
-            mCheatEngine.saveCheatFile();
-            mCheatsNeedSaving = false;
-        }
-    }
-
-    public Cheat[] getCheats() {
-        return mCheats;
-    }
-
-    public LiveData<Cheat> getSelectedCheat() {
-        return mSelectedCheat;
-    }
-
-    public void setSelectedCheat(Cheat cheat, int position) {
-        if (mIsEditing.getValue()) {
-            setIsEditing(false);
-        }
-
-        mSelectedCheat.setValue(cheat);
-        mSelectedCheatPosition = position;
-    }
-
-    public LiveData<Boolean> getIsAdding() {
-        return mIsAdding;
-    }
-
-    public LiveData<Boolean> getIsEditing() {
-        return mIsEditing;
-    }
-
-    public void setIsEditing(boolean isEditing) {
-        mIsEditing.setValue(isEditing);
-
-        if (mIsAdding.getValue() && !isEditing) {
-            mIsAdding.setValue(false);
-            setSelectedCheat(null, -1);
-        }
-    }
-
-    /**
-     * When a cheat is added, the integer stored in the returned LiveData
-     * changes to the position of that cheat, then changes back to null.
-     */
-    public LiveData<Integer> getCheatAddedEvent() {
-        return mCheatAddedEvent;
-    }
-
-    private void notifyCheatAdded(int position) {
-        mCheatAddedEvent.setValue(position);
-        mCheatAddedEvent.setValue(null);
-    }
-
-    public void startAddingCheat() {
-        mSelectedCheat.setValue(null);
-        mSelectedCheatPosition = -1;
-
-        mIsAdding.setValue(true);
-        mIsEditing.setValue(true);
-    }
-
-    public void finishAddingCheat(Cheat cheat) {
-        if (!mIsAdding.getValue()) {
-            throw new IllegalStateException();
-        }
-
-        mIsAdding.setValue(false);
-        mIsEditing.setValue(false);
-
-        int position = mCheats.length;
-
-        mCheatEngine.addCheat(cheat);
-
-        mCheatsNeedSaving = true;
-        load();
-
-        notifyCheatAdded(position);
-        setSelectedCheat(mCheats[position], position);
-    }
-
-    /**
-     * When a cheat is edited, the integer stored in the returned LiveData
-     * changes to the position of that cheat, then changes back to null.
-     */
-    public LiveData<Integer> getCheatUpdatedEvent() {
-        return mCheatChangedEvent;
-    }
-
-    /**
-     * Notifies that an edit has been made to the contents of the cheat at the given position.
-     */
-    private void notifyCheatUpdated(int position) {
-        mCheatChangedEvent.setValue(position);
-        mCheatChangedEvent.setValue(null);
-    }
-
-    public void updateSelectedCheat(Cheat newCheat) {
-        mCheatEngine.updateCheat(mSelectedCheatPosition, newCheat);
-
-        mCheatsNeedSaving = true;
-        load();
-
-        notifyCheatUpdated(mSelectedCheatPosition);
-        setSelectedCheat(mCheats[mSelectedCheatPosition], mSelectedCheatPosition);
-    }
-
-    /**
-     * When a cheat is deleted, the integer stored in the returned LiveData
-     * changes to the position of that cheat, then changes back to null.
-     */
-    public LiveData<Integer> getCheatDeletedEvent() {
-        return mCheatDeletedEvent;
-    }
-
-    /**
-     * Notifies that the cheat at the given position has been deleted.
-     */
-    private void notifyCheatDeleted(int position) {
-        mCheatDeletedEvent.setValue(position);
-        mCheatDeletedEvent.setValue(null);
-    }
-
-    public void deleteSelectedCheat() {
-        int position = mSelectedCheatPosition;
-
-        setSelectedCheat(null, -1);
-
-        mCheatEngine.removeCheat(position);
-
-        mCheatsNeedSaving = true;
-        load();
-
-        notifyCheatDeleted(position);
-    }
-
-    public LiveData<Boolean> getOpenDetailsViewEvent() {
-        return mOpenDetailsViewEvent;
-    }
-
-    public void openDetailsView() {
-        mOpenDetailsViewEvent.setValue(true);
-        mOpenDetailsViewEvent.setValue(false);
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.kt
new file mode 100644
index 000000000..48786ad82
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/model/CheatsViewModel.kt
@@ -0,0 +1,169 @@
+// 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.cheats.model
+
+import androidx.lifecycle.ViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class CheatsViewModel : ViewModel() {
+    val selectedCheat get() = _selectedCheat.asStateFlow()
+    private val _selectedCheat = MutableStateFlow<Cheat?>(null)
+
+    val isAdding get() = _isAdding.asStateFlow()
+    private val _isAdding = MutableStateFlow(false)
+
+    val isEditing get() = _isEditing.asStateFlow()
+    private val _isEditing = MutableStateFlow(false)
+
+    /**
+     * When a cheat is added, the integer stored in the returned StateFlow
+     * changes to the position of that cheat, then changes back to null.
+     */
+    val cheatAddedEvent get() = _cheatAddedEvent.asStateFlow()
+    private val _cheatAddedEvent = MutableStateFlow<Int?>(null)
+
+    val cheatChangedEvent get() = _cheatChangedEvent.asStateFlow()
+    private val _cheatChangedEvent = MutableStateFlow<Int?>(null)
+
+    /**
+     * When a cheat is deleted, the integer stored in the returned StateFlow
+     * changes to the position of that cheat, then changes back to null.
+     */
+    val cheatDeletedEvent get() = _cheatDeletedEvent.asStateFlow()
+    private val _cheatDeletedEvent = MutableStateFlow<Int?>(null)
+
+    val openDetailsViewEvent get() = _openDetailsViewEvent.asStateFlow()
+    private val _openDetailsViewEvent = MutableStateFlow(false)
+
+    val closeDetailsViewEvent get() = _closeDetailsViewEvent.asStateFlow()
+    private val _closeDetailsViewEvent = MutableStateFlow(false)
+
+    val listViewFocusChange get() = _listViewFocusChange.asStateFlow()
+    private val _listViewFocusChange = MutableStateFlow(false)
+
+    val detailsViewFocusChange get() = _detailsViewFocusChange.asStateFlow()
+    private val _detailsViewFocusChange = MutableStateFlow(false)
+
+    private var cheatEngine: CheatEngine? = null
+    lateinit var cheats: Array<Cheat>
+    private var cheatsNeedSaving = false
+    private var selectedCheatPosition = -1
+
+    fun initialize(titleId: Long) {
+        cheatEngine = CheatEngine(titleId)
+        load()
+    }
+
+    private fun load() {
+        cheats = cheatEngine!!.getCheats()
+        for (i in cheats.indices) {
+            cheats[i].setEnabledChangedCallback {
+                cheatsNeedSaving = true
+                notifyCheatUpdated(i)
+            }
+        }
+    }
+
+    fun saveIfNeeded() {
+        if (cheatsNeedSaving) {
+            cheatEngine!!.saveCheatFile()
+            cheatsNeedSaving = false
+        }
+    }
+
+    fun setSelectedCheat(cheat: Cheat?, position: Int) {
+        if (isEditing.value) {
+            setIsEditing(false)
+        }
+        _selectedCheat.value = cheat
+        selectedCheatPosition = position
+    }
+
+    fun setIsEditing(value: Boolean) {
+        _isEditing.value = value
+        if (isAdding.value && !value) {
+            _isAdding.value = false
+            setSelectedCheat(null, -1)
+        }
+    }
+
+    private fun notifyCheatAdded(position: Int) {
+        _cheatAddedEvent.value = position
+        _cheatAddedEvent.value = null
+    }
+
+    fun startAddingCheat() {
+        _selectedCheat.value = null
+        selectedCheatPosition = -1
+        _isAdding.value = true
+        _isEditing.value = true
+    }
+
+    fun finishAddingCheat(cheat: Cheat?) {
+        check(isAdding.value)
+        _isAdding.value = false
+        _isEditing.value = false
+        val position = cheats.size
+        cheatEngine!!.addCheat(cheat)
+        cheatsNeedSaving = true
+        load()
+        notifyCheatAdded(position)
+        setSelectedCheat(cheats[position], position)
+    }
+
+    /**
+     * Notifies that an edit has been made to the contents of the cheat at the given position.
+     */
+    private fun notifyCheatUpdated(position: Int) {
+        _cheatChangedEvent.value = position
+        _cheatChangedEvent.value = null
+    }
+
+    fun updateSelectedCheat(newCheat: Cheat?) {
+        cheatEngine!!.updateCheat(selectedCheatPosition, newCheat)
+        cheatsNeedSaving = true
+        load()
+        notifyCheatUpdated(selectedCheatPosition)
+        setSelectedCheat(cheats[selectedCheatPosition], selectedCheatPosition)
+    }
+
+    /**
+     * Notifies that the cheat at the given position has been deleted.
+     */
+    private fun notifyCheatDeleted(position: Int) {
+        _cheatDeletedEvent.value = position
+        _cheatDeletedEvent.value = null
+    }
+
+    fun deleteSelectedCheat() {
+        val position = selectedCheatPosition
+        setSelectedCheat(null, -1)
+        cheatEngine!!.removeCheat(position)
+        cheatsNeedSaving = true
+        load()
+        notifyCheatDeleted(position)
+    }
+
+    fun openDetailsView() {
+        _openDetailsViewEvent.value = true
+        _openDetailsViewEvent.value = false
+    }
+
+    fun closeDetailsView() {
+        _closeDetailsViewEvent.value = true
+        _closeDetailsViewEvent.value = false
+    }
+
+    fun onListViewFocusChanged(changed: Boolean) {
+        _listViewFocusChange.value = changed
+        _listViewFocusChange.value = false
+    }
+
+    fun onDetailsViewFocusChanged(changed: Boolean) {
+        _detailsViewFocusChange.value = changed
+        _detailsViewFocusChange.value = false
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.java
deleted file mode 100644
index 83b3430cd..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.java
+++ /dev/null
@@ -1,175 +0,0 @@
-package org.citra.citra_emu.features.cheats.ui;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.ScrollView;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.lifecycle.ViewModelProvider;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.cheats.model.Cheat;
-import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
-
-public class CheatDetailsFragment extends Fragment {
-    private View mRoot;
-    private ScrollView mScrollView;
-    private TextView mLabelName;
-    private EditText mEditName;
-    private EditText mEditNotes;
-    private EditText mEditCode;
-    private Button mButtonDelete;
-    private Button mButtonEdit;
-    private Button mButtonCancel;
-    private Button mButtonOk;
-
-    private CheatsViewModel mViewModel;
-
-    @Nullable
-    @Override
-    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
-                             @Nullable Bundle savedInstanceState) {
-        return inflater.inflate(R.layout.fragment_cheat_details, container, false);
-    }
-
-    @Override
-    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
-        mRoot = view.findViewById(R.id.root);
-        mScrollView = view.findViewById(R.id.scroll_view);
-        mLabelName = view.findViewById(R.id.label_name);
-        mEditName = view.findViewById(R.id.edit_name);
-        mEditNotes = view.findViewById(R.id.edit_notes);
-        mEditCode = view.findViewById(R.id.edit_code);
-        mButtonDelete = view.findViewById(R.id.button_delete);
-        mButtonEdit = view.findViewById(R.id.button_edit);
-        mButtonCancel = view.findViewById(R.id.button_cancel);
-        mButtonOk = view.findViewById(R.id.button_ok);
-
-        CheatsActivity activity = (CheatsActivity) requireActivity();
-        mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
-
-        mViewModel.getSelectedCheat().observe(getViewLifecycleOwner(),
-                this::onSelectedCheatUpdated);
-        mViewModel.getIsEditing().observe(getViewLifecycleOwner(), this::onIsEditingUpdated);
-
-        mButtonDelete.setOnClickListener(this::onDeleteClicked);
-        mButtonEdit.setOnClickListener(this::onEditClicked);
-        mButtonCancel.setOnClickListener(this::onCancelClicked);
-        mButtonOk.setOnClickListener(this::onOkClicked);
-
-        // On a portrait phone screen (or other narrow screen), only one of the two panes are shown
-        // at the same time. If the user is navigating using a d-pad and moves focus to an element
-        // in the currently hidden pane, we need to manually show that pane.
-        CheatsActivity.setOnFocusChangeListenerRecursively(view,
-                (v, hasFocus) -> activity.onDetailsViewFocusChange(hasFocus));
-    }
-
-    private void clearEditErrors() {
-        mEditName.setError(null);
-        mEditCode.setError(null);
-    }
-
-    private void onDeleteClicked(View view) {
-        String name = mEditName.getText().toString();
-
-        new MaterialAlertDialogBuilder(requireContext())
-                .setMessage(getString(R.string.cheats_delete_confirmation, name))
-                .setPositiveButton(android.R.string.yes,
-                        (dialog, i) -> mViewModel.deleteSelectedCheat())
-                .setNegativeButton(android.R.string.no, null)
-                .show();
-    }
-
-    private void onEditClicked(View view) {
-        mViewModel.setIsEditing(true);
-        mButtonOk.requestFocus();
-    }
-
-    private void onCancelClicked(View view) {
-        mViewModel.setIsEditing(false);
-        onSelectedCheatUpdated(mViewModel.getSelectedCheat().getValue());
-        mButtonDelete.requestFocus();
-    }
-
-    private void onOkClicked(View view) {
-        clearEditErrors();
-
-        String name = mEditName.getText().toString();
-        String notes = mEditNotes.getText().toString();
-        String code = mEditCode.getText().toString();
-
-        if (name.isEmpty()) {
-            mEditName.setError(getString(R.string.cheats_error_no_name));
-            mScrollView.smoothScrollTo(0, mLabelName.getTop());
-            return;
-        } else if (code.isEmpty()) {
-            mEditCode.setError(getString(R.string.cheats_error_no_code_lines));
-            mScrollView.smoothScrollTo(0, mEditCode.getBottom());
-            return;
-        }
-
-        int validityResult = Cheat.isValidGatewayCode(code);
-
-        if (validityResult != 0) {
-            mEditCode.setError(getString(R.string.cheats_error_on_line, validityResult));
-            mScrollView.smoothScrollTo(0, mEditCode.getBottom());
-            return;
-        }
-
-        Cheat newCheat = Cheat.createGatewayCode(name, notes, code);
-
-        if (mViewModel.getIsAdding().getValue()) {
-            mViewModel.finishAddingCheat(newCheat);
-        } else {
-            mViewModel.updateSelectedCheat(newCheat);
-        }
-
-        mButtonEdit.requestFocus();
-    }
-
-    private void onSelectedCheatUpdated(@Nullable Cheat cheat) {
-        clearEditErrors();
-
-        boolean isEditing = mViewModel.getIsEditing().getValue();
-
-        mRoot.setVisibility(isEditing || cheat != null ? View.VISIBLE : View.GONE);
-
-        // If the fragment was recreated while editing a cheat, it's vital that we
-        // don't repopulate the fields, otherwise the user's changes will be lost
-        if (!isEditing) {
-            if (cheat == null) {
-                mEditName.setText("");
-                mEditNotes.setText("");
-                mEditCode.setText("");
-            } else {
-                mEditName.setText(cheat.getName());
-                mEditNotes.setText(cheat.getNotes());
-                mEditCode.setText(cheat.getCode());
-            }
-        }
-    }
-
-    private void onIsEditingUpdated(boolean isEditing) {
-        if (isEditing) {
-            mRoot.setVisibility(View.VISIBLE);
-        }
-
-        mEditName.setEnabled(isEditing);
-        mEditNotes.setEnabled(isEditing);
-        mEditCode.setEnabled(isEditing);
-
-        mButtonDelete.setVisibility(isEditing ? View.GONE : View.VISIBLE);
-        mButtonEdit.setVisibility(isEditing ? View.GONE : View.VISIBLE);
-        mButtonCancel.setVisibility(isEditing ? View.VISIBLE : View.GONE);
-        mButtonOk.setVisibility(isEditing ? View.VISIBLE : View.GONE);
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.kt
new file mode 100644
index 000000000..2357335a9
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatDetailsFragment.kt
@@ -0,0 +1,193 @@
+// 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.cheats.ui
+
+import android.annotation.SuppressLint
+import android.content.DialogInterface
+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.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import org.citra.citra_emu.R
+import org.citra.citra_emu.databinding.FragmentCheatDetailsBinding
+import org.citra.citra_emu.features.cheats.model.Cheat
+import org.citra.citra_emu.features.cheats.model.CheatsViewModel
+
+class CheatDetailsFragment : Fragment() {
+    private val cheatsViewModel: CheatsViewModel by activityViewModels()
+
+    private var _binding: FragmentCheatDetailsBinding? = null
+    private val binding get() = _binding!!
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        _binding = FragmentCheatDetailsBinding.inflate(layoutInflater)
+        return binding.root
+    }
+
+    // This is using the correct scope, lint is just acting up
+    @SuppressLint("UnsafeRepeatOnLifecycleDetector")
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        viewLifecycleOwner.lifecycleScope.apply {
+            launch {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    cheatsViewModel.selectedCheat.collect { onSelectedCheatUpdated(it) }
+                }
+            }
+            launch {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    cheatsViewModel.isEditing.collect { onIsEditingUpdated(it) }
+                }
+            }
+        }
+        binding.buttonDelete.setOnClickListener { onDeleteClicked() }
+        binding.buttonEdit.setOnClickListener { onEditClicked() }
+        binding.buttonCancel.setOnClickListener { onCancelClicked() }
+        binding.buttonOk.setOnClickListener { onOkClicked() }
+
+        // On a portrait phone screen (or other narrow screen), only one of the two panes are shown
+        // at the same time. If the user is navigating using a d-pad and moves focus to an element
+        // in the currently hidden pane, we need to manually show that pane.
+        CheatsActivity.setOnFocusChangeListenerRecursively(view) { _, hasFocus ->
+            cheatsViewModel.onDetailsViewFocusChanged(hasFocus)
+        }
+
+        binding.toolbarCheatDetails.setNavigationOnClickListener {
+            cheatsViewModel.closeDetailsView()
+        }
+
+        setInsets()
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        _binding = null
+    }
+
+    private fun clearEditErrors() {
+        binding.editName.error = null
+        binding.editCode.error = null
+    }
+
+    private fun onDeleteClicked() {
+        val name = binding.editNameInput.text.toString()
+        MaterialAlertDialogBuilder(requireContext())
+            .setMessage(getString(R.string.cheats_delete_confirmation, name))
+            .setPositiveButton(
+                android.R.string.ok
+            ) { _: DialogInterface?, _: Int -> cheatsViewModel.deleteSelectedCheat() }
+            .setNegativeButton(android.R.string.cancel, null)
+            .show()
+    }
+
+    private fun onEditClicked() {
+        cheatsViewModel.setIsEditing(true)
+        binding.buttonOk.requestFocus()
+    }
+
+    private fun onCancelClicked() {
+        cheatsViewModel.setIsEditing(false)
+        onSelectedCheatUpdated(cheatsViewModel.selectedCheat.value)
+        binding.buttonDelete.requestFocus()
+        cheatsViewModel.closeDetailsView()
+    }
+
+    private fun onOkClicked() {
+        clearEditErrors()
+        val name = binding.editNameInput.text.toString()
+        val notes = binding.editNotesInput.text.toString()
+        val code = binding.editCodeInput.text.toString()
+        if (name.isEmpty()) {
+            binding.editName.error = getString(R.string.cheats_error_no_name)
+            binding.scrollView.smoothScrollTo(0, binding.editNameInput.top)
+            return
+        } else if (code.isEmpty()) {
+            binding.editCode.error = getString(R.string.cheats_error_no_code_lines)
+            binding.scrollView.smoothScrollTo(0, binding.editCodeInput.bottom)
+            return
+        }
+        val validityResult = Cheat.isValidGatewayCode(code)
+        if (validityResult != 0) {
+            binding.editCode.error = getString(R.string.cheats_error_on_line, validityResult)
+            binding.scrollView.smoothScrollTo(0, binding.editCodeInput.bottom)
+            return
+        }
+        val newCheat = Cheat.createGatewayCode(name, notes, code)
+        if (cheatsViewModel.isAdding.value == true) {
+            cheatsViewModel.finishAddingCheat(newCheat)
+        } else {
+            cheatsViewModel.updateSelectedCheat(newCheat)
+        }
+        binding.buttonEdit.requestFocus()
+    }
+
+    private fun onSelectedCheatUpdated(cheat: Cheat?) {
+        clearEditErrors()
+        val isEditing: Boolean = cheatsViewModel.isEditing.value == true
+
+        // If the fragment was recreated while editing a cheat, it's vital that we
+        // don't repopulate the fields, otherwise the user's changes will be lost
+        if (!isEditing) {
+            if (cheat == null) {
+                binding.editNameInput.setText("")
+                binding.editNotesInput.setText("")
+                binding.editCodeInput.setText("")
+            } else {
+                binding.editNameInput.setText(cheat.getName())
+                binding.editNotesInput.setText(cheat.getNotes())
+                binding.editCodeInput.setText(cheat.getCode())
+            }
+        }
+    }
+
+    private fun onIsEditingUpdated(isEditing: Boolean) {
+        if (isEditing) {
+            binding.root.visibility = View.VISIBLE
+        }
+        binding.editNameInput.isEnabled = isEditing
+        binding.editNotesInput.isEnabled = isEditing
+        binding.editCodeInput.isEnabled = isEditing
+
+        binding.buttonDelete.visibility = if (isEditing) View.GONE else View.VISIBLE
+        binding.buttonEdit.visibility = if (isEditing) View.GONE else View.VISIBLE
+        binding.buttonCancel.visibility = if (isEditing) View.VISIBLE else View.GONE
+        binding.buttonOk.visibility = if (isEditing) View.VISIBLE else View.GONE
+    }
+
+    private fun setInsets() =
+        ViewCompat.setOnApplyWindowInsetsListener(
+            binding.root
+        ) { _: View?, windowInsets: WindowInsetsCompat ->
+            val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+            val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+
+            val leftInsets = barInsets.left + cutoutInsets.left
+            val rightInsets = barInsets.right + cutoutInsets.right
+
+            val mlpAppBar = binding.toolbarCheatDetails.layoutParams as ViewGroup.MarginLayoutParams
+            mlpAppBar.leftMargin = leftInsets
+            mlpAppBar.rightMargin = rightInsets
+            binding.toolbarCheatDetails.layoutParams = mlpAppBar
+
+            binding.scrollView.updatePadding(left = leftInsets, right = rightInsets)
+            binding.buttonContainer.updatePadding(left = leftInsets, right = rightInsets)
+
+            windowInsets
+        }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.java
deleted file mode 100644
index 552cf796e..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package org.citra.citra_emu.features.cheats.ui;
-
-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.lifecycle.ViewModelProvider;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-import com.google.android.material.floatingactionbutton.FloatingActionButton;
-
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
-import org.citra.citra_emu.ui.DividerItemDecoration;
-
-public class CheatListFragment extends Fragment {
-    private RecyclerView mRecyclerView;
-    private FloatingActionButton mFab;
-
-    @Nullable
-    @Override
-    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
-                             @Nullable Bundle savedInstanceState) {
-        return inflater.inflate(R.layout.fragment_cheat_list, container, false);
-    }
-
-    @Override
-    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
-        mRecyclerView = view.findViewById(R.id.cheat_list);
-        mFab = view.findViewById(R.id.fab);
-
-        CheatsActivity activity = (CheatsActivity) requireActivity();
-        CheatsViewModel viewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
-
-        mRecyclerView.setAdapter(new CheatsAdapter(activity, viewModel));
-        mRecyclerView.setLayoutManager(new LinearLayoutManager(activity));
-        mRecyclerView.addItemDecoration(new DividerItemDecoration(activity, null));
-
-        mFab.setOnClickListener(v -> {
-            viewModel.startAddingCheat();
-            viewModel.openDetailsView();
-        });
-
-        setInsets();
-    }
-
-    private void setInsets() {
-        ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> {
-            Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
-            v.setPadding(0, 0, 0, insets.bottom + getResources().getDimensionPixelSize(R.dimen.spacing_fab_list));
-
-            ViewGroup.MarginLayoutParams mlpFab =
-                    (ViewGroup.MarginLayoutParams) mFab.getLayoutParams();
-            int fabPadding = getResources().getDimensionPixelSize(R.dimen.spacing_large);
-            mlpFab.leftMargin = insets.left + fabPadding;
-            mlpFab.bottomMargin = insets.bottom + fabPadding;
-            mlpFab.rightMargin = insets.right + fabPadding;
-            mFab.setLayoutParams(mlpFab);
-
-            return windowInsets;
-        });
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.kt
new file mode 100644
index 000000000..c71ba99fa
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatListFragment.kt
@@ -0,0 +1,143 @@
+// 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.cheats.ui
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.MarginLayoutParams
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.findNavController
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.divider.MaterialDividerItemDecoration
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import org.citra.citra_emu.R
+import org.citra.citra_emu.databinding.FragmentCheatListBinding
+import org.citra.citra_emu.features.cheats.model.CheatsViewModel
+import org.citra.citra_emu.ui.main.MainActivity
+
+class CheatListFragment : Fragment() {
+    private var _binding: FragmentCheatListBinding? = null
+    private val binding get() = _binding!!
+
+    private val cheatsViewModel: CheatsViewModel by activityViewModels()
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        _binding = FragmentCheatListBinding.inflate(layoutInflater)
+        return binding.root
+    }
+
+    // This is using the correct scope, lint is just acting up
+    @SuppressLint("UnsafeRepeatOnLifecycleDetector")
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        binding.cheatList.adapter = CheatsAdapter(requireActivity(), cheatsViewModel)
+        binding.cheatList.layoutManager = LinearLayoutManager(requireContext())
+        binding.cheatList.addItemDecoration(
+            MaterialDividerItemDecoration(
+                requireContext(),
+                MaterialDividerItemDecoration.VERTICAL
+            )
+        )
+
+        viewLifecycleOwner.lifecycleScope.apply {
+            launch {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    cheatsViewModel.cheatAddedEvent.collect { position: Int? ->
+                        position?.let {
+                            binding.cheatList.apply {
+                                post { (adapter as CheatsAdapter).notifyItemInserted(it) }
+                            }
+                        }
+                    }
+                }
+            }
+            launch {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    cheatsViewModel.cheatChangedEvent.collect { position: Int? ->
+                        position?.let {
+                            binding.cheatList.apply {
+                                post { (adapter as CheatsAdapter).notifyItemChanged(it) }
+                            }
+                        }
+                    }
+                }
+            }
+            launch {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    cheatsViewModel.cheatDeletedEvent.collect { position: Int? ->
+                        position?.let {
+                            binding.cheatList.apply {
+                                post { (adapter as CheatsAdapter).notifyItemRemoved(it) }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        binding.fab.setOnClickListener {
+            cheatsViewModel.startAddingCheat()
+            cheatsViewModel.openDetailsView()
+        }
+
+        binding.toolbarCheatList.setNavigationOnClickListener {
+            if (requireActivity() is MainActivity) {
+                view.findNavController().popBackStack()
+            } else {
+                requireActivity().finish()
+            }
+        }
+
+        setInsets()
+    }
+
+    private fun setInsets() {
+        ViewCompat.setOnApplyWindowInsetsListener(
+            binding.root
+        ) { _: View, windowInsets: WindowInsetsCompat ->
+            val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+            val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+
+            val leftInsets = barInsets.left + cutoutInsets.left
+            val rightInsets = barInsets.right + cutoutInsets.right
+
+            val mlpAppBar = binding.toolbarCheatList.layoutParams as MarginLayoutParams
+            mlpAppBar.leftMargin = leftInsets
+            mlpAppBar.rightMargin = rightInsets
+            binding.toolbarCheatList.layoutParams = mlpAppBar
+
+            binding.cheatList.updatePadding(
+                left = leftInsets,
+                right = rightInsets,
+                bottom = barInsets.bottom +
+                        resources.getDimensionPixelSize(R.dimen.spacing_fab_list)
+            )
+
+            val mlpFab = binding.fab.layoutParams as MarginLayoutParams
+            val fabPadding = resources.getDimensionPixelSize(R.dimen.spacing_large)
+            mlpFab.leftMargin = leftInsets + fabPadding
+            mlpFab.bottomMargin = barInsets.bottom + fabPadding
+            mlpFab.rightMargin = rightInsets + fabPadding
+            binding.fab.layoutParams = mlpFab
+            windowInsets
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatViewHolder.java
deleted file mode 100644
index 8ba8f86e7..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatViewHolder.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package org.citra.citra_emu.features.cheats.ui;
-
-import android.view.View;
-import android.widget.CheckBox;
-import android.widget.CompoundButton;
-import android.widget.TextView;
-
-import androidx.annotation.NonNull;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.recyclerview.widget.RecyclerView;
-
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.cheats.model.Cheat;
-import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
-
-public class CheatViewHolder extends RecyclerView.ViewHolder
-        implements View.OnClickListener, CompoundButton.OnCheckedChangeListener {
-    private final View mRoot;
-    private final TextView mName;
-    private final CheckBox mCheckbox;
-
-    private CheatsViewModel mViewModel;
-    private Cheat mCheat;
-    private int mPosition;
-
-    public CheatViewHolder(@NonNull View itemView) {
-        super(itemView);
-
-        mRoot = itemView.findViewById(R.id.root);
-        mName = itemView.findViewById(R.id.text_name);
-        mCheckbox = itemView.findViewById(R.id.checkbox);
-    }
-
-    public void bind(CheatsActivity activity, Cheat cheat, int position) {
-        mCheckbox.setOnCheckedChangeListener(null);
-
-        mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
-        mCheat = cheat;
-        mPosition = position;
-
-        mName.setText(mCheat.getName());
-        mCheckbox.setChecked(mCheat.getEnabled());
-
-        mRoot.setOnClickListener(this);
-        mCheckbox.setOnCheckedChangeListener(this);
-    }
-
-    public void onClick(View root) {
-        mViewModel.setSelectedCheat(mCheat, mPosition);
-        mViewModel.openDetailsView();
-    }
-
-    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
-        mCheat.setEnabled(isChecked);
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java
deleted file mode 100644
index 9446d1ad9..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.java
+++ /dev/null
@@ -1,235 +0,0 @@
-package org.citra.citra_emu.features.cheats.ui;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-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.WindowInsetsAnimationCompat;
-import androidx.core.view.WindowInsetsCompat;
-import androidx.lifecycle.ViewModelProvider;
-import androidx.slidingpanelayout.widget.SlidingPaneLayout;
-
-import com.google.android.material.appbar.AppBarLayout;
-import com.google.android.material.appbar.MaterialToolbar;
-
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.cheats.model.Cheat;
-import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
-import org.citra.citra_emu.ui.TwoPaneOnBackPressedCallback;
-import org.citra.citra_emu.utils.InsetsHelper;
-import org.citra.citra_emu.utils.ThemeUtil;
-
-import java.util.List;
-
-public class CheatsActivity extends AppCompatActivity
-        implements SlidingPaneLayout.PanelSlideListener {
-    private static String ARG_TITLE_ID = "title_id";
-
-    private CheatsViewModel mViewModel;
-
-    private SlidingPaneLayout mSlidingPaneLayout;
-    private View mCheatList;
-    private View mCheatDetails;
-
-    private View mCheatListLastFocus;
-    private View mCheatDetailsLastFocus;
-
-    public static void launch(Context context, long titleId) {
-        Intent intent = new Intent(context, CheatsActivity.class);
-        intent.putExtra(ARG_TITLE_ID, titleId);
-        context.startActivity(intent);
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        ThemeUtil.INSTANCE.setTheme(this);
-        super.onCreate(savedInstanceState);
-
-        WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
-
-        long titleId = getIntent().getLongExtra(ARG_TITLE_ID, -1);
-
-        mViewModel = new ViewModelProvider(this).get(CheatsViewModel.class);
-        mViewModel.initialize(titleId);
-
-        setContentView(R.layout.activity_cheats);
-
-        mSlidingPaneLayout = findViewById(R.id.sliding_pane_layout);
-        mCheatList = findViewById(R.id.cheat_list_container);
-        mCheatDetails = findViewById(R.id.cheat_details_container);
-
-        mCheatListLastFocus = mCheatList;
-        mCheatDetailsLastFocus = mCheatDetails;
-
-        mSlidingPaneLayout.addPanelSlideListener(this);
-
-        getOnBackPressedDispatcher().addCallback(this,
-                new TwoPaneOnBackPressedCallback(mSlidingPaneLayout));
-
-        mViewModel.getSelectedCheat().observe(this, this::onSelectedCheatChanged);
-        mViewModel.getIsEditing().observe(this, this::onIsEditingChanged);
-        onSelectedCheatChanged(mViewModel.getSelectedCheat().getValue());
-
-        mViewModel.getOpenDetailsViewEvent().observe(this, this::openDetailsView);
-
-        // Show "Up" button in the action bar for navigation
-        MaterialToolbar toolbar = findViewById(R.id.toolbar_cheats);
-        setSupportActionBar(toolbar);
-        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
-
-        setInsets();
-    }
-
-    @Override
-    public boolean onCreateOptionsMenu(Menu menu) {
-        MenuInflater inflater = getMenuInflater();
-        inflater.inflate(R.menu.menu_settings, menu);
-
-        return true;
-    }
-
-    @Override
-    protected void onStop() {
-        super.onStop();
-
-        mViewModel.saveIfNeeded();
-    }
-
-    @Override
-    public void onPanelSlide(@NonNull View panel, float slideOffset) {
-    }
-
-    @Override
-    public void onPanelOpened(@NonNull View panel) {
-        boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
-        mCheatDetailsLastFocus.requestFocus(rtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
-    }
-
-    @Override
-    public void onPanelClosed(@NonNull View panel) {
-        boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
-        mCheatListLastFocus.requestFocus(rtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
-    }
-
-    private void onIsEditingChanged(boolean isEditing) {
-        if (isEditing) {
-            mSlidingPaneLayout.setLockMode(SlidingPaneLayout.LOCK_MODE_UNLOCKED);
-        }
-    }
-
-    private void onSelectedCheatChanged(Cheat selectedCheat) {
-        boolean cheatSelected = selectedCheat != null || mViewModel.getIsEditing().getValue();
-
-        if (!cheatSelected && mSlidingPaneLayout.isOpen()) {
-            mSlidingPaneLayout.close();
-        }
-
-        mSlidingPaneLayout.setLockMode(cheatSelected ?
-                SlidingPaneLayout.LOCK_MODE_UNLOCKED : SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED);
-    }
-
-    public void onListViewFocusChange(boolean hasFocus) {
-        if (hasFocus) {
-            mCheatListLastFocus = mCheatList.findFocus();
-            if (mCheatListLastFocus == null)
-                throw new NullPointerException();
-
-            mSlidingPaneLayout.close();
-        }
-    }
-
-    public void onDetailsViewFocusChange(boolean hasFocus) {
-        if (hasFocus) {
-            mCheatDetailsLastFocus = mCheatDetails.findFocus();
-            if (mCheatDetailsLastFocus == null)
-                throw new NullPointerException();
-
-            mSlidingPaneLayout.open();
-        }
-    }
-
-    @Override
-    public boolean onSupportNavigateUp() {
-        onBackPressed();
-        return true;
-    }
-
-    private void openDetailsView(boolean open) {
-        if (open) {
-            mSlidingPaneLayout.open();
-        }
-    }
-
-    public static void setOnFocusChangeListenerRecursively(@NonNull View view, View.OnFocusChangeListener listener) {
-        view.setOnFocusChangeListener(listener);
-
-        if (view instanceof ViewGroup) {
-            ViewGroup viewGroup = (ViewGroup) view;
-            for (int i = 0; i < viewGroup.getChildCount(); i++) {
-                View child = viewGroup.getChildAt(i);
-                setOnFocusChangeListenerRecursively(child, listener);
-            }
-        }
-    }
-
-    private void setInsets() {
-        AppBarLayout appBarLayout = findViewById(R.id.appbar_cheats);
-        ViewCompat.setOnApplyWindowInsetsListener(mSlidingPaneLayout, (v, windowInsets) -> {
-            Insets barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
-            Insets keyboardInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime());
-
-            InsetsHelper.insetAppBar(barInsets, appBarLayout);
-            mSlidingPaneLayout.setPadding(barInsets.left, 0, barInsets.right, 0);
-
-            // Set keyboard insets if the system supports smooth keyboard animations
-            ViewGroup.MarginLayoutParams mlpDetails =
-                    (ViewGroup.MarginLayoutParams) mCheatDetails.getLayoutParams();
-            if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.R) {
-                if (keyboardInsets.bottom > 0) {
-                    mlpDetails.bottomMargin = keyboardInsets.bottom;
-                } else {
-                    mlpDetails.bottomMargin = barInsets.bottom;
-                }
-            } else {
-                if (mlpDetails.bottomMargin == 0) {
-                    mlpDetails.bottomMargin = barInsets.bottom;
-                }
-            }
-            mCheatDetails.setLayoutParams(mlpDetails);
-
-            return windowInsets;
-        });
-
-        // Update the layout for every frame that the keyboard animates in
-        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
-            ViewCompat.setWindowInsetsAnimationCallback(mCheatDetails,
-                    new WindowInsetsAnimationCompat.Callback(
-                            WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP) {
-                        int keyboardInsets = 0;
-                        int barInsets = 0;
-
-                        @NonNull
-                        @Override
-                        public WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets,
-                                                             @NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
-                            ViewGroup.MarginLayoutParams mlpDetails =
-                                    (ViewGroup.MarginLayoutParams) mCheatDetails.getLayoutParams();
-                            keyboardInsets = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
-                            barInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
-                            mlpDetails.bottomMargin = Math.max(keyboardInsets, barInsets);
-                            mCheatDetails.setLayoutParams(mlpDetails);
-                            return insets;
-                        }
-                    });
-        }
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.kt
new file mode 100644
index 000000000..f66a8d373
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsActivity.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.cheats.ui
+
+import android.os.Bundle
+import android.view.View
+import android.view.View.OnFocusChangeListener
+import android.view.ViewGroup
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.view.WindowCompat
+import androidx.navigation.fragment.NavHostFragment
+import com.google.android.material.color.MaterialColors
+import org.citra.citra_emu.R
+import org.citra.citra_emu.databinding.ActivityCheatsBinding
+import org.citra.citra_emu.utils.InsetsHelper
+import org.citra.citra_emu.utils.ThemeUtil
+
+class CheatsActivity : AppCompatActivity() {
+    private lateinit var binding: ActivityCheatsBinding
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        ThemeUtil.setTheme(this)
+
+        super.onCreate(savedInstanceState)
+
+        binding = ActivityCheatsBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        WindowCompat.setDecorFitsSystemWindows(window, false)
+        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
+                )
+            )
+        }
+
+        val navHostFragment =
+            supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
+        val navController = navHostFragment.navController
+        navController.setGraph(R.navigation.cheats_navigation, intent.extras)
+    }
+
+    companion object {
+        fun setOnFocusChangeListenerRecursively(view: View, listener: OnFocusChangeListener?) {
+            view.onFocusChangeListener = listener
+            if (view is ViewGroup) {
+                for (i in 0 until view.childCount) {
+                    val child = view.getChildAt(i)
+                    setOnFocusChangeListenerRecursively(child, listener)
+                }
+            }
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.java b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.java
deleted file mode 100644
index 9cb2ce8d8..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package org.citra.citra_emu.features.cheats.ui;
-
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
-
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.features.cheats.model.Cheat;
-import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
-
-public class CheatsAdapter extends RecyclerView.Adapter<CheatViewHolder> {
-    private final CheatsActivity mActivity;
-    private final CheatsViewModel mViewModel;
-
-    public CheatsAdapter(CheatsActivity activity, CheatsViewModel viewModel) {
-        mActivity = activity;
-        mViewModel = viewModel;
-
-        mViewModel.getCheatAddedEvent().observe(activity, (position) -> {
-            if (position != null) {
-                notifyItemInserted(position);
-            }
-        });
-
-        mViewModel.getCheatUpdatedEvent().observe(activity, (position) -> {
-            if (position != null) {
-                notifyItemChanged(position);
-            }
-        });
-
-        mViewModel.getCheatDeletedEvent().observe(activity, (position) -> {
-            if (position != null) {
-                notifyItemRemoved(position);
-            }
-        });
-    }
-
-    @NonNull
-    @Override
-    public CheatViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
-        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
-
-        View cheatView = inflater.inflate(R.layout.list_item_cheat, parent, false);
-        addViewListeners(cheatView);
-        return new CheatViewHolder(cheatView);
-    }
-
-    @Override
-    public void onBindViewHolder(@NonNull CheatViewHolder holder, int position) {
-        holder.bind(mActivity, getItemAt(position), position);
-    }
-
-    @Override
-    public int getItemCount() {
-        return mViewModel.getCheats().length;
-    }
-
-    private void addViewListeners(View view) {
-        // On a portrait phone screen (or other narrow screen), only one of the two panes are shown
-        // at the same time. If the user is navigating using a d-pad and moves focus to an element
-        // in the currently hidden pane, we need to manually show that pane.
-        CheatsActivity.setOnFocusChangeListenerRecursively(view,
-                (v, hasFocus) -> mActivity.onListViewFocusChange(hasFocus));
-    }
-
-    private Cheat getItemAt(int position) {
-        return mViewModel.getCheats()[position];
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.kt
new file mode 100644
index 000000000..960b14f1d
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsAdapter.kt
@@ -0,0 +1,69 @@
+// 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.cheats.ui
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.CompoundButton
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.RecyclerView
+import org.citra.citra_emu.databinding.ListItemCheatBinding
+import org.citra.citra_emu.features.cheats.model.Cheat
+import org.citra.citra_emu.features.cheats.model.CheatsViewModel
+
+class CheatsAdapter(
+    private val activity: FragmentActivity,
+    private val viewModel: CheatsViewModel
+) : RecyclerView.Adapter<CheatsAdapter.CheatViewHolder>() {
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheatViewHolder {
+        val binding =
+            ListItemCheatBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+        addViewListeners(binding.root)
+        return CheatViewHolder(binding)
+    }
+
+    override fun onBindViewHolder(holder: CheatViewHolder, position: Int) =
+        holder.bind(activity, viewModel.cheats[position], position)
+
+    override fun getItemCount(): Int = viewModel.cheats.size
+
+    private fun addViewListeners(view: View) {
+        // On a portrait phone screen (or other narrow screen), only one of the two panes are shown
+        // at the same time. If the user is navigating using a d-pad and moves focus to an element
+        // in the currently hidden pane, we need to manually show that pane.
+        CheatsActivity.setOnFocusChangeListenerRecursively(view) { _, hasFocus ->
+            viewModel.onListViewFocusChanged(hasFocus)
+        }
+    }
+
+    inner class CheatViewHolder(private val binding: ListItemCheatBinding) :
+        RecyclerView.ViewHolder(binding.root), View.OnClickListener,
+        CompoundButton.OnCheckedChangeListener {
+        private lateinit var viewModel: CheatsViewModel
+        private lateinit var cheat: Cheat
+        private var position = 0
+
+        fun bind(activity: FragmentActivity, cheat: Cheat, position: Int) {
+            viewModel = ViewModelProvider(activity)[CheatsViewModel::class.java]
+            this.cheat = cheat
+            this.position = position
+            binding.textName.text = this.cheat.getName()
+            binding.cheatSwitch.isChecked = this.cheat.getEnabled()
+            binding.cheatContainer.setOnClickListener(this)
+            binding.cheatSwitch.setOnCheckedChangeListener(this)
+        }
+
+        override fun onClick(root: View) {
+            viewModel.setSelectedCheat(cheat, position)
+            viewModel.openDetailsView()
+        }
+
+        override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
+            cheat.setEnabled(isChecked)
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsFragment.kt
new file mode 100644
index 000000000..0c446cfd8
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/features/cheats/ui/CheatsFragment.kt
@@ -0,0 +1,244 @@
+// 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.cheats.ui
+
+import android.annotation.SuppressLint
+import android.os.Build
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.OnBackPressedCallback
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsAnimationCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.navigation.findNavController
+import androidx.navigation.fragment.navArgs
+import androidx.slidingpanelayout.widget.SlidingPaneLayout
+import com.google.android.material.transition.MaterialSharedAxis
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import org.citra.citra_emu.databinding.FragmentCheatsBinding
+import org.citra.citra_emu.features.cheats.model.Cheat
+import org.citra.citra_emu.features.cheats.model.CheatsViewModel
+import org.citra.citra_emu.ui.TwoPaneOnBackPressedCallback
+import org.citra.citra_emu.ui.main.MainActivity
+import org.citra.citra_emu.viewmodel.HomeViewModel
+
+class CheatsFragment : Fragment(), SlidingPaneLayout.PanelSlideListener {
+    private var cheatListLastFocus: View? = null
+    private var cheatDetailsLastFocus: View? = null
+
+    private var _binding: FragmentCheatsBinding? = null
+    private val binding get() = _binding!!
+
+    private val cheatsViewModel: CheatsViewModel by activityViewModels()
+    private val homeViewModel: HomeViewModel by activityViewModels()
+
+    private val args by navArgs<CheatsFragmentArgs>()
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
+        returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        _binding = FragmentCheatsBinding.inflate(inflater)
+        return binding.root
+    }
+
+    // This is using the correct scope, lint is just acting up
+    @SuppressLint("UnsafeRepeatOnLifecycleDetector")
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        homeViewModel.setNavigationVisibility(visible = false, animated = true)
+        homeViewModel.setStatusBarShadeVisibility(visible = false)
+
+        cheatsViewModel.initialize(args.titleId)
+
+        cheatListLastFocus = binding.cheatListContainer
+        cheatDetailsLastFocus = binding.cheatDetailsContainer
+        binding.slidingPaneLayout.addPanelSlideListener(this)
+        requireActivity().onBackPressedDispatcher.addCallback(
+            viewLifecycleOwner,
+            TwoPaneOnBackPressedCallback(binding.slidingPaneLayout)
+        )
+        requireActivity().onBackPressedDispatcher.addCallback(
+            viewLifecycleOwner,
+            object : OnBackPressedCallback(true) {
+                override fun handleOnBackPressed() {
+                    if (binding.slidingPaneLayout.isOpen) {
+                        binding.slidingPaneLayout.close()
+                    } else {
+                        if (requireActivity() is MainActivity) {
+                            view.findNavController().popBackStack()
+                        } else {
+                            requireActivity().finish()
+                        }
+                    }
+                }
+            }
+        )
+
+        viewLifecycleOwner.lifecycleScope.apply {
+            launch {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    cheatsViewModel.selectedCheat.collect { onSelectedCheatChanged(it) }
+                }
+            }
+            launch {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    cheatsViewModel.isEditing.collect { onIsEditingChanged(it) }
+                }
+            }
+            launch {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    cheatsViewModel.openDetailsViewEvent.collect { openDetailsView(it) }
+                }
+            }
+            launch {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    cheatsViewModel.closeDetailsViewEvent.collect { closeDetailsView(it) }
+                }
+            }
+            launch {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    cheatsViewModel.listViewFocusChange.collect { onListViewFocusChange(it) }
+                }
+            }
+            launch {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    cheatsViewModel.detailsViewFocusChange.collect { onDetailsViewFocusChange(it) }
+                }
+            }
+        }
+
+        setInsets()
+    }
+
+    override fun onStop() {
+        super.onStop()
+        cheatsViewModel.saveIfNeeded()
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        _binding = null
+    }
+
+    override fun onPanelSlide(panel: View, slideOffset: Float) {}
+    override fun onPanelOpened(panel: View) {
+        val rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL
+        cheatDetailsLastFocus!!.requestFocus(if (rtl) View.FOCUS_LEFT else View.FOCUS_RIGHT)
+    }
+
+    override fun onPanelClosed(panel: View) {
+        val rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL
+        cheatListLastFocus!!.requestFocus(if (rtl) View.FOCUS_RIGHT else View.FOCUS_LEFT)
+    }
+
+    private fun onIsEditingChanged(isEditing: Boolean) {
+        if (isEditing) {
+            binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_UNLOCKED
+        }
+    }
+
+    private fun onSelectedCheatChanged(selectedCheat: Cheat?) {
+        val cheatSelected = selectedCheat != null || cheatsViewModel.isEditing.value!!
+        if (!cheatSelected && binding.slidingPaneLayout.isOpen) {
+            binding.slidingPaneLayout.close()
+        }
+        binding.slidingPaneLayout.lockMode =
+            if (cheatSelected) SlidingPaneLayout.LOCK_MODE_UNLOCKED else SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED
+    }
+
+    fun onListViewFocusChange(hasFocus: Boolean) {
+        if (hasFocus) {
+            cheatListLastFocus = binding.cheatListContainer.findFocus()
+            if (cheatListLastFocus == null) throw NullPointerException()
+            binding.slidingPaneLayout.close()
+        }
+    }
+
+    fun onDetailsViewFocusChange(hasFocus: Boolean) {
+        if (hasFocus) {
+            cheatDetailsLastFocus = binding.cheatDetailsContainer.findFocus()
+            if (cheatDetailsLastFocus == null) {
+                throw NullPointerException()
+            }
+            binding.slidingPaneLayout.open()
+        }
+    }
+
+    private fun openDetailsView(open: Boolean) {
+        if (open) {
+            binding.slidingPaneLayout.open()
+        }
+    }
+
+    private fun closeDetailsView(close: Boolean) {
+        if (close) {
+            binding.slidingPaneLayout.close()
+        }
+    }
+
+    private fun setInsets() {
+        ViewCompat.setOnApplyWindowInsetsListener(
+            binding.slidingPaneLayout
+        ) { _: View?, windowInsets: WindowInsetsCompat ->
+            val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+            val keyboardInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
+
+            // Set keyboard insets if the system supports smooth keyboard animations
+            val mlpDetails = binding.cheatDetailsContainer.layoutParams as ViewGroup.MarginLayoutParams
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
+                if (keyboardInsets.bottom > 0) {
+                    mlpDetails.bottomMargin = keyboardInsets.bottom
+                } else {
+                    mlpDetails.bottomMargin = barInsets.bottom
+                }
+            } else {
+                if (mlpDetails.bottomMargin == 0) {
+                    mlpDetails.bottomMargin = barInsets.bottom
+                }
+            }
+            binding.cheatDetailsContainer.layoutParams = mlpDetails
+            windowInsets
+        }
+
+        // Update the layout for every frame that the keyboard animates in
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+            ViewCompat.setWindowInsetsAnimationCallback(
+                binding.cheatDetailsContainer,
+                object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
+                    var keyboardInsets = 0
+                    var barInsets = 0
+                    override fun onProgress(
+                        insets: WindowInsetsCompat,
+                        runningAnimations: List<WindowInsetsAnimationCompat>
+                    ): WindowInsetsCompat {
+                        val mlpDetails =
+                            binding.cheatDetailsContainer.layoutParams as ViewGroup.MarginLayoutParams
+                        keyboardInsets = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
+                        barInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
+                        mlpDetails.bottomMargin = keyboardInsets.coerceAtLeast(barInsets)
+                        binding.cheatDetailsContainer.layoutParams = mlpDetails
+                        return insets
+                    }
+                })
+        }
+    }
+}
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/AbstractShortSetting.kt
similarity index 62%
rename from src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/view/AbstractShortSetting.kt
rename to src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/AbstractShortSetting.kt
index 865ebbdd0..9fafc5410 100644
--- 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/AbstractShortSetting.kt
@@ -2,9 +2,7 @@
 // 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
+package org.citra.citra_emu.features.settings.model
 
 interface AbstractShortSetting : AbstractSetting {
     var short: Short
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
index 75c6ec331..f3e316dea 100644
--- 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
@@ -6,6 +6,7 @@ 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
+import org.citra.citra_emu.features.settings.model.AbstractShortSetting
 
 class SingleChoiceSetting(
     setting: AbstractSetting?,
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
index c763e29f5..17e1ae72c 100644
--- 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
@@ -5,6 +5,7 @@
 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.AbstractShortSetting
 import org.citra.citra_emu.features.settings.model.AbstractStringSetting
 
 class StringSingleChoiceSetting(
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
index 6d9cb9798..9f314c6dc 100644
--- 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
@@ -37,7 +37,7 @@ 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.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
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
index de0cc1826..534aee626 100644
--- 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
@@ -23,7 +23,7 @@ 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.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
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt
index 62c787c1e..66ec4cfbc 100644
--- a/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/EmulationFragment.kt
@@ -139,9 +139,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
         emulationActivity = requireActivity() as EmulationActivity
     }
 
-    /**
-     * Initialize the UI and start emulation in here.
-     */
     override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/KeyboardDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/KeyboardDialogFragment.kt
new file mode 100644
index 000000000..b4f581441
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/KeyboardDialogFragment.kt
@@ -0,0 +1,115 @@
+// 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.content.DialogInterface
+import android.os.Bundle
+import android.text.InputFilter
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.citra.citra_emu.R
+import org.citra.citra_emu.applets.SoftwareKeyboard
+import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding
+import org.citra.citra_emu.utils.SerializableHelper.serializable
+
+class KeyboardDialogFragment : DialogFragment() {
+    private lateinit var config: SoftwareKeyboard.KeyboardConfig
+
+    private var _binding: DialogSoftwareKeyboardBinding? = null
+    private val binding get() = _binding!!
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        _binding = DialogSoftwareKeyboardBinding.inflate(layoutInflater)
+
+        config = requireArguments().serializable<SoftwareKeyboard.KeyboardConfig>(CONFIG)!!
+
+        binding.apply {
+            editText.hint = config.hintText
+            editTextInput.isSingleLine = !config.multilineMode
+            editTextInput.filters =
+                arrayOf(SoftwareKeyboard.Filter(), InputFilter.LengthFilter(config.maxTextLength))
+        }
+
+        val builder = MaterialAlertDialogBuilder(requireContext())
+            .setTitle(R.string.software_keyboard)
+            .setView(binding.root)
+
+        isCancelable = false
+
+        when (config.buttonConfig) {
+            SoftwareKeyboard.ButtonConfig.Triple -> {
+                val negativeText =
+                    config.buttonText[0].ifEmpty { getString(android.R.string.cancel) }
+                val neutralText = config.buttonText[1].ifEmpty { getString(R.string.i_forgot) }
+                val positiveText = config.buttonText[2].ifEmpty { getString(android.R.string.ok) }
+                builder.setNegativeButton(negativeText, null)
+                    .setNeutralButton(neutralText, null)
+                    .setPositiveButton(positiveText, null)
+            }
+
+            SoftwareKeyboard.ButtonConfig.Dual -> {
+                val negativeText =
+                    config.buttonText[0].ifEmpty { getString(android.R.string.cancel) }
+                val positiveText = config.buttonText[2].ifEmpty { getString(android.R.string.ok) }
+                builder.setNegativeButton(negativeText, null)
+                    .setPositiveButton(positiveText, null)
+            }
+
+            SoftwareKeyboard.ButtonConfig.Single -> {
+                val positiveText = config.buttonText[2].ifEmpty { getString(android.R.string.ok) }
+                builder.setPositiveButton(positiveText, null)
+            }
+        }
+
+        // This overrides the default alert dialog behavior to prevent dismissing the keyboard
+        // dialog while we show an error message
+        val alertDialog = builder.create()
+        alertDialog.create()
+        if (alertDialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) {
+            alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {
+                SoftwareKeyboard.data.button = config.buttonConfig
+                SoftwareKeyboard.data.text = binding.editTextInput.text.toString()
+                val error = SoftwareKeyboard.ValidateInput(SoftwareKeyboard.data.text)
+                if (error != SoftwareKeyboard.ValidationError.None) {
+                    SoftwareKeyboard.HandleValidationError(config, error)
+                    return@setOnClickListener
+                }
+                dismiss()
+                synchronized(SoftwareKeyboard.finishLock) { SoftwareKeyboard.finishLock.notifyAll() }
+            }
+        }
+        if (alertDialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) {
+            alertDialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener {
+                SoftwareKeyboard.data.button = 1
+                dismiss()
+                synchronized(SoftwareKeyboard.finishLock) { SoftwareKeyboard.finishLock.notifyAll() }
+            }
+        }
+        if (alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) {
+            alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener {
+                SoftwareKeyboard.data.button = 0
+                dismiss()
+                synchronized(SoftwareKeyboard.finishLock) { SoftwareKeyboard.finishLock.notifyAll() }
+            }
+        }
+
+        return alertDialog
+    }
+
+    companion object {
+        const val TAG = "KeyboardDialogFragment"
+
+        const val CONFIG = "config"
+
+        fun newInstance(config: SoftwareKeyboard.KeyboardConfig): KeyboardDialogFragment {
+            val frag = KeyboardDialogFragment()
+            val args = Bundle()
+            args.putSerializable(CONFIG, config)
+            frag.arguments = args
+            return frag
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/fragments/MiiSelectorDialogFragment.kt b/src/android/app/src/main/java/org/citra/citra_emu/fragments/MiiSelectorDialogFragment.kt
new file mode 100644
index 000000000..fa1e1ae99
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/fragments/MiiSelectorDialogFragment.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.fragments
+
+import android.app.Dialog
+import android.content.DialogInterface
+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.applets.MiiSelector
+import org.citra.citra_emu.utils.SerializableHelper.serializable
+
+class MiiSelectorDialogFragment : DialogFragment() {
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        val config = requireArguments().serializable<MiiSelector.MiiSelectorConfig>(CONFIG)!!
+
+        // Note: we intentionally leave out the Standard Mii in the native code so that
+        // the string can get translated
+        val list = mutableListOf<String>()
+        list.add(getString(R.string.standard_mii))
+        list.addAll(config.miiNames)
+        val initialIndex =
+            if (config.initiallySelectedMiiIndex < list.size) config.initiallySelectedMiiIndex.toInt() else 0
+        MiiSelector.data.index = initialIndex
+        val builder = MaterialAlertDialogBuilder(requireActivity())
+            .setTitle(if (config.title!!.isEmpty()) getString(R.string.mii_selector) else config.title)
+            .setSingleChoiceItems(list.toTypedArray(), initialIndex) { _: DialogInterface?, which: Int ->
+                MiiSelector.data.index = which
+            }
+            .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
+                MiiSelector.data.returnCode = 0
+                synchronized(MiiSelector.finishLock) { MiiSelector.finishLock.notifyAll() }
+            }
+        if (config.enableCancelButton) {
+            builder.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int ->
+                MiiSelector.data.returnCode = 1
+                synchronized(MiiSelector.finishLock) { MiiSelector.finishLock.notifyAll() }
+            }
+        }
+        isCancelable = false
+        return builder.create()
+    }
+
+    companion object {
+        const val TAG = "MiiSelectorDialogFragment"
+
+        const val CONFIG = "config"
+
+        fun newInstance(config: MiiSelector.MiiSelectorConfig): MiiSelectorDialogFragment {
+            val frag = MiiSelectorDialogFragment()
+            val args = Bundle()
+            args.putSerializable(CONFIG, config)
+            frag.arguments = args
+            return frag
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/CheapDocument.java b/src/android/app/src/main/java/org/citra/citra_emu/model/CheapDocument.java
deleted file mode 100644
index 743e1d842..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/model/CheapDocument.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package org.citra.citra_emu.model;
-
-import android.net.Uri;
-import android.provider.DocumentsContract;
-
-/**
- * A struct that is much more "cheaper" than DocumentFile.
- * Only contains the information we needed.
- */
-public class CheapDocument {
-    private final String filename;
-    private final Uri uri;
-    private final String mimeType;
-
-    public CheapDocument(String filename, String mimeType, Uri uri) {
-        this.filename = filename;
-        this.mimeType = mimeType;
-        this.uri = uri;
-    }
-
-    public String getFilename() {
-        return filename;
-    }
-
-    public Uri getUri() {
-        return uri;
-    }
-
-    public String getMimeType() {
-        return mimeType;
-    }
-
-    public boolean isDirectory() {
-        return mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/model/CheapDocument.kt b/src/android/app/src/main/java/org/citra/citra_emu/model/CheapDocument.kt
new file mode 100644
index 000000000..d3a9490d8
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/model/CheapDocument.kt
@@ -0,0 +1,17 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.model
+
+import android.net.Uri
+import android.provider.DocumentsContract
+
+/**
+ * A struct that is much more "cheaper" than DocumentFile.
+ * Only contains the information we needed.
+ */
+class CheapDocument(val filename: String, val mimeType: String, val uri: Uri) {
+    val isDirectory: Boolean
+        get() = mimeType == DocumentsContract.Document.MIME_TYPE_DIR
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.java b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.java
deleted file mode 100644
index df41beb4e..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.java
+++ /dev/null
@@ -1,766 +0,0 @@
-/**
- * Copyright 2013 Dolphin Emulator Project
- * Licensed under GPLv2+
- * Refer to the license.txt file included.
- */
-
-package org.citra.citra_emu.overlay;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.preference.PreferenceManager;
-import android.util.AttributeSet;
-import android.util.DisplayMetrics;
-import android.view.Display;
-import android.view.MotionEvent;
-import android.view.SurfaceView;
-import android.view.View;
-import android.view.View.OnTouchListener;
-
-import org.citra.citra_emu.NativeLibrary;
-import org.citra.citra_emu.NativeLibrary.ButtonState;
-import org.citra.citra_emu.NativeLibrary.ButtonType;
-import org.citra.citra_emu.R;
-import org.citra.citra_emu.utils.EmulationMenuSettings;
-
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Draws the interactive input overlay on top of the
- * {@link SurfaceView} that is rendering emulation.
- */
-public final class InputOverlay extends SurfaceView implements OnTouchListener {
-    private final Set<InputOverlayDrawableButton> overlayButtons = new HashSet<>();
-    private final Set<InputOverlayDrawableDpad> overlayDpads = new HashSet<>();
-    private final Set<InputOverlayDrawableJoystick> overlayJoysticks = new HashSet<>();
-
-    private boolean mIsInEditMode = false;
-    private InputOverlayDrawableButton mButtonBeingConfigured;
-    private InputOverlayDrawableDpad mDpadBeingConfigured;
-    private InputOverlayDrawableJoystick mJoystickBeingConfigured;
-
-    private SharedPreferences mPreferences;
-
-    // Stores the ID of the pointer that interacted with the 3DS touchscreen.
-    private int mTouchscreenPointerId = -1;
-
-    /**
-     * Constructor
-     *
-     * @param context The current {@link Context}.
-     * @param attrs   {@link AttributeSet} for parsing XML attributes.
-     */
-    public InputOverlay(Context context, AttributeSet attrs) {
-        super(context, attrs);
-
-        mPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
-        if (!mPreferences.getBoolean("OverlayInit", false)) {
-            defaultOverlay();
-        }
-
-        // Reset 3ds touchscreen pointer ID
-        mTouchscreenPointerId = -1;
-
-        // Load the controls.
-        refreshControls();
-
-        // Set the on touch listener.
-        setOnTouchListener(this);
-
-        // Force draw
-        setWillNotDraw(false);
-
-        // Request focus for the overlay so it has priority on presses.
-        requestFocus();
-    }
-
-    /**
-     * Resizes a {@link Bitmap} by a given scale factor
-     *
-     * @param context The current {@link Context}
-     * @param bitmap  The {@link Bitmap} to scale.
-     * @param scale   The scale factor for the bitmap.
-     * @return The scaled {@link Bitmap}
-     */
-    public static Bitmap resizeBitmap(Context context, Bitmap bitmap, float scale) {
-        // Determine the button size based on the smaller screen dimension.
-        // This makes sure the buttons are the same size in both portrait and landscape.
-        DisplayMetrics dm = context.getResources().getDisplayMetrics();
-        int minDimension = Math.min(dm.widthPixels, dm.heightPixels);
-
-        return Bitmap.createScaledBitmap(bitmap,
-                (int) (minDimension * scale),
-                (int) (minDimension * scale),
-                true);
-    }
-
-    /**
-     * Initializes an InputOverlayDrawableButton, given by resId, with all of the
-     * parameters set for it to be properly shown on the InputOverlay.
-     * <p>
-     * This works due to the way the X and Y coordinates are stored within
-     * the {@link SharedPreferences}.
-     * <p>
-     * In the input overlay configuration menu,
-     * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay).
-     * the X and Y coordinates of the button at the END of its touch event
-     * (when you remove your finger/stylus from the touchscreen) are then stored
-     * within a SharedPreferences instance so that those values can be retrieved here.
-     * <p>
-     * This has a few benefits over the conventional way of storing the values
-     * (ie. within the Citra ini file).
-     * <ul>
-     * <li>No native calls</li>
-     * <li>Keeps Android-only values inside the Android environment</li>
-     * </ul>
-     * <p>
-     * Technically no modifications should need to be performed on the returned
-     * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait
-     * for Android to call the onDraw method.
-     *
-     * @param context      The current {@link Context}.
-     * @param defaultResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Default State).
-     * @param pressedResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Pressed State).
-     * @param buttonId     Identifier for determining what type of button the initialized InputOverlayDrawableButton represents.
-     * @return An {@link InputOverlayDrawableButton} with the correct drawing bounds set.
-     */
-    private static InputOverlayDrawableButton initializeOverlayButton(Context context,
-                                                                      int defaultResId, int pressedResId, int buttonId, String orientation) {
-        // Resources handle for fetching the initial Drawable resource.
-        final Resources res = context.getResources();
-
-        // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton.
-        final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context);
-
-        // Decide scale based on button ID and user preference
-        float scale;
-
-        switch (buttonId) {
-            case ButtonType.BUTTON_HOME:
-            case ButtonType.BUTTON_START:
-            case ButtonType.BUTTON_SELECT:
-                scale = 0.08f;
-                break;
-            case ButtonType.TRIGGER_L:
-            case ButtonType.TRIGGER_R:
-            case ButtonType.BUTTON_ZL:
-            case ButtonType.BUTTON_ZR:
-                scale = 0.18f;
-                break;
-            default:
-                scale = 0.11f;
-                break;
-        }
-
-        scale *= (sPrefs.getInt("controlScale", 50) + 50);
-        scale /= 100;
-
-        // Initialize the InputOverlayDrawableButton.
-        final Bitmap defaultStateBitmap =
-                resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale);
-        final Bitmap pressedStateBitmap =
-                resizeBitmap(context, BitmapFactory.decodeResource(res, pressedResId), scale);
-        final InputOverlayDrawableButton overlayDrawable =
-                new InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId);
-
-        // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
-        // These were set in the input overlay configuration menu.
-        String xKey;
-        String yKey;
-
-        xKey = buttonId + orientation + "-X";
-        yKey = buttonId + orientation + "-Y";
-
-        int drawableX = (int) sPrefs.getFloat(xKey, 0f);
-        int drawableY = (int) sPrefs.getFloat(yKey, 0f);
-
-        int width = overlayDrawable.getWidth();
-        int height = overlayDrawable.getHeight();
-
-        // Now set the bounds for the InputOverlayDrawableButton.
-        // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be.
-        overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height);
-
-        // Need to set the image's position
-        overlayDrawable.setPosition(drawableX, drawableY);
-
-        return overlayDrawable;
-    }
-
-    /**
-     * Initializes an {@link InputOverlayDrawableDpad}
-     *
-     * @param context                   The current {@link Context}.
-     * @param defaultResId              The {@link Bitmap} resource ID of the default sate.
-     * @param pressedOneDirectionResId  The {@link Bitmap} resource ID of the pressed sate in one direction.
-     * @param pressedTwoDirectionsResId The {@link Bitmap} resource ID of the pressed sate in two directions.
-     * @param buttonUp                  Identifier for the up button.
-     * @param buttonDown                Identifier for the down button.
-     * @param buttonLeft                Identifier for the left button.
-     * @param buttonRight               Identifier for the right button.
-     * @return the initialized {@link InputOverlayDrawableDpad}
-     */
-    private static InputOverlayDrawableDpad initializeOverlayDpad(Context context,
-                                                                  int defaultResId,
-                                                                  int pressedOneDirectionResId,
-                                                                  int pressedTwoDirectionsResId,
-                                                                  int buttonUp,
-                                                                  int buttonDown,
-                                                                  int buttonLeft,
-                                                                  int buttonRight,
-                                                                  String orientation) {
-        // Resources handle for fetching the initial Drawable resource.
-        final Resources res = context.getResources();
-
-        // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad.
-        final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context);
-
-        // Decide scale based on button ID and user preference
-        float scale = 0.22f;
-
-        scale *= (sPrefs.getInt("controlScale", 50) + 50);
-        scale /= 100;
-
-        // Initialize the InputOverlayDrawableDpad.
-        final Bitmap defaultStateBitmap =
-                resizeBitmap(context, BitmapFactory.decodeResource(res, defaultResId), scale);
-        final Bitmap pressedOneDirectionStateBitmap =
-                resizeBitmap(context, BitmapFactory.decodeResource(res, pressedOneDirectionResId),
-                        scale);
-        final Bitmap pressedTwoDirectionsStateBitmap =
-                resizeBitmap(context, BitmapFactory.decodeResource(res, pressedTwoDirectionsResId),
-                        scale);
-        final InputOverlayDrawableDpad overlayDrawable =
-                new InputOverlayDrawableDpad(res, defaultStateBitmap,
-                        pressedOneDirectionStateBitmap, pressedTwoDirectionsStateBitmap,
-                        buttonUp, buttonDown, buttonLeft, buttonRight);
-
-        // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
-        // These were set in the input overlay configuration menu.
-        int drawableX = (int) sPrefs.getFloat(buttonUp + orientation + "-X", 0f);
-        int drawableY = (int) sPrefs.getFloat(buttonUp + orientation + "-Y", 0f);
-
-        int width = overlayDrawable.getWidth();
-        int height = overlayDrawable.getHeight();
-
-        // Now set the bounds for the InputOverlayDrawableDpad.
-        // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be.
-        overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height);
-
-        // Need to set the image's position
-        overlayDrawable.setPosition(drawableX, drawableY);
-
-        return overlayDrawable;
-    }
-
-    /**
-     * Initializes an {@link InputOverlayDrawableJoystick}
-     *
-     * @param context         The current {@link Context}
-     * @param resOuter        Resource ID for the outer image of the joystick (the static image that shows the circular bounds).
-     * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around).
-     * @param pressedResInner Resource ID for the pressed inner image of the joystick.
-     * @param joystick        Identifier for which joystick this is.
-     * @return the initialized {@link InputOverlayDrawableJoystick}.
-     */
-    private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context context,
-                                                                          int resOuter, int defaultResInner, int pressedResInner, int joystick, String orientation) {
-        // Resources handle for fetching the initial Drawable resource.
-        final Resources res = context.getResources();
-
-        // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick.
-        final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context);
-
-        // Decide scale based on user preference
-        float scale = 0.275f;
-        scale *= (sPrefs.getInt("controlScale", 50) + 50);
-        scale /= 100;
-
-        // Initialize the InputOverlayDrawableJoystick.
-        final Bitmap bitmapOuter =
-                resizeBitmap(context, BitmapFactory.decodeResource(res, resOuter), scale);
-        final Bitmap bitmapInnerDefault = BitmapFactory.decodeResource(res, defaultResInner);
-        final Bitmap bitmapInnerPressed = BitmapFactory.decodeResource(res, pressedResInner);
-
-        // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
-        // These were set in the input overlay configuration menu.
-        int drawableX = (int) sPrefs.getFloat(joystick + orientation + "-X", 0f);
-        int drawableY = (int) sPrefs.getFloat(joystick + orientation + "-Y", 0f);
-
-        // Decide inner scale based on joystick ID
-        float outerScale = 1.f;
-        if (joystick == ButtonType.STICK_C) {
-            outerScale = 2.f;
-        }
-
-        // Now set the bounds for the InputOverlayDrawableJoystick.
-        // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be.
-        int outerSize = bitmapOuter.getWidth();
-        Rect outerRect = new Rect(drawableX, drawableY, drawableX + (int) (outerSize / outerScale), drawableY + (int) (outerSize / outerScale));
-        Rect innerRect = new Rect(0, 0, (int) (outerSize / outerScale), (int) (outerSize / outerScale));
-
-        // Send the drawableId to the joystick so it can be referenced when saving control position.
-        final InputOverlayDrawableJoystick overlayDrawable
-                = new InputOverlayDrawableJoystick(res, bitmapOuter,
-                bitmapInnerDefault, bitmapInnerPressed,
-                outerRect, innerRect, joystick);
-
-        // Need to set the image's position
-        overlayDrawable.setPosition(drawableX, drawableY);
-
-        return overlayDrawable;
-    }
-
-    @Override
-    public void draw(Canvas canvas) {
-        super.draw(canvas);
-
-        for (InputOverlayDrawableButton button : overlayButtons) {
-            button.draw(canvas);
-        }
-
-        for (InputOverlayDrawableDpad dpad : overlayDpads) {
-            dpad.draw(canvas);
-        }
-
-        for (InputOverlayDrawableJoystick joystick : overlayJoysticks) {
-            joystick.draw(canvas);
-        }
-    }
-
-    @Override
-    public boolean onTouch(View v, MotionEvent event) {
-        if (isInEditMode()) {
-            return onTouchWhileEditing(event);
-        }
-        boolean shouldUpdateView = false;
-        for (InputOverlayDrawableButton button : overlayButtons) {
-            if (!button.updateStatus(event)) {
-                continue;
-            }
-            NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.getId(), button.getStatus());
-            shouldUpdateView = true;
-        }
-
-        for (InputOverlayDrawableDpad dpad : overlayDpads) {
-            if (!dpad.updateStatus(event, EmulationMenuSettings.INSTANCE.getDpadSlide())) {
-                continue;
-            }
-            NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getUpId(), dpad.getUpStatus());
-            NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getDownId(), dpad.getDownStatus());
-            NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getLeftId(), dpad.getLeftStatus());
-            NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.getRightId(), dpad.getRightStatus());
-            shouldUpdateView = true;
-        }
-
-        for (InputOverlayDrawableJoystick joystick : overlayJoysticks) {
-            if (!joystick.updateStatus(event)) {
-                continue;
-            }
-            int axisID = joystick.getJoystickId();
-            NativeLibrary.INSTANCE
-                    .onGamePadMoveEvent(NativeLibrary.TouchScreenDevice, axisID, joystick.getXAxis(), joystick.getYAxis());
-            shouldUpdateView = true;
-        }
-
-        if (shouldUpdateView) {
-            invalidate();
-        }
-
-        if (!mPreferences.getBoolean("isTouchEnabled", true)) {
-            return true;
-        }
-
-        int pointerIndex = event.getActionIndex();
-        int xPosition = (int) event.getX(pointerIndex);
-        int yPosition = (int) event.getY(pointerIndex);
-        int pointerId = event.getPointerId(pointerIndex);
-        int motionEvent = event.getAction() & MotionEvent.ACTION_MASK;
-        boolean isActionDown = motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN;
-        boolean isActionMove = motionEvent == MotionEvent.ACTION_MOVE;
-        boolean isActionUp = motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP;
-
-        if (isActionDown && !isTouchInputConsumed(pointerId)) {
-            NativeLibrary.INSTANCE.onTouchEvent(xPosition, yPosition, true);
-        }
-
-        if (isActionMove) {
-            for (int i = 0; i < event.getPointerCount(); i++) {
-                int fingerId = event.getPointerId(i);
-                if (isTouchInputConsumed(fingerId)) {
-                    continue;
-                }
-                NativeLibrary.INSTANCE.onTouchMoved(xPosition, yPosition);
-            }
-        }
-
-        if (isActionUp && !isTouchInputConsumed(pointerId)) {
-            NativeLibrary.INSTANCE.onTouchEvent(0, 0, false);
-        }
-
-        return true;
-    }
-
-    private boolean isTouchInputConsumed(int trackId) {
-        for (InputOverlayDrawableButton button : overlayButtons) {
-            if (button.getTrackId() == trackId) {
-                return true;
-            }
-        }
-        for (InputOverlayDrawableDpad dpad : overlayDpads) {
-            if (dpad.getTrackId() == trackId) {
-                return true;
-            }
-        }
-        for (InputOverlayDrawableJoystick joystick : overlayJoysticks) {
-            if (joystick.getTrackId() == trackId) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    public boolean onTouchWhileEditing(MotionEvent event) {
-        int pointerIndex = event.getActionIndex();
-        int fingerPositionX = (int) event.getX(pointerIndex);
-        int fingerPositionY = (int) event.getY(pointerIndex);
-
-        String orientation =
-                getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ?
-                        "-Portrait" : "";
-
-        // Maybe combine Button and Joystick as subclasses of the same parent?
-        // Or maybe create an interface like IMoveableHUDControl?
-
-        for (InputOverlayDrawableButton button : overlayButtons) {
-            // Determine the button state to apply based on the MotionEvent action flag.
-            switch (event.getAction() & MotionEvent.ACTION_MASK) {
-                case MotionEvent.ACTION_DOWN:
-                case MotionEvent.ACTION_POINTER_DOWN:
-                    // If no button is being moved now, remember the currently touched button to move.
-                    if (mButtonBeingConfigured == null &&
-                            button.getBounds().contains(fingerPositionX, fingerPositionY)) {
-                        mButtonBeingConfigured = button;
-                        mButtonBeingConfigured.onConfigureTouch(event);
-                    }
-                    break;
-                case MotionEvent.ACTION_MOVE:
-                    if (mButtonBeingConfigured != null) {
-                        mButtonBeingConfigured.onConfigureTouch(event);
-                        invalidate();
-                        return true;
-                    }
-                    break;
-
-                case MotionEvent.ACTION_UP:
-                case MotionEvent.ACTION_POINTER_UP:
-                    if (mButtonBeingConfigured == button) {
-                        // Persist button position by saving new place.
-                        saveControlPosition(mButtonBeingConfigured.getId(),
-                                mButtonBeingConfigured.getBounds().left,
-                                mButtonBeingConfigured.getBounds().top, orientation);
-                        mButtonBeingConfigured = null;
-                    }
-                    break;
-            }
-        }
-
-        for (InputOverlayDrawableDpad dpad : overlayDpads) {
-            // Determine the button state to apply based on the MotionEvent action flag.
-            switch (event.getAction() & MotionEvent.ACTION_MASK) {
-                case MotionEvent.ACTION_DOWN:
-                case MotionEvent.ACTION_POINTER_DOWN:
-                    // If no button is being moved now, remember the currently touched button to move.
-                    if (mButtonBeingConfigured == null &&
-                            dpad.getBounds().contains(fingerPositionX, fingerPositionY)) {
-                        mDpadBeingConfigured = dpad;
-                        mDpadBeingConfigured.onConfigureTouch(event);
-                    }
-                    break;
-                case MotionEvent.ACTION_MOVE:
-                    if (mDpadBeingConfigured != null) {
-                        mDpadBeingConfigured.onConfigureTouch(event);
-                        invalidate();
-                        return true;
-                    }
-                    break;
-
-                case MotionEvent.ACTION_UP:
-                case MotionEvent.ACTION_POINTER_UP:
-                    if (mDpadBeingConfigured == dpad) {
-                        // Persist button position by saving new place.
-                        saveControlPosition(mDpadBeingConfigured.getUpId(),
-                                mDpadBeingConfigured.getBounds().left, mDpadBeingConfigured.getBounds().top,
-                                orientation);
-                        mDpadBeingConfigured = null;
-                    }
-                    break;
-            }
-        }
-
-        for (InputOverlayDrawableJoystick joystick : overlayJoysticks) {
-            switch (event.getAction()) {
-                case MotionEvent.ACTION_DOWN:
-                case MotionEvent.ACTION_POINTER_DOWN:
-                    if (mJoystickBeingConfigured == null &&
-                            joystick.getBounds().contains(fingerPositionX, fingerPositionY)) {
-                        mJoystickBeingConfigured = joystick;
-                        mJoystickBeingConfigured.onConfigureTouch(event);
-                    }
-                    break;
-                case MotionEvent.ACTION_MOVE:
-                    if (mJoystickBeingConfigured != null) {
-                        mJoystickBeingConfigured.onConfigureTouch(event);
-                        invalidate();
-                    }
-                    break;
-                case MotionEvent.ACTION_UP:
-                case MotionEvent.ACTION_POINTER_UP:
-                    if (mJoystickBeingConfigured != null) {
-                        saveControlPosition(mJoystickBeingConfigured.getJoystickId(),
-                                mJoystickBeingConfigured.getBounds().left,
-                                mJoystickBeingConfigured.getBounds().top, orientation);
-                        mJoystickBeingConfigured = null;
-                    }
-                    break;
-            }
-        }
-
-        return true;
-    }
-
-    private void addOverlayControls(String orientation) {
-        if (mPreferences.getBoolean("buttonToggle0", true)) {
-            overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_a,
-                    R.drawable.button_a_pressed, ButtonType.BUTTON_A, orientation));
-        }
-        if (mPreferences.getBoolean("buttonToggle1", true)) {
-            overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_b,
-                    R.drawable.button_b_pressed, ButtonType.BUTTON_B, orientation));
-        }
-        if (mPreferences.getBoolean("buttonToggle2", true)) {
-            overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_x,
-                    R.drawable.button_x_pressed, ButtonType.BUTTON_X, orientation));
-        }
-        if (mPreferences.getBoolean("buttonToggle3", true)) {
-            overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_y,
-                    R.drawable.button_y_pressed, ButtonType.BUTTON_Y, orientation));
-        }
-        if (mPreferences.getBoolean("buttonToggle4", true)) {
-            overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_l,
-                    R.drawable.button_l_pressed, ButtonType.TRIGGER_L, orientation));
-        }
-        if (mPreferences.getBoolean("buttonToggle5", true)) {
-            overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_r,
-                    R.drawable.button_r_pressed, ButtonType.TRIGGER_R, orientation));
-        }
-        if (mPreferences.getBoolean("buttonToggle6", false)) {
-            overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zl,
-                    R.drawable.button_zl_pressed, ButtonType.BUTTON_ZL, orientation));
-        }
-        if (mPreferences.getBoolean("buttonToggle7", false)) {
-            overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_zr,
-                    R.drawable.button_zr_pressed, ButtonType.BUTTON_ZR, orientation));
-        }
-        if (mPreferences.getBoolean("buttonToggle8", true)) {
-            overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_start,
-                    R.drawable.button_start_pressed, ButtonType.BUTTON_START, orientation));
-        }
-        if (mPreferences.getBoolean("buttonToggle9", true)) {
-            overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.button_select,
-                    R.drawable.button_select_pressed, ButtonType.BUTTON_SELECT, orientation));
-        }
-        if (mPreferences.getBoolean("buttonToggle10", true)) {
-            overlayDpads.add(initializeOverlayDpad(getContext(), R.drawable.dpad,
-                    R.drawable.dpad_pressed_one_direction,
-                    R.drawable.dpad_pressed_two_directions,
-                    ButtonType.DPAD_UP, ButtonType.DPAD_DOWN,
-                    ButtonType.DPAD_LEFT, ButtonType.DPAD_RIGHT, orientation));
-        }
-        if (mPreferences.getBoolean("buttonToggle11", true)) {
-            overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_main_range,
-                    R.drawable.stick_main, R.drawable.stick_main_pressed,
-                    ButtonType.STICK_LEFT, orientation));
-        }
-        if (mPreferences.getBoolean("buttonToggle12", false)) {
-            overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.stick_c_range,
-                    R.drawable.stick_c, R.drawable.stick_c_pressed, ButtonType.STICK_C, orientation));
-        }
-    }
-
-    public void refreshControls() {
-        // Remove all the overlay buttons from the HashSet.
-        overlayButtons.clear();
-        overlayDpads.clear();
-        overlayJoysticks.clear();
-
-        String orientation =
-                getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ?
-                        "-Portrait" : "";
-
-        // Add all the enabled overlay items back to the HashSet.
-        if (EmulationMenuSettings.INSTANCE.getShowOverlay()) {
-            addOverlayControls(orientation);
-        }
-
-        invalidate();
-    }
-
-    private void saveControlPosition(int sharedPrefsId, int x, int y, String orientation) {
-        final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getContext());
-        SharedPreferences.Editor sPrefsEditor = sPrefs.edit();
-        sPrefsEditor.putFloat(sharedPrefsId + orientation + "-X", x);
-        sPrefsEditor.putFloat(sharedPrefsId + orientation + "-Y", y);
-        sPrefsEditor.apply();
-    }
-
-    public void setIsInEditMode(boolean isInEditMode) {
-        mIsInEditMode = isInEditMode;
-    }
-
-    private void defaultOverlay() {
-        if (!mPreferences.getBoolean("OverlayInit", false)) {
-            // It's possible that a user has created their overlay before this was added
-            // Only change the overlay if the 'A' button is not in the upper corner.
-            if (mPreferences.getFloat(ButtonType.BUTTON_A + "-X", 0f) == 0f) {
-                defaultOverlayLandscape();
-            }
-            if (mPreferences.getFloat(ButtonType.BUTTON_A + "-Portrait" + "-X", 0f) == 0f) {
-                defaultOverlayPortrait();
-            }
-        }
-
-        SharedPreferences.Editor sPrefsEditor = mPreferences.edit();
-        sPrefsEditor.putBoolean("OverlayInit", true);
-        sPrefsEditor.apply();
-    }
-
-    public void resetButtonPlacement() {
-        boolean isLandscape =
-                getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
-
-        if (isLandscape) {
-            defaultOverlayLandscape();
-        } else {
-            defaultOverlayPortrait();
-        }
-
-        refreshControls();
-    }
-
-    private void defaultOverlayLandscape() {
-        SharedPreferences.Editor sPrefsEditor = mPreferences.edit();
-        // Get screen size
-        Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay();
-        DisplayMetrics outMetrics = new DisplayMetrics();
-        display.getMetrics(outMetrics);
-        float maxX = outMetrics.heightPixels;
-        float maxY = outMetrics.widthPixels;
-        // Height and width changes depending on orientation. Use the larger value for height.
-        if (maxY > maxX) {
-            float tmp = maxX;
-            maxX = maxY;
-            maxY = tmp;
-        }
-        Resources res = getResources();
-
-        // Each value is a percent from max X/Y stored as an int. Have to bring that value down
-        // to a decimal before multiplying by MAX X/Y.
-        sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_START + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.STICK_C + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_C_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.STICK_C + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_C_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.STICK_LEFT + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_Y) / 1000) * maxY));
-
-        // We want to commit right away, otherwise the overlay could load before this is saved.
-        sPrefsEditor.commit();
-    }
-
-    private void defaultOverlayPortrait() {
-        SharedPreferences.Editor sPrefsEditor = mPreferences.edit();
-        // Get screen size
-        Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay();
-        DisplayMetrics outMetrics = new DisplayMetrics();
-        display.getMetrics(outMetrics);
-        float maxX = outMetrics.heightPixels;
-        float maxY = outMetrics.widthPixels;
-        // Height and width changes depending on orientation. Use the larger value for height.
-        if (maxY < maxX) {
-            float tmp = maxX;
-            maxX = maxY;
-            maxY = tmp;
-        }
-        Resources res = getResources();
-        String portrait = "-Portrait";
-
-        // Each value is a percent from max X/Y stored as an int. Have to bring that value down
-        // to a decimal before multiplying by MAX X/Y.
-        sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_A + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_B + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_X + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_Y + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_ZL + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_ZR + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.DPAD_UP + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.TRIGGER_L + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.TRIGGER_R + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_START + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_SELECT + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.STICK_C + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_Y) / 1000) * maxY));
-        sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-X", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_X) / 1000) * maxX));
-        sPrefsEditor.putFloat(ButtonType.STICK_LEFT + portrait + "-Y", (((float) res.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_Y) / 1000) * maxY));
-
-        // We want to commit right away, otherwise the overlay could load before this is saved.
-        sPrefsEditor.commit();
-    }
-
-    public boolean isInEditMode() {
-        return mIsInEditMode;
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt
new file mode 100644
index 000000000..deb718c7e
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlay.kt
@@ -0,0 +1,1051 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.overlay
+
+import android.app.Activity
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.res.Configuration
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.VectorDrawable
+import android.util.AttributeSet
+import android.util.DisplayMetrics
+import android.view.MotionEvent
+import android.view.SurfaceView
+import android.view.View
+import android.view.View.OnTouchListener
+import androidx.core.content.ContextCompat
+import androidx.preference.PreferenceManager
+import org.citra.citra_emu.CitraApplication
+import org.citra.citra_emu.NativeLibrary
+import org.citra.citra_emu.R
+import org.citra.citra_emu.utils.EmulationMenuSettings
+import java.lang.NullPointerException
+import kotlin.math.min
+
+/**
+ * Draws the interactive input overlay on top of the
+ * [SurfaceView] that is rendering emulation.
+ *
+ * @param context The current [Context].
+ * @param attrs   [AttributeSet] for parsing XML attributes.
+ */
+class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(context, attrs),
+    OnTouchListener {
+    private val overlayButtons: MutableSet<InputOverlayDrawableButton> = HashSet()
+    private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet()
+    private val overlayJoysticks: MutableSet<InputOverlayDrawableJoystick> = HashSet()
+    private var isInEditMode = false
+    private var buttonBeingConfigured: InputOverlayDrawableButton? = null
+    private var dpadBeingConfigured: InputOverlayDrawableDpad? = null
+    private var joystickBeingConfigured: InputOverlayDrawableJoystick? = null
+
+    // Stores the ID of the pointer that interacted with the 3DS touchscreen.
+    private var touchscreenPointerId = -1
+
+    init {
+        if (!preferences.getBoolean("OverlayInit", false)) {
+            defaultOverlay()
+        }
+
+        // Reset 3ds touchscreen pointer ID
+        touchscreenPointerId = -1
+
+        // Load the controls.
+        refreshControls()
+
+        // Set the on touch listener.
+        setOnTouchListener(this)
+
+        // Force draw
+        setWillNotDraw(false)
+
+        // Request focus for the overlay so it has priority on presses.
+        requestFocus()
+    }
+
+    override fun draw(canvas: Canvas) {
+        super.draw(canvas)
+        overlayButtons.forEach { it.draw(canvas) }
+        overlayDpads.forEach { it.draw(canvas) }
+        overlayJoysticks.forEach { it.draw(canvas) }
+    }
+
+    override fun onTouch(v: View, event: MotionEvent): Boolean {
+        if (isInEditMode) {
+            return onTouchWhileEditing(event)
+        }
+        var shouldUpdateView = false
+        for (button in overlayButtons) {
+            if (!button.updateStatus(event)) {
+                continue
+            }
+            NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, button.id, button.status)
+            shouldUpdateView = true
+        }
+        for (dpad in overlayDpads) {
+            if (!dpad.updateStatus(event, EmulationMenuSettings.dpadSlide)) {
+                continue
+            }
+            NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, dpad.upId, dpad.upStatus)
+            NativeLibrary.onGamePadEvent(
+                NativeLibrary.TouchScreenDevice,
+                dpad.downId,
+                dpad.downStatus
+            )
+            NativeLibrary.onGamePadEvent(
+                NativeLibrary.TouchScreenDevice,
+                dpad.leftId,
+                dpad.leftStatus
+            )
+            NativeLibrary.onGamePadEvent(
+                NativeLibrary.TouchScreenDevice,
+                dpad.rightId,
+                dpad.rightStatus
+            )
+            shouldUpdateView = true
+        }
+        for (joystick in overlayJoysticks) {
+            if (!joystick.updateStatus(event)) {
+                continue
+            }
+            val axisID = joystick.joystickId
+            NativeLibrary.onGamePadMoveEvent(
+                NativeLibrary.TouchScreenDevice,
+                axisID,
+                joystick.xAxis,
+                joystick.yAxis
+            )
+            shouldUpdateView = true
+        }
+
+        if (shouldUpdateView) {
+            invalidate()
+        }
+
+        if (!preferences.getBoolean("isTouchEnabled", true)) {
+            return true
+        }
+
+        val pointerIndex = event.actionIndex
+        val xPosition = event.getX(pointerIndex).toInt()
+        val yPosition = event.getY(pointerIndex).toInt()
+        val pointerId = event.getPointerId(pointerIndex)
+        val motionEvent = event.action and MotionEvent.ACTION_MASK
+        val isActionDown =
+            motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
+        val isActionMove = motionEvent == MotionEvent.ACTION_MOVE
+        val isActionUp =
+            motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
+        if (isActionDown && !isTouchInputConsumed(pointerId)) {
+            NativeLibrary.onTouchEvent(xPosition.toFloat(), yPosition.toFloat(), true)
+        }
+        if (isActionMove) {
+            for (i in 0 until event.pointerCount) {
+                val fingerId = event.getPointerId(i)
+                if (isTouchInputConsumed(fingerId)) {
+                    continue
+                }
+                NativeLibrary.onTouchMoved(xPosition.toFloat(), yPosition.toFloat())
+            }
+        }
+        if (isActionUp && !isTouchInputConsumed(pointerId)) {
+            NativeLibrary.onTouchEvent(0f, 0f, false)
+        }
+        return true
+    }
+
+    private fun isTouchInputConsumed(trackId: Int): Boolean {
+        overlayButtons.forEach {
+            if (it.trackId == trackId) {
+                return true
+            }
+        }
+        overlayDpads.forEach {
+            if (it.trackId == trackId) {
+                return true
+            }
+        }
+        overlayJoysticks.forEach {
+            if (it.trackId == trackId) {
+                return true
+            }
+        }
+        return false
+    }
+
+    fun onTouchWhileEditing(event: MotionEvent): Boolean {
+        val pointerIndex = event.actionIndex
+        val fingerPositionX = event.getX(pointerIndex).toInt()
+        val fingerPositionY = event.getY(pointerIndex).toInt()
+        val orientation =
+            if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else ""
+
+        // Maybe combine Button and Joystick as subclasses of the same parent?
+        // Or maybe create an interface like IMoveableHUDControl?
+        overlayButtons.forEach {
+            // Determine the button state to apply based on the MotionEvent action flag.
+            when (event.action and MotionEvent.ACTION_MASK) {
+                MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN ->
+                    // If no button is being moved now, remember the currently touched button to move.
+                    if (buttonBeingConfigured == null &&
+                        it.bounds.contains(fingerPositionX, fingerPositionY)
+                    ) {
+                        buttonBeingConfigured = it
+                        buttonBeingConfigured!!.onConfigureTouch(event)
+                    }
+
+                MotionEvent.ACTION_MOVE -> if (buttonBeingConfigured != null) {
+                    buttonBeingConfigured!!.onConfigureTouch(event)
+                    invalidate()
+                    return true
+                }
+
+                MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> if (buttonBeingConfigured == it) {
+                    // Persist button position by saving new place.
+                    saveControlPosition(
+                        buttonBeingConfigured!!.id,
+                        buttonBeingConfigured!!.bounds.left,
+                        buttonBeingConfigured!!.bounds.top, orientation
+                    )
+                    buttonBeingConfigured = null
+                }
+            }
+        }
+        overlayDpads.forEach {
+            // Determine the button state to apply based on the MotionEvent action flag.
+            when (event.action and MotionEvent.ACTION_MASK) {
+                MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN ->
+                    // If no button is being moved now, remember the currently touched button to move.
+                    if (buttonBeingConfigured == null &&
+                        it.bounds.contains(fingerPositionX, fingerPositionY)
+                    ) {
+                        dpadBeingConfigured = it
+                        dpadBeingConfigured!!.onConfigureTouch(event)
+                    }
+
+                MotionEvent.ACTION_MOVE -> if (dpadBeingConfigured != null) {
+                    dpadBeingConfigured!!.onConfigureTouch(event)
+                    invalidate()
+                    return true
+                }
+
+                MotionEvent.ACTION_UP,
+                MotionEvent.ACTION_POINTER_UP -> if (dpadBeingConfigured == it) {
+                    // Persist button position by saving new place.
+                    saveControlPosition(
+                        dpadBeingConfigured!!.upId,
+                        dpadBeingConfigured!!.bounds.left, dpadBeingConfigured!!.bounds.top,
+                        orientation
+                    )
+                    dpadBeingConfigured = null
+                }
+            }
+        }
+        overlayJoysticks.forEach {
+            when (event.action) {
+                MotionEvent.ACTION_DOWN,
+                MotionEvent.ACTION_POINTER_DOWN -> if (joystickBeingConfigured == null &&
+                    it.bounds.contains(fingerPositionX, fingerPositionY)
+                ) {
+                    joystickBeingConfigured = it
+                    joystickBeingConfigured!!.onConfigureTouch(event)
+                }
+
+                MotionEvent.ACTION_MOVE -> if (joystickBeingConfigured != null) {
+                    joystickBeingConfigured!!.onConfigureTouch(event)
+                    invalidate()
+                }
+
+                MotionEvent.ACTION_UP,
+                MotionEvent.ACTION_POINTER_UP -> if (joystickBeingConfigured != null) {
+                    saveControlPosition(
+                        joystickBeingConfigured!!.joystickId,
+                        joystickBeingConfigured!!.bounds.left,
+                        joystickBeingConfigured!!.bounds.top, orientation
+                    )
+                    joystickBeingConfigured = null
+                }
+            }
+        }
+        return true
+    }
+
+    private fun addOverlayControls(orientation: String) {
+        if (preferences.getBoolean("buttonToggle0", true)) {
+            overlayButtons.add(
+                initializeOverlayButton(
+                    context,
+                    R.drawable.button_a,
+                    R.drawable.button_a_pressed,
+                    NativeLibrary.ButtonType.BUTTON_A,
+                    orientation
+                )
+            )
+        }
+        if (preferences.getBoolean("buttonToggle1", true)) {
+            overlayButtons.add(
+                initializeOverlayButton(
+                    context,
+                    R.drawable.button_b,
+                    R.drawable.button_b_pressed,
+                    NativeLibrary.ButtonType.BUTTON_B,
+                    orientation
+                )
+            )
+        }
+        if (preferences.getBoolean("buttonToggle2", true)) {
+            overlayButtons.add(
+                initializeOverlayButton(
+                    context,
+                    R.drawable.button_x,
+                    R.drawable.button_x_pressed,
+                    NativeLibrary.ButtonType.BUTTON_X,
+                    orientation
+                )
+            )
+        }
+        if (preferences.getBoolean("buttonToggle3", true)) {
+            overlayButtons.add(
+                initializeOverlayButton(
+                    context,
+                    R.drawable.button_y,
+                    R.drawable.button_y_pressed,
+                    NativeLibrary.ButtonType.BUTTON_Y,
+                    orientation
+                )
+            )
+        }
+        if (preferences.getBoolean("buttonToggle4", true)) {
+            overlayButtons.add(
+                initializeOverlayButton(
+                    context,
+                    R.drawable.button_l,
+                    R.drawable.button_l_pressed,
+                    NativeLibrary.ButtonType.TRIGGER_L,
+                    orientation
+                )
+            )
+        }
+        if (preferences.getBoolean("buttonToggle5", true)) {
+            overlayButtons.add(
+                initializeOverlayButton(
+                    context,
+                    R.drawable.button_r,
+                    R.drawable.button_r_pressed,
+                    NativeLibrary.ButtonType.TRIGGER_R,
+                    orientation
+                )
+            )
+        }
+        if (preferences.getBoolean("buttonToggle6", false)) {
+            overlayButtons.add(
+                initializeOverlayButton(
+                    context,
+                    R.drawable.button_zl,
+                    R.drawable.button_zl_pressed,
+                    NativeLibrary.ButtonType.BUTTON_ZL,
+                    orientation
+                )
+            )
+        }
+        if (preferences.getBoolean("buttonToggle7", false)) {
+            overlayButtons.add(
+                initializeOverlayButton(
+                    context,
+                    R.drawable.button_zr,
+                    R.drawable.button_zr_pressed,
+                    NativeLibrary.ButtonType.BUTTON_ZR,
+                    orientation
+                )
+            )
+        }
+        if (preferences.getBoolean("buttonToggle8", true)) {
+            overlayButtons.add(
+                initializeOverlayButton(
+                    context,
+                    R.drawable.button_start,
+                    R.drawable.button_start_pressed,
+                    NativeLibrary.ButtonType.BUTTON_START,
+                    orientation
+                )
+            )
+        }
+        if (preferences.getBoolean("buttonToggle9", true)) {
+            overlayButtons.add(
+                initializeOverlayButton(
+                    context,
+                    R.drawable.button_select,
+                    R.drawable.button_select_pressed,
+                    NativeLibrary.ButtonType.BUTTON_SELECT,
+                    orientation
+                )
+            )
+        }
+        if (preferences.getBoolean("buttonToggle10", true)) {
+            overlayDpads.add(
+                initializeOverlayDpad(
+                    context,
+                    R.drawable.dpad,
+                    R.drawable.dpad_pressed_one_direction,
+                    R.drawable.dpad_pressed_two_directions,
+                    NativeLibrary.ButtonType.DPAD_UP,
+                    NativeLibrary.ButtonType.DPAD_DOWN,
+                    NativeLibrary.ButtonType.DPAD_LEFT,
+                    NativeLibrary.ButtonType.DPAD_RIGHT,
+                    orientation
+                )
+            )
+        }
+        if (preferences.getBoolean("buttonToggle11", true)) {
+            overlayJoysticks.add(
+                initializeOverlayJoystick(
+                    context,
+                    R.drawable.stick_main_range,
+                    R.drawable.stick_main,
+                    R.drawable.stick_main_pressed,
+                    NativeLibrary.ButtonType.STICK_LEFT,
+                    orientation
+                )
+            )
+        }
+        if (preferences.getBoolean("buttonToggle12", false)) {
+            overlayJoysticks.add(
+                initializeOverlayJoystick(
+                    context,
+                    R.drawable.stick_c_range,
+                    R.drawable.stick_c,
+                    R.drawable.stick_c_pressed,
+                    NativeLibrary.ButtonType.STICK_C,
+                    orientation
+                )
+            )
+        }
+        if (preferences.getBoolean("buttonToggle13", false)) {
+            overlayButtons.add(
+                initializeOverlayButton(
+                    context,
+                    R.drawable.button_home,
+                    R.drawable.button_home_pressed,
+                    NativeLibrary.ButtonType.BUTTON_HOME,
+                    orientation
+                )
+            )
+        }
+    }
+
+    fun refreshControls() {
+        // Remove all the overlay buttons from the HashSet.
+        overlayButtons.clear()
+        overlayDpads.clear()
+        overlayJoysticks.clear()
+        val orientation =
+            if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
+                "-Portrait"
+            } else {
+                ""
+            }
+
+        // Add all the enabled overlay items back to the HashSet.
+        if (EmulationMenuSettings.showOverlay) {
+            addOverlayControls(orientation)
+        }
+        invalidate()
+    }
+
+    private fun saveControlPosition(sharedPrefsId: Int, x: Int, y: Int, orientation: String) {
+        preferences.edit()
+            .putFloat("$sharedPrefsId$orientation-X", x.toFloat())
+            .putFloat("$sharedPrefsId$orientation-Y", y.toFloat())
+            .apply()
+    }
+
+    fun setIsInEditMode(isInEditMode: Boolean) {
+        this.isInEditMode = isInEditMode
+    }
+
+    private fun defaultOverlay() {
+        if (!preferences.getBoolean("OverlayInit", false)) {
+            // It's possible that a user has created their overlay before this was added
+            // Only change the overlay if the 'A' button is not in the upper corner.
+            val aButtonPosition = preferences.getFloat(
+                NativeLibrary.ButtonType.BUTTON_A.toString() + "-X",
+                0f
+            )
+            if (aButtonPosition == 0f) {
+                defaultOverlayLandscape()
+            }
+
+            val aButtonPositionPortrait = preferences.getFloat(
+                NativeLibrary.ButtonType.BUTTON_A.toString() + "-Portrait" + "-X",
+                0f
+            )
+            if (aButtonPositionPortrait == 0f) {
+                defaultOverlayPortrait()
+            }
+        }
+
+        preferences.edit()
+            .putBoolean("OverlayInit", true)
+            .apply()
+    }
+
+    fun resetButtonPlacement() {
+        val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
+        if (isLandscape) {
+            defaultOverlayLandscape()
+        } else {
+            defaultOverlayPortrait()
+        }
+        refreshControls()
+    }
+
+    private fun defaultOverlayLandscape() {
+        // Get screen size
+        val display = (context as Activity).windowManager.defaultDisplay
+        val outMetrics = DisplayMetrics()
+        display.getMetrics(outMetrics)
+        var maxX = outMetrics.heightPixels.toFloat()
+        var maxY = outMetrics.widthPixels.toFloat()
+        // Height and width changes depending on orientation. Use the larger value for height.
+        if (maxY > maxX) {
+            val tmp = maxX
+            maxX = maxY
+            maxY = tmp
+        }
+
+        // Each value is a percent from max X/Y stored as an int. Have to bring that value down
+        // to a decimal before multiplying by MAX X/Y.
+        preferences.edit()
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_A.toString() + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_A_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_A.toString() + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_A_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_B.toString() + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_B_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_B.toString() + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_B_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_X.toString() + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_X_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_X.toString() + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_X_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_Y.toString() + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_Y_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_Y.toString() + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_Y_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_ZL.toString() + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_ZL_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_ZL.toString() + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_ZL_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_ZR.toString() + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_ZR_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_ZR.toString() + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_ZR_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.DPAD_UP.toString() + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_UP_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.DPAD_UP.toString() + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_UP_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.TRIGGER_L.toString() + "-X",
+                resources.getInteger(R.integer.N3DS_TRIGGER_L_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.TRIGGER_L.toString() + "-Y",
+                resources.getInteger(R.integer.N3DS_TRIGGER_L_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.TRIGGER_R.toString() + "-X",
+                resources.getInteger(R.integer.N3DS_TRIGGER_R_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.TRIGGER_R.toString() + "-Y",
+                resources.getInteger(R.integer.N3DS_TRIGGER_R_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_START.toString() + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_START_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_START.toString() + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_START_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_SELECT.toString() + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_SELECT_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_SELECT.toString() + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_SELECT_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_HOME.toString() + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_HOME_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_HOME.toString() + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_HOME_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.STICK_C.toString() + "-X",
+                resources.getInteger(R.integer.N3DS_STICK_C_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.STICK_C.toString() + "-Y",
+                resources.getInteger(R.integer.N3DS_STICK_C_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.STICK_LEFT.toString() + "-X",
+                resources.getInteger(R.integer.N3DS_STICK_MAIN_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.STICK_LEFT.toString() + "-Y",
+                resources.getInteger(R.integer.N3DS_STICK_MAIN_Y).toFloat() / 1000 * maxY
+            )
+            .apply()
+    }
+
+    private fun defaultOverlayPortrait() {
+        // Get screen size
+        val display = (context as Activity).windowManager.defaultDisplay
+        val outMetrics = DisplayMetrics()
+        display.getMetrics(outMetrics)
+        var maxX = outMetrics.heightPixels.toFloat()
+        var maxY = outMetrics.widthPixels.toFloat()
+        // Height and width changes depending on orientation. Use the larger value for height.
+        if (maxY < maxX) {
+            val tmp = maxX
+            maxX = maxY
+            maxY = tmp
+        }
+        val portrait = "-Portrait"
+
+        // Each value is a percent from max X/Y stored as an int. Have to bring that value down
+        // to a decimal before multiplying by MAX X/Y.
+        preferences.edit()
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_A.toString() + portrait + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_A.toString() + portrait + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_A_PORTRAIT_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_B.toString() + portrait + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_B.toString() + portrait + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_B_PORTRAIT_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_X.toString() + portrait + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_X.toString() + portrait + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_X_PORTRAIT_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_Y.toString() + portrait + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_Y.toString() + portrait + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_Y_PORTRAIT_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_ZL.toString() + portrait + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_ZL.toString() + portrait + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_ZL_PORTRAIT_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_ZR.toString() + portrait + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_ZR.toString() + portrait + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_ZR_PORTRAIT_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.DPAD_UP.toString() + portrait + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.DPAD_UP.toString() + portrait + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_UP_PORTRAIT_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.TRIGGER_L.toString() + portrait + "-X",
+                resources.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.TRIGGER_L.toString() + portrait + "-Y",
+                resources.getInteger(R.integer.N3DS_TRIGGER_L_PORTRAIT_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.TRIGGER_R.toString() + portrait + "-X",
+                resources.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.TRIGGER_R.toString() + portrait + "-Y",
+                resources.getInteger(R.integer.N3DS_TRIGGER_R_PORTRAIT_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_START.toString() + portrait + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_START.toString() + portrait + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_START_PORTRAIT_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_SELECT.toString() + portrait + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_X)
+                    .toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_SELECT.toString() + portrait + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_SELECT_PORTRAIT_Y)
+                    .toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_HOME.toString() + portrait + "-X",
+                resources.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.BUTTON_HOME.toString() + portrait + "-Y",
+                resources.getInteger(R.integer.N3DS_BUTTON_HOME_PORTRAIT_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.STICK_C.toString() + portrait + "-X",
+                resources.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.STICK_C.toString() + portrait + "-Y",
+                resources.getInteger(R.integer.N3DS_STICK_C_PORTRAIT_Y).toFloat() / 1000 * maxY
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.STICK_LEFT.toString() + portrait + "-X",
+                resources.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_X).toFloat() / 1000 * maxX
+            )
+            .putFloat(
+                NativeLibrary.ButtonType.STICK_LEFT.toString() + portrait + "-Y",
+                resources.getInteger(R.integer.N3DS_STICK_MAIN_PORTRAIT_Y).toFloat() / 1000 * maxY
+            )
+            .apply()
+    }
+
+    override fun isInEditMode(): Boolean {
+        return isInEditMode
+    }
+
+    companion object {
+        private val preferences
+            get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
+
+        /**
+         * Resizes a [Bitmap] by a given scale factor
+         *
+         * @param context       Context for getting the drawable/vector drawable
+         * @param drawableId    The ID of the drawable to scale.
+         * @param scale         The scale factor for the bitmap.
+         * @return The scaled [Bitmap]
+         */
+        private fun getBitmap(context: Context, drawableId: Int, scale: Float): Bitmap {
+            try {
+                val bitmap = BitmapFactory.decodeResource(context.resources, drawableId)
+                return resizeBitmap(context, bitmap, scale)
+            } catch (_: NullPointerException) {
+            }
+
+            val vectorDrawable = ContextCompat.getDrawable(context, drawableId) as VectorDrawable
+
+            val bitmap = Bitmap.createBitmap(
+                (vectorDrawable.intrinsicWidth * scale).toInt(),
+                (vectorDrawable.intrinsicHeight * scale).toInt(),
+                Bitmap.Config.ARGB_8888
+            )
+
+            val scaledBitmap = resizeBitmap(context, bitmap, scale)
+            val canvas = Canvas(scaledBitmap)
+            vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
+            vectorDrawable.draw(canvas)
+            return scaledBitmap
+        }
+
+        /**
+         * Resizes a [Bitmap] by a given scale factor
+         *
+         * @param context The current [Context]
+         * @param bitmap  The [Bitmap] to scale.
+         * @param scale   The scale factor for the bitmap.
+         * @return The scaled [Bitmap]
+         */
+        private fun resizeBitmap(context: Context, bitmap: Bitmap, scale: Float): Bitmap {
+            // Determine the button size based on the smaller screen dimension.
+            // This makes sure the buttons are the same size in both portrait and landscape.
+            val dm = context.resources.displayMetrics
+            val minDimension = min(dm.widthPixels, dm.heightPixels)
+            return Bitmap.createScaledBitmap(
+                bitmap,
+                (minDimension * scale).toInt(),
+                (minDimension * scale).toInt(),
+                true
+            )
+        }
+
+        /**
+         * Initializes an InputOverlayDrawableButton, given by resId, with all of the
+         * parameters set for it to be properly shown on the InputOverlay.
+         *
+         *
+         * This works due to the way the X and Y coordinates are stored within
+         * the [SharedPreferences].
+         *
+         *
+         * In the input overlay configuration menu,
+         * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay).
+         * the X and Y coordinates of the button at the END of its touch event
+         * (when you remove your finger/stylus from the touchscreen) are then stored
+         * within a SharedPreferences instance so that those values can be retrieved here.
+         *
+         *
+         * This has a few benefits over the conventional way of storing the values
+         * (ie. within the Citra ini file).
+         *
+         *  * No native calls
+         *  * Keeps Android-only values inside the Android environment
+         *
+         *
+         *
+         * Technically no modifications should need to be performed on the returned
+         * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait
+         * for Android to call the onDraw method.
+         *
+         * @param context      The current [Context].
+         * @param defaultResId The resource ID of the [Drawable] to get the [Bitmap] of (Default State).
+         * @param pressedResId The resource ID of the [Drawable] to get the [Bitmap] of (Pressed State).
+         * @param buttonId     Identifier for determining what type of button the initialized InputOverlayDrawableButton represents.
+         * @return An [InputOverlayDrawableButton] with the correct drawing bounds set.
+         */
+        private fun initializeOverlayButton(
+            context: Context,
+            defaultResId: Int,
+            pressedResId: Int,
+            buttonId: Int,
+            orientation: String
+        ): InputOverlayDrawableButton {
+            // Resources handle for fetching the initial Drawable resource.
+            val res = context.resources
+
+            // Decide scale based on button ID and user preference
+            var scale: Float = when (buttonId) {
+                NativeLibrary.ButtonType.BUTTON_HOME,
+                NativeLibrary.ButtonType.BUTTON_START,
+                NativeLibrary.ButtonType.BUTTON_SELECT -> 0.08f
+
+                NativeLibrary.ButtonType.TRIGGER_L,
+                NativeLibrary.ButtonType.TRIGGER_R,
+                NativeLibrary.ButtonType.BUTTON_ZL,
+                NativeLibrary.ButtonType.BUTTON_ZR -> 0.18f
+
+                else -> 0.11f
+            }
+            scale *= (preferences.getInt("controlScale", 50) + 50).toFloat()
+            scale /= 100f
+
+            // Initialize the InputOverlayDrawableButton.
+            val defaultStateBitmap = getBitmap(context, defaultResId, scale)
+            val pressedStateBitmap = getBitmap(context, pressedResId, scale)
+            val overlayDrawable =
+                InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId)
+
+            // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
+            // These were set in the input overlay configuration menu.
+            val xKey = "$buttonId$orientation-X"
+            val yKey = "$buttonId$orientation-Y"
+            val drawableX = preferences.getFloat(xKey, 0f).toInt()
+            val drawableY = preferences.getFloat(yKey, 0f).toInt()
+            val width = overlayDrawable.width
+            val height = overlayDrawable.height
+
+            // Now set the bounds for the InputOverlayDrawableButton.
+            // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be.
+            overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height)
+
+            // Need to set the image's position
+            overlayDrawable.setPosition(drawableX, drawableY)
+            return overlayDrawable
+        }
+
+        /**
+         * Initializes an [InputOverlayDrawableDpad]
+         *
+         * @param context                   The current [Context].
+         * @param defaultResId              The [Bitmap] resource ID of the default sate.
+         * @param pressedOneDirectionResId  The [Bitmap] resource ID of the pressed sate in one direction.
+         * @param pressedTwoDirectionsResId The [Bitmap] resource ID of the pressed sate in two directions.
+         * @param buttonUp                  Identifier for the up button.
+         * @param buttonDown                Identifier for the down button.
+         * @param buttonLeft                Identifier for the left button.
+         * @param buttonRight               Identifier for the right button.
+         * @return the initialized [InputOverlayDrawableDpad]
+         */
+        private fun initializeOverlayDpad(
+            context: Context,
+            defaultResId: Int,
+            pressedOneDirectionResId: Int,
+            pressedTwoDirectionsResId: Int,
+            buttonUp: Int,
+            buttonDown: Int,
+            buttonLeft: Int,
+            buttonRight: Int,
+            orientation: String
+        ): InputOverlayDrawableDpad {
+            // Resources handle for fetching the initial Drawable resource.
+            val res = context.resources
+
+            // Decide scale based on button ID and user preference
+            var scale = 0.22f
+            scale *= (preferences.getInt("controlScale", 50) + 50).toFloat()
+            scale /= 100f
+
+            // Initialize the InputOverlayDrawableDpad.
+            val defaultStateBitmap = getBitmap(context, defaultResId, scale)
+            val pressedOneDirectionStateBitmap = getBitmap(context, pressedOneDirectionResId, scale)
+            val pressedTwoDirectionsStateBitmap = getBitmap(context, pressedTwoDirectionsResId, scale)
+            val overlayDrawable = InputOverlayDrawableDpad(
+                res,
+                defaultStateBitmap,
+                pressedOneDirectionStateBitmap,
+                pressedTwoDirectionsStateBitmap,
+                buttonUp,
+                buttonDown,
+                buttonLeft,
+                buttonRight
+            )
+
+            // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
+            // These were set in the input overlay configuration menu.
+            val drawableX = preferences.getFloat("$buttonUp$orientation-X", 0f).toInt()
+            val drawableY = preferences.getFloat("$buttonUp$orientation-Y", 0f).toInt()
+            val width = overlayDrawable.width
+            val height = overlayDrawable.height
+
+            // Now set the bounds for the InputOverlayDrawableDpad.
+            // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be.
+            overlayDrawable.setBounds(drawableX, drawableY, drawableX + width, drawableY + height)
+
+            // Need to set the image's position
+            overlayDrawable.setPosition(drawableX, drawableY)
+            return overlayDrawable
+        }
+
+        /**
+         * Initializes an [InputOverlayDrawableJoystick]
+         *
+         * @param context         The current [Context]
+         * @param resOuter        Resource ID for the outer image of the joystick (the static image that shows the circular bounds).
+         * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around).
+         * @param pressedResInner Resource ID for the pressed inner image of the joystick.
+         * @param joystick        Identifier for which joystick this is.
+         * @return the initialized [InputOverlayDrawableJoystick].
+         */
+        private fun initializeOverlayJoystick(
+            context: Context,
+            resOuter: Int,
+            defaultResInner: Int,
+            pressedResInner: Int,
+            joystick: Int,
+            orientation: String
+        ): InputOverlayDrawableJoystick {
+            // Resources handle for fetching the initial Drawable resource.
+            val res = context.resources
+
+            // Decide scale based on user preference
+            var scale = 0.275f
+            scale *= (preferences.getInt("controlScale", 50) + 50).toFloat()
+            scale /= 100f
+
+            // Initialize the InputOverlayDrawableJoystick.
+            val bitmapOuter = getBitmap(context, resOuter, scale)
+            val bitmapInnerDefault = getBitmap(context, defaultResInner, scale)
+            val bitmapInnerPressed = getBitmap(context, pressedResInner, scale)
+
+            // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
+            // These were set in the input overlay configuration menu.
+            val drawableX = preferences.getFloat("$joystick$orientation-X", 0f).toInt()
+            val drawableY = preferences.getFloat("$joystick$orientation-Y", 0f).toInt()
+
+            // Decide inner scale based on joystick ID
+            var outerScale = 1f
+            if (joystick == NativeLibrary.ButtonType.STICK_C) {
+                outerScale = 2f
+            }
+
+            // Now set the bounds for the InputOverlayDrawableJoystick.
+            // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be.
+            val outerSize = bitmapOuter.width
+            val outerRect = Rect(
+                drawableX,
+                drawableY,
+                drawableX + (outerSize / outerScale).toInt(),
+                drawableY + (outerSize / outerScale).toInt()
+            )
+            val innerRect =
+                Rect(0, 0, (outerSize / outerScale).toInt(), (outerSize / outerScale).toInt())
+
+            // Send the drawableId to the joystick so it can be referenced when saving control position.
+            val overlayDrawable = InputOverlayDrawableJoystick(
+                res,
+                bitmapOuter,
+                bitmapInnerDefault,
+                bitmapInnerPressed,
+                outerRect,
+                innerRect,
+                joystick
+            )
+
+            // Need to set the image's position
+            overlayDrawable.setPosition(drawableX, drawableY)
+            return overlayDrawable
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.java b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.java
deleted file mode 100644
index ec49808af..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.java
+++ /dev/null
@@ -1,159 +0,0 @@
-/**
- * Copyright 2013 Dolphin Emulator Project
- * Licensed under GPLv2+
- * Refer to the license.txt file included.
- */
-
-package org.citra.citra_emu.overlay;
-
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.BitmapDrawable;
-import android.view.MotionEvent;
-
-import org.citra.citra_emu.NativeLibrary;
-
-/**
- * Custom {@link BitmapDrawable} that is capable
- * of storing it's own ID.
- */
-public final class InputOverlayDrawableButton {
-    // The ID identifying what type of button this Drawable represents.
-    private int mButtonType;
-    private int mTrackId;
-    private int mPreviousTouchX, mPreviousTouchY;
-    private int mControlPositionX, mControlPositionY;
-    private int mWidth;
-    private int mHeight;
-    private BitmapDrawable mDefaultStateBitmap;
-    private BitmapDrawable mPressedStateBitmap;
-    private boolean mPressedState = false;
-
-    /**
-     * Constructor
-     *
-     * @param res                {@link Resources} instance.
-     * @param defaultStateBitmap {@link Bitmap} to use with the default state Drawable.
-     * @param pressedStateBitmap {@link Bitmap} to use with the pressed state Drawable.
-     * @param buttonType         Identifier for this type of button.
-     */
-    public InputOverlayDrawableButton(Resources res, Bitmap defaultStateBitmap,
-                                      Bitmap pressedStateBitmap, int buttonType) {
-        mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap);
-        mPressedStateBitmap = new BitmapDrawable(res, pressedStateBitmap);
-        mButtonType = buttonType;
-        mTrackId = -1;
-
-        mWidth = mDefaultStateBitmap.getIntrinsicWidth();
-        mHeight = mDefaultStateBitmap.getIntrinsicHeight();
-    }
-
-    /**
-     * Updates button status based on the motion event.
-     *
-     * @return true if value was changed
-     */
-    public boolean updateStatus(MotionEvent event) {
-        int pointerIndex = event.getActionIndex();
-        int xPosition = (int) event.getX(pointerIndex);
-        int yPosition = (int) event.getY(pointerIndex);
-        int pointerId = event.getPointerId(pointerIndex);
-        int motionEvent = event.getAction() & MotionEvent.ACTION_MASK;
-        boolean isActionDown = motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN;
-        boolean isActionUp = motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP;
-
-        if (isActionDown) {
-            if (!getBounds().contains(xPosition, yPosition)) {
-                return false;
-            }
-            mPressedState = true;
-            mTrackId = pointerId;
-            return true;
-        }
-
-        if (isActionUp) {
-            if (mTrackId != pointerId) {
-                return false;
-            }
-            mPressedState = false;
-            mTrackId = -1;
-            return true;
-        }
-
-        return false;
-    }
-
-    public boolean onConfigureTouch(MotionEvent event) {
-        int pointerIndex = event.getActionIndex();
-        int fingerPositionX = (int) event.getX(pointerIndex);
-        int fingerPositionY = (int) event.getY(pointerIndex);
-        switch (event.getAction()) {
-            case MotionEvent.ACTION_DOWN:
-                mPreviousTouchX = fingerPositionX;
-                mPreviousTouchY = fingerPositionY;
-                break;
-            case MotionEvent.ACTION_MOVE:
-                mControlPositionX += fingerPositionX - mPreviousTouchX;
-                mControlPositionY += fingerPositionY - mPreviousTouchY;
-                setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX,
-                        getHeight() + mControlPositionY);
-                mPreviousTouchX = fingerPositionX;
-                mPreviousTouchY = fingerPositionY;
-                break;
-
-        }
-        return true;
-    }
-
-    public void setPosition(int x, int y) {
-        mControlPositionX = x;
-        mControlPositionY = y;
-    }
-
-    public void draw(Canvas canvas) {
-        getCurrentStateBitmapDrawable().draw(canvas);
-    }
-
-    private BitmapDrawable getCurrentStateBitmapDrawable() {
-        return mPressedState ? mPressedStateBitmap : mDefaultStateBitmap;
-    }
-
-    public void setBounds(int left, int top, int right, int bottom) {
-        mDefaultStateBitmap.setBounds(left, top, right, bottom);
-        mPressedStateBitmap.setBounds(left, top, right, bottom);
-    }
-
-    public int getId() {
-        return mButtonType;
-    }
-
-    public int getTrackId() {
-        return mTrackId;
-    }
-
-    public void setTrackId(int trackId) {
-        mTrackId = trackId;
-    }
-
-    public int getStatus() {
-        return mPressedState ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED;
-    }
-
-    public Rect getBounds() {
-        return mDefaultStateBitmap.getBounds();
-    }
-
-    public int getWidth() {
-        return mWidth;
-    }
-
-    public int getHeight() {
-        return mHeight;
-    }
-
-    public void setPressedState(boolean isPressed) {
-        mPressedState = isPressed;
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.kt b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.kt
new file mode 100644
index 000000000..5f83fa776
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableButton.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.overlay
+
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.BitmapDrawable
+import android.view.MotionEvent
+import org.citra.citra_emu.NativeLibrary
+
+/**
+ * Custom [BitmapDrawable] that is capable
+ * of storing it's own ID.
+ *
+ * @param res                [Resources] instance.
+ * @param defaultStateBitmap [Bitmap] to use with the default state Drawable.
+ * @param pressedStateBitmap [Bitmap] to use with the pressed state Drawable.
+ * @param id                 Identifier for this type of button.
+ */
+class InputOverlayDrawableButton(
+    res: Resources,
+    defaultStateBitmap: Bitmap,
+    pressedStateBitmap: Bitmap,
+    val id: Int
+) {
+    var trackId: Int
+    private var previousTouchX = 0
+    private var previousTouchY = 0
+    private var controlPositionX = 0
+    private var controlPositionY = 0
+    val width: Int
+    val height: Int
+    private val defaultStateBitmap: BitmapDrawable
+    private val pressedStateBitmap: BitmapDrawable
+    private var pressedState = false
+
+    init {
+        this.defaultStateBitmap = BitmapDrawable(res, defaultStateBitmap)
+        this.pressedStateBitmap = BitmapDrawable(res, pressedStateBitmap)
+        trackId = -1
+        width = this.defaultStateBitmap.intrinsicWidth
+        height = this.defaultStateBitmap.intrinsicHeight
+    }
+
+    /**
+     * Updates button status based on the motion event.
+     *
+     * @return true if value was changed
+     */
+    fun updateStatus(event: MotionEvent): Boolean {
+        val pointerIndex = event.actionIndex
+        val xPosition = event.getX(pointerIndex).toInt()
+        val yPosition = event.getY(pointerIndex).toInt()
+        val pointerId = event.getPointerId(pointerIndex)
+        val motionEvent = event.action and MotionEvent.ACTION_MASK
+        val isActionDown =
+            motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
+        val isActionUp =
+            motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
+        if (isActionDown) {
+            if (!bounds.contains(xPosition, yPosition)) {
+                return false
+            }
+            pressedState = true
+            trackId = pointerId
+            return true
+        }
+        if (isActionUp) {
+            if (trackId != pointerId) {
+                return false
+            }
+            pressedState = false
+            trackId = -1
+            return true
+        }
+        return false
+    }
+
+    fun onConfigureTouch(event: MotionEvent): Boolean {
+        val pointerIndex = event.actionIndex
+        val fingerPositionX = event.getX(pointerIndex).toInt()
+        val fingerPositionY = event.getY(pointerIndex).toInt()
+        when (event.action) {
+            MotionEvent.ACTION_DOWN -> {
+                previousTouchX = fingerPositionX
+                previousTouchY = fingerPositionY
+            }
+
+            MotionEvent.ACTION_MOVE -> {
+                controlPositionX += fingerPositionX - previousTouchX
+                controlPositionY += fingerPositionY - previousTouchY
+                setBounds(
+                    controlPositionX,
+                    controlPositionY,
+                    width + controlPositionX,
+                    height + controlPositionY
+                )
+                previousTouchX = fingerPositionX
+                previousTouchY = fingerPositionY
+            }
+        }
+        return true
+    }
+
+    fun setPosition(x: Int, y: Int) {
+        controlPositionX = x
+        controlPositionY = y
+    }
+
+    fun draw(canvas: Canvas) = currentStateBitmapDrawable.draw(canvas)
+
+    private val currentStateBitmapDrawable: BitmapDrawable
+        get() = if (pressedState) pressedStateBitmap else defaultStateBitmap
+
+    fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
+        defaultStateBitmap.setBounds(left, top, right, bottom)
+        pressedStateBitmap.setBounds(left, top, right, bottom)
+    }
+
+    val status: Int
+        get() = if (pressedState) NativeLibrary.ButtonState.PRESSED else NativeLibrary.ButtonState.RELEASED
+    val bounds: Rect
+        get() = defaultStateBitmap.bounds
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.java b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.java
deleted file mode 100644
index 2555b2c52..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.java
+++ /dev/null
@@ -1,299 +0,0 @@
-/**
- * Copyright 2016 Dolphin Emulator Project
- * Licensed under GPLv2+
- * Refer to the license.txt file included.
- */
-
-package org.citra.citra_emu.overlay;
-
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.BitmapDrawable;
-import android.view.MotionEvent;
-
-import org.citra.citra_emu.NativeLibrary;
-
-/**
- * Custom {@link BitmapDrawable} that is capable
- * of storing it's own ID.
- */
-public final class InputOverlayDrawableDpad {
-    public static final float VIRT_AXIS_DEADZONE = 0.5f;
-    // The ID identifying what type of button this Drawable represents.
-    private int mUpButtonId;
-    private int mDownButtonId;
-    private int mLeftButtonId;
-    private int mRightButtonId;
-    private int mTrackId;
-    private int mPreviousTouchX, mPreviousTouchY;
-    private int mControlPositionX, mControlPositionY;
-    private int mWidth;
-    private int mHeight;
-    private BitmapDrawable mDefaultStateBitmap;
-    private BitmapDrawable mPressedOneDirectionStateBitmap;
-    private BitmapDrawable mPressedTwoDirectionsStateBitmap;
-    private boolean mUpButtonState;
-    private boolean mDownButtonState;
-    private boolean mLeftButtonState;
-    private boolean mRightButtonState;
-
-    /**
-     * Constructor
-     *
-     * @param res                             {@link Resources} instance.
-     * @param defaultStateBitmap              {@link Bitmap} of the default state.
-     * @param pressedOneDirectionStateBitmap  {@link Bitmap} of the pressed state in one direction.
-     * @param pressedTwoDirectionsStateBitmap {@link Bitmap} of the pressed state in two direction.
-     * @param buttonUp                        Identifier for the up button.
-     * @param buttonDown                      Identifier for the down button.
-     * @param buttonLeft                      Identifier for the left button.
-     * @param buttonRight                     Identifier for the right button.
-     */
-    public InputOverlayDrawableDpad(Resources res,
-                                    Bitmap defaultStateBitmap,
-                                    Bitmap pressedOneDirectionStateBitmap,
-                                    Bitmap pressedTwoDirectionsStateBitmap,
-                                    int buttonUp, int buttonDown,
-                                    int buttonLeft, int buttonRight) {
-        mDefaultStateBitmap = new BitmapDrawable(res, defaultStateBitmap);
-        mPressedOneDirectionStateBitmap = new BitmapDrawable(res, pressedOneDirectionStateBitmap);
-        mPressedTwoDirectionsStateBitmap = new BitmapDrawable(res, pressedTwoDirectionsStateBitmap);
-
-        mWidth = mDefaultStateBitmap.getIntrinsicWidth();
-        mHeight = mDefaultStateBitmap.getIntrinsicHeight();
-
-        mUpButtonId = buttonUp;
-        mDownButtonId = buttonDown;
-        mLeftButtonId = buttonLeft;
-        mRightButtonId = buttonRight;
-
-        mTrackId = -1;
-    }
-
-    public boolean updateStatus(MotionEvent event, boolean dpadSlide) {
-        int pointerIndex = event.getActionIndex();
-        int xPosition = (int) event.getX(pointerIndex);
-        int yPosition = (int) event.getY(pointerIndex);
-        int pointerId = event.getPointerId(pointerIndex);
-        int motionEvent = event.getAction() & MotionEvent.ACTION_MASK;
-        boolean isActionDown = motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN;
-        boolean isActionUp = motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP;
-
-        if (isActionDown) {
-            if (!getBounds().contains(xPosition, yPosition)) {
-                return false;
-            }
-            mTrackId = pointerId;
-        }
-
-        if (isActionUp) {
-            if (mTrackId != pointerId) {
-                return false;
-            }
-            mTrackId = -1;
-            mUpButtonState = false;
-            mDownButtonState = false;
-            mLeftButtonState = false;
-            mRightButtonState = false;
-            return true;
-        }
-
-        if (mTrackId == -1) {
-            return false;
-        }
-
-        if (!dpadSlide && !isActionDown) {
-            return false;
-        }
-
-        for (int i = 0; i < event.getPointerCount(); i++) {
-            if (mTrackId != event.getPointerId(i)) {
-                continue;
-            }
-            float touchX = event.getX(i);
-            float touchY = event.getY(i);
-            float maxY = getBounds().bottom;
-            float maxX = getBounds().right;
-            touchX -= getBounds().centerX();
-            maxX -= getBounds().centerX();
-            touchY -= getBounds().centerY();
-            maxY -= getBounds().centerY();
-            final float AxisX = touchX / maxX;
-            final float AxisY = touchY / maxY;
-            final boolean upState = mUpButtonState;
-            final boolean downState = mDownButtonState;
-            final boolean leftState = mLeftButtonState;
-            final boolean rightState = mRightButtonState;
-
-            mUpButtonState = AxisY < -InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE;
-            mDownButtonState = AxisY > InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE;
-            mLeftButtonState = AxisX < -InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE;
-            mRightButtonState = AxisX > InputOverlayDrawableDpad.VIRT_AXIS_DEADZONE;
-            return upState != mUpButtonState || downState != mDownButtonState || leftState != mLeftButtonState || rightState != mRightButtonState;
-        }
-
-        return false;
-    }
-
-    public void draw(Canvas canvas) {
-        int px = mControlPositionX + (getWidth() / 2);
-        int py = mControlPositionY + (getHeight() / 2);
-
-        // Pressed up
-        if (mUpButtonState && !mLeftButtonState && !mRightButtonState) {
-            mPressedOneDirectionStateBitmap.draw(canvas);
-            return;
-        }
-
-        // Pressed down
-        if (mDownButtonState && !mLeftButtonState && !mRightButtonState) {
-            canvas.save();
-            canvas.rotate(180, px, py);
-            mPressedOneDirectionStateBitmap.draw(canvas);
-            canvas.restore();
-            return;
-        }
-
-        // Pressed left
-        if (mLeftButtonState && !mUpButtonState && !mDownButtonState) {
-            canvas.save();
-            canvas.rotate(270, px, py);
-            mPressedOneDirectionStateBitmap.draw(canvas);
-            canvas.restore();
-            return;
-        }
-
-        // Pressed right
-        if (mRightButtonState && !mUpButtonState && !mDownButtonState) {
-            canvas.save();
-            canvas.rotate(90, px, py);
-            mPressedOneDirectionStateBitmap.draw(canvas);
-            canvas.restore();
-            return;
-        }
-
-        // Pressed up left
-        if (mUpButtonState && mLeftButtonState && !mRightButtonState) {
-            mPressedTwoDirectionsStateBitmap.draw(canvas);
-            return;
-        }
-
-        // Pressed up right
-        if (mUpButtonState && !mLeftButtonState && mRightButtonState) {
-            canvas.save();
-            canvas.rotate(90, px, py);
-            mPressedTwoDirectionsStateBitmap.draw(canvas);
-            canvas.restore();
-            return;
-        }
-
-        // Pressed down left
-        if (mDownButtonState && mLeftButtonState && !mRightButtonState) {
-            canvas.save();
-            canvas.rotate(270, px, py);
-            mPressedTwoDirectionsStateBitmap.draw(canvas);
-            canvas.restore();
-            return;
-        }
-
-        // Pressed down right
-        if (mDownButtonState && !mLeftButtonState && mRightButtonState) {
-            canvas.save();
-            canvas.rotate(180, px, py);
-            mPressedTwoDirectionsStateBitmap.draw(canvas);
-            canvas.restore();
-            return;
-        }
-
-        // Not pressed
-        mDefaultStateBitmap.draw(canvas);
-    }
-
-    public int getUpId() {
-        return mUpButtonId;
-    }
-
-    public int getDownId() {
-        return mDownButtonId;
-    }
-
-    public int getLeftId() {
-        return mLeftButtonId;
-    }
-
-    public int getRightId() {
-        return mRightButtonId;
-    }
-
-    public int getTrackId() {
-        return mTrackId;
-    }
-
-    public void setTrackId(int trackId) {
-        mTrackId = trackId;
-    }
-
-    public int getUpStatus() {
-        return mUpButtonState ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED;
-    }
-
-    public int getDownStatus() {
-        return mDownButtonState ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED;
-    }
-
-    public int getLeftStatus() {
-        return mLeftButtonState ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED;
-    }
-
-    public int getRightStatus() {
-        return mRightButtonState ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED;
-    }
-
-    public boolean onConfigureTouch(MotionEvent event) {
-        int pointerIndex = event.getActionIndex();
-        int fingerPositionX = (int) event.getX(pointerIndex);
-        int fingerPositionY = (int) event.getY(pointerIndex);
-        switch (event.getAction()) {
-            case MotionEvent.ACTION_DOWN:
-                mPreviousTouchX = fingerPositionX;
-                mPreviousTouchY = fingerPositionY;
-                break;
-            case MotionEvent.ACTION_MOVE:
-                mControlPositionX += fingerPositionX - mPreviousTouchX;
-                mControlPositionY += fingerPositionY - mPreviousTouchY;
-                setBounds(mControlPositionX, mControlPositionY, getWidth() + mControlPositionX,
-                        getHeight() + mControlPositionY);
-                mPreviousTouchX = fingerPositionX;
-                mPreviousTouchY = fingerPositionY;
-                break;
-
-        }
-        return true;
-    }
-
-    public void setPosition(int x, int y) {
-        mControlPositionX = x;
-        mControlPositionY = y;
-    }
-
-    public void setBounds(int left, int top, int right, int bottom) {
-        mDefaultStateBitmap.setBounds(left, top, right, bottom);
-        mPressedOneDirectionStateBitmap.setBounds(left, top, right, bottom);
-        mPressedTwoDirectionsStateBitmap.setBounds(left, top, right, bottom);
-    }
-
-    public Rect getBounds() {
-        return mDefaultStateBitmap.getBounds();
-    }
-
-    public int getWidth() {
-        return mWidth;
-    }
-
-    public int getHeight() {
-        return mHeight;
-    }
-
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.kt b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.kt
new file mode 100644
index 000000000..f7a5a3fe5
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableDpad.kt
@@ -0,0 +1,262 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.overlay
+
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.BitmapDrawable
+import android.view.MotionEvent
+import org.citra.citra_emu.NativeLibrary
+
+/**
+ * Custom [BitmapDrawable] that is capable
+ * of storing it's own ID.
+ *
+ * @param res                             [Resources] instance.
+ * @param defaultStateBitmap              [Bitmap] of the default state.
+ * @param pressedOneDirectionStateBitmap  [Bitmap] of the pressed state in one direction.
+ * @param pressedTwoDirectionsStateBitmap [Bitmap] of the pressed state in two direction.
+ * @param upId                            Identifier for the up button.
+ * @param downId                          Identifier for the down button.
+ * @param leftId                          Identifier for the left button.
+ * @param rightId                         Identifier for the right button.
+ */
+class InputOverlayDrawableDpad(
+    res: Resources,
+    defaultStateBitmap: Bitmap,
+    pressedOneDirectionStateBitmap: Bitmap,
+    pressedTwoDirectionsStateBitmap: Bitmap,
+    val upId: Int,
+    val downId: Int,
+    val leftId: Int,
+    val rightId: Int
+) {
+    var trackId: Int
+    private var previousTouchX = 0
+    private var previousTouchY = 0
+    private var controlPositionX = 0
+    private var controlPositionY = 0
+    val width: Int
+    val height: Int
+    private val defaultStateBitmap: BitmapDrawable
+    private val pressedOneDirectionStateBitmap: BitmapDrawable
+    private val pressedTwoDirectionsStateBitmap: BitmapDrawable
+    private var upButtonState = false
+    private var downButtonState = false
+    private var leftButtonState = false
+    private var rightButtonState = false
+
+    init {
+        this.defaultStateBitmap = BitmapDrawable(res, defaultStateBitmap)
+        this.pressedOneDirectionStateBitmap = BitmapDrawable(res, pressedOneDirectionStateBitmap)
+        this.pressedTwoDirectionsStateBitmap = BitmapDrawable(res, pressedTwoDirectionsStateBitmap)
+        width = this.defaultStateBitmap.intrinsicWidth
+        height = this.defaultStateBitmap.intrinsicHeight
+        trackId = -1
+    }
+
+    fun updateStatus(event: MotionEvent, dpadSlide: Boolean): Boolean {
+        val pointerIndex = event.actionIndex
+        val xPosition = event.getX(pointerIndex).toInt()
+        val yPosition = event.getY(pointerIndex).toInt()
+        val pointerId = event.getPointerId(pointerIndex)
+        val motionEvent = event.action and MotionEvent.ACTION_MASK
+        val isActionDown =
+            motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
+        val isActionUp =
+            motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
+        if (isActionDown) {
+            if (!bounds.contains(xPosition, yPosition)) {
+                return false
+            }
+            trackId = pointerId
+        }
+        if (isActionUp) {
+            if (trackId != pointerId) {
+                return false
+            }
+            trackId = -1
+            upButtonState = false
+            downButtonState = false
+            leftButtonState = false
+            rightButtonState = false
+            return true
+        }
+        if (trackId == -1) {
+            return false
+        }
+        if (!dpadSlide && !isActionDown) {
+            return false
+        }
+        for (i in 0 until event.pointerCount) {
+            if (trackId != event.getPointerId(i)) {
+                continue
+            }
+            var touchX = event.getX(i)
+            var touchY = event.getY(i)
+            var maxY = bounds.bottom.toFloat()
+            var maxX = bounds.right.toFloat()
+            touchX -= bounds.centerX().toFloat()
+            maxX -= bounds.centerX().toFloat()
+            touchY -= bounds.centerY().toFloat()
+            maxY -= bounds.centerY().toFloat()
+            val xAxis = touchX / maxX
+            val yAxis = touchY / maxY
+            val upState = upButtonState
+            val downState = downButtonState
+            val leftState = leftButtonState
+            val rightState = rightButtonState
+            upButtonState = yAxis < -VIRT_AXIS_DEADZONE
+            downButtonState = yAxis > VIRT_AXIS_DEADZONE
+            leftButtonState = xAxis < -VIRT_AXIS_DEADZONE
+            rightButtonState = xAxis > VIRT_AXIS_DEADZONE
+            return upState != upButtonState || downState != downButtonState || leftState != leftButtonState || rightState != rightButtonState
+        }
+        return false
+    }
+
+    fun draw(canvas: Canvas) {
+        val px = controlPositionX + width / 2
+        val py = controlPositionY + height / 2
+
+        // Pressed up
+        if (upButtonState && !leftButtonState && !rightButtonState) {
+            pressedOneDirectionStateBitmap.draw(canvas)
+            return
+        }
+
+        // Pressed down
+        if (downButtonState && !leftButtonState && !rightButtonState) {
+            canvas.save()
+            canvas.rotate(180f, px.toFloat(), py.toFloat())
+            pressedOneDirectionStateBitmap.draw(canvas)
+            canvas.restore()
+            return
+        }
+
+        // Pressed left
+        if (leftButtonState && !upButtonState && !downButtonState) {
+            canvas.save()
+            canvas.rotate(270f, px.toFloat(), py.toFloat())
+            pressedOneDirectionStateBitmap.draw(canvas)
+            canvas.restore()
+            return
+        }
+
+        // Pressed right
+        if (rightButtonState && !upButtonState && !downButtonState) {
+            canvas.save()
+            canvas.rotate(90f, px.toFloat(), py.toFloat())
+            pressedOneDirectionStateBitmap.draw(canvas)
+            canvas.restore()
+            return
+        }
+
+        // Pressed up left
+        if (upButtonState && leftButtonState && !rightButtonState) {
+            pressedTwoDirectionsStateBitmap.draw(canvas)
+            return
+        }
+
+        // Pressed up right
+        if (upButtonState && !leftButtonState && rightButtonState) {
+            canvas.save()
+            canvas.rotate(90f, px.toFloat(), py.toFloat())
+            pressedTwoDirectionsStateBitmap.draw(canvas)
+            canvas.restore()
+            return
+        }
+
+        // Pressed down left
+        if (downButtonState && leftButtonState && !rightButtonState) {
+            canvas.save()
+            canvas.rotate(270f, px.toFloat(), py.toFloat())
+            pressedTwoDirectionsStateBitmap.draw(canvas)
+            canvas.restore()
+            return
+        }
+
+        // Pressed down right
+        if (downButtonState && !leftButtonState && rightButtonState) {
+            canvas.save()
+            canvas.rotate(180f, px.toFloat(), py.toFloat())
+            pressedTwoDirectionsStateBitmap.draw(canvas)
+            canvas.restore()
+            return
+        }
+
+        // Not pressed
+        defaultStateBitmap.draw(canvas)
+    }
+
+    val upStatus: Int
+        get() = if (upButtonState) {
+            NativeLibrary.ButtonState.PRESSED
+        } else {
+            NativeLibrary.ButtonState.RELEASED
+        }
+    val downStatus: Int
+        get() = if (downButtonState) {
+            NativeLibrary.ButtonState.PRESSED
+        } else {
+            NativeLibrary.ButtonState.RELEASED
+        }
+    val leftStatus: Int
+        get() = if (leftButtonState) {
+            NativeLibrary.ButtonState.PRESSED
+        } else {
+            NativeLibrary.ButtonState.RELEASED
+        }
+    val rightStatus: Int
+        get() = if (rightButtonState) {
+            NativeLibrary.ButtonState.PRESSED
+        } else {
+            NativeLibrary.ButtonState.RELEASED
+        }
+
+    fun onConfigureTouch(event: MotionEvent): Boolean {
+        val pointerIndex = event.actionIndex
+        val fingerPositionX = event.getX(pointerIndex).toInt()
+        val fingerPositionY = event.getY(pointerIndex).toInt()
+        when (event.action) {
+            MotionEvent.ACTION_DOWN -> {
+                previousTouchX = fingerPositionX
+                previousTouchY = fingerPositionY
+            }
+
+            MotionEvent.ACTION_MOVE -> {
+                controlPositionX += fingerPositionX - previousTouchX
+                controlPositionY += fingerPositionY - previousTouchY
+                setBounds(
+                    controlPositionX, controlPositionY, width + controlPositionX,
+                    height + controlPositionY
+                )
+                previousTouchX = fingerPositionX
+                previousTouchY = fingerPositionY
+            }
+        }
+        return true
+    }
+
+    fun setPosition(x: Int, y: Int) {
+        controlPositionX = x
+        controlPositionY = y
+    }
+
+    fun setBounds(left: Int, top: Int, right: Int, bottom: Int) {
+        defaultStateBitmap.setBounds(left, top, right, bottom)
+        pressedOneDirectionStateBitmap.setBounds(left, top, right, bottom)
+        pressedTwoDirectionsStateBitmap.setBounds(left, top, right, bottom)
+    }
+
+    val bounds: Rect
+        get() = defaultStateBitmap.bounds
+
+    companion object {
+        private const val VIRT_AXIS_DEADZONE = 0.5f
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.java b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.java
deleted file mode 100644
index f25771afc..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.java
+++ /dev/null
@@ -1,267 +0,0 @@
-/**
- * Copyright 2013 Dolphin Emulator Project
- * Licensed under GPLv2+
- * Refer to the license.txt file included.
- */
-
-package org.citra.citra_emu.overlay;
-
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.BitmapDrawable;
-import android.view.MotionEvent;
-
-import org.citra.citra_emu.NativeLibrary.ButtonType;
-import org.citra.citra_emu.utils.EmulationMenuSettings;
-
-/**
- * Custom {@link BitmapDrawable} that is capable
- * of storing it's own ID.
- */
-public final class InputOverlayDrawableJoystick {
-    // The ID value what type of joystick this Drawable represents.
-    private int mJoystickId;
-    // The ID value what motion event is tracking
-    private int mTrackId = -1;
-    private float mXAxis;
-    private float mYAxis;
-    private int mControlPositionX, mControlPositionY;
-    private int mPreviousTouchX, mPreviousTouchY;
-    private int mWidth;
-    private int mHeight;
-    private Rect mVirtBounds;
-    private Rect mOrigBounds;
-    private BitmapDrawable mOuterBitmap;
-    private BitmapDrawable mDefaultStateInnerBitmap;
-    private BitmapDrawable mPressedStateInnerBitmap;
-    private BitmapDrawable mBoundsBoxBitmap;
-    private boolean mPressedState = false;
-
-    /**
-     * Constructor
-     *
-     * @param res                {@link Resources} instance.
-     * @param bitmapOuter        {@link Bitmap} which represents the outer non-movable part of the joystick.
-     * @param bitmapInnerDefault {@link Bitmap} which represents the default inner movable part of the joystick.
-     * @param bitmapInnerPressed {@link Bitmap} which represents the pressed inner movable part of the joystick.
-     * @param rectOuter          {@link Rect} which represents the outer joystick bounds.
-     * @param rectInner          {@link Rect} which represents the inner joystick bounds.
-     * @param joystick           Identifier for which joystick this is.
-     */
-    public InputOverlayDrawableJoystick(Resources res, Bitmap bitmapOuter,
-                                        Bitmap bitmapInnerDefault, Bitmap bitmapInnerPressed,
-                                        Rect rectOuter, Rect rectInner, int joystick) {
-        mJoystickId = joystick;
-
-        mOuterBitmap = new BitmapDrawable(res, bitmapOuter);
-        mDefaultStateInnerBitmap = new BitmapDrawable(res, bitmapInnerDefault);
-        mPressedStateInnerBitmap = new BitmapDrawable(res, bitmapInnerPressed);
-        mBoundsBoxBitmap = new BitmapDrawable(res, bitmapOuter);
-        mWidth = bitmapOuter.getWidth();
-        mHeight = bitmapOuter.getHeight();
-
-        setBounds(rectOuter);
-        mDefaultStateInnerBitmap.setBounds(rectInner);
-        mPressedStateInnerBitmap.setBounds(rectInner);
-        mVirtBounds = getBounds();
-        mOrigBounds = mOuterBitmap.copyBounds();
-        mBoundsBoxBitmap.setAlpha(0);
-        mBoundsBoxBitmap.setBounds(getVirtBounds());
-        SetInnerBounds();
-    }
-
-    public void draw(Canvas canvas) {
-        mOuterBitmap.draw(canvas);
-        getCurrentStateBitmapDrawable().draw(canvas);
-        mBoundsBoxBitmap.draw(canvas);
-    }
-
-    public boolean updateStatus(MotionEvent event) {
-        int pointerIndex = event.getActionIndex();
-        int xPosition = (int) event.getX(pointerIndex);
-        int yPosition = (int) event.getY(pointerIndex);
-        int pointerId = event.getPointerId(pointerIndex);
-        int motionEvent = event.getAction() & MotionEvent.ACTION_MASK;
-        boolean isActionDown = motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN;
-        boolean isActionUp = motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP;
-
-        if (isActionDown) {
-            if (!getBounds().contains(xPosition, yPosition)) {
-                return false;
-            }
-            mPressedState = true;
-            mOuterBitmap.setAlpha(0);
-            mBoundsBoxBitmap.setAlpha(255);
-            if (EmulationMenuSettings.INSTANCE.getJoystickRelCenter()) {
-                getVirtBounds().offset(xPosition - getVirtBounds().centerX(),
-                        yPosition - getVirtBounds().centerY());
-            }
-            mBoundsBoxBitmap.setBounds(getVirtBounds());
-            mTrackId = pointerId;
-        }
-
-        if (isActionUp) {
-            if (mTrackId != pointerId) {
-                return false;
-            }
-            mPressedState = false;
-            mXAxis = 0.0f;
-            mYAxis = 0.0f;
-            mOuterBitmap.setAlpha(255);
-            mBoundsBoxBitmap.setAlpha(0);
-            setVirtBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right,
-                    mOrigBounds.bottom));
-            setBounds(new Rect(mOrigBounds.left, mOrigBounds.top, mOrigBounds.right,
-                    mOrigBounds.bottom));
-            SetInnerBounds();
-            mTrackId = -1;
-            return true;
-        }
-
-        if (mTrackId == -1)
-            return false;
-
-        for (int i = 0; i < event.getPointerCount(); i++) {
-            if (mTrackId != event.getPointerId(i)) {
-                continue;
-            }
-            float touchX = event.getX(i);
-            float touchY = event.getY(i);
-            float maxY = getVirtBounds().bottom;
-            float maxX = getVirtBounds().right;
-            touchX -= getVirtBounds().centerX();
-            maxX -= getVirtBounds().centerX();
-            touchY -= getVirtBounds().centerY();
-            maxY -= getVirtBounds().centerY();
-            final float AxisX = touchX / maxX;
-            final float AxisY = touchY / maxY;
-            final float oldXAxis = mXAxis;
-            final float oldYAxis = mYAxis;
-
-            // Clamp the circle pad input to a circle
-            final float angle = (float) Math.atan2(AxisY, AxisX);
-            float radius = (float) Math.sqrt(AxisX * AxisX + AxisY * AxisY);
-            if (radius > 1.0f) {
-                radius = 1.0f;
-            }
-            mXAxis = ((float) Math.cos(angle) * radius);
-            mYAxis = ((float) Math.sin(angle) * radius);
-            SetInnerBounds();
-            return oldXAxis != mXAxis && oldYAxis != mYAxis;
-        }
-
-        return false;
-    }
-
-    public boolean onConfigureTouch(MotionEvent event) {
-        int pointerIndex = event.getActionIndex();
-        int fingerPositionX = (int) event.getX(pointerIndex);
-        int fingerPositionY = (int) event.getY(pointerIndex);
-
-        int scale = 1;
-        if (mJoystickId == ButtonType.STICK_C) {
-            // C-stick is scaled down to be half the size of the circle pad
-            scale = 2;
-        }
-
-        switch (event.getAction()) {
-            case MotionEvent.ACTION_DOWN:
-                mPreviousTouchX = fingerPositionX;
-                mPreviousTouchY = fingerPositionY;
-                break;
-            case MotionEvent.ACTION_MOVE:
-                int deltaX = fingerPositionX - mPreviousTouchX;
-                int deltaY = fingerPositionY - mPreviousTouchY;
-                mControlPositionX += deltaX;
-                mControlPositionY += deltaY;
-                setBounds(new Rect(mControlPositionX, mControlPositionY,
-                        mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX,
-                        mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY));
-                setVirtBounds(new Rect(mControlPositionX, mControlPositionY,
-                        mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX,
-                        mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY));
-                SetInnerBounds();
-                setOrigBounds(new Rect(new Rect(mControlPositionX, mControlPositionY,
-                        mOuterBitmap.getIntrinsicWidth() / scale + mControlPositionX,
-                        mOuterBitmap.getIntrinsicHeight() / scale + mControlPositionY)));
-                mPreviousTouchX = fingerPositionX;
-                mPreviousTouchY = fingerPositionY;
-                break;
-        }
-        return true;
-    }
-
-    public int getJoystickId() {
-        return mJoystickId;
-    }
-
-    public float getXAxis() {
-        return mXAxis;
-    }
-
-    public float getYAxis() {
-        return mYAxis;
-    }
-
-    public int getTrackId() {
-        return mTrackId;
-    }
-
-    private void SetInnerBounds() {
-        int X = getVirtBounds().centerX() + (int) ((mXAxis) * (getVirtBounds().width() / 2));
-        int Y = getVirtBounds().centerY() + (int) ((mYAxis) * (getVirtBounds().height() / 2));
-
-        if (X > getVirtBounds().centerX() + (getVirtBounds().width() / 2))
-            X = getVirtBounds().centerX() + (getVirtBounds().width() / 2);
-        if (X < getVirtBounds().centerX() - (getVirtBounds().width() / 2))
-            X = getVirtBounds().centerX() - (getVirtBounds().width() / 2);
-        if (Y > getVirtBounds().centerY() + (getVirtBounds().height() / 2))
-            Y = getVirtBounds().centerY() + (getVirtBounds().height() / 2);
-        if (Y < getVirtBounds().centerY() - (getVirtBounds().height() / 2))
-            Y = getVirtBounds().centerY() - (getVirtBounds().height() / 2);
-
-        int width = mPressedStateInnerBitmap.getBounds().width() / 2;
-        int height = mPressedStateInnerBitmap.getBounds().height() / 2;
-        mDefaultStateInnerBitmap.setBounds(X - width, Y - height, X + width, Y + height);
-        mPressedStateInnerBitmap.setBounds(mDefaultStateInnerBitmap.getBounds());
-    }
-
-    public void setPosition(int x, int y) {
-        mControlPositionX = x;
-        mControlPositionY = y;
-    }
-
-    private BitmapDrawable getCurrentStateBitmapDrawable() {
-        return mPressedState ? mPressedStateInnerBitmap : mDefaultStateInnerBitmap;
-    }
-
-    public Rect getBounds() {
-        return mOuterBitmap.getBounds();
-    }
-
-    public void setBounds(Rect bounds) {
-        mOuterBitmap.setBounds(bounds);
-    }
-
-    private void setOrigBounds(Rect bounds) {
-        mOrigBounds = bounds;
-    }
-
-    private Rect getVirtBounds() {
-        return mVirtBounds;
-    }
-
-    private void setVirtBounds(Rect bounds) {
-        mVirtBounds = bounds;
-    }
-
-    public int getWidth() {
-        return mWidth;
-    }
-
-    public int getHeight() {
-        return mHeight;
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.kt b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.kt
new file mode 100644
index 000000000..f521077a4
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/overlay/InputOverlayDrawableJoystick.kt
@@ -0,0 +1,238 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+package org.citra.citra_emu.overlay
+
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.BitmapDrawable
+import android.view.MotionEvent
+import org.citra.citra_emu.NativeLibrary
+import org.citra.citra_emu.utils.EmulationMenuSettings
+import kotlin.math.atan2
+import kotlin.math.cos
+import kotlin.math.sin
+import kotlin.math.sqrt
+
+/**
+ * Custom [BitmapDrawable] that is capable
+ * of storing it's own ID.
+ *
+ * @param res                [Resources] instance.
+ * @param bitmapOuter        [Bitmap] which represents the outer non-movable part of the joystick.
+ * @param bitmapInnerDefault [Bitmap] which represents the default inner movable part of the joystick.
+ * @param bitmapInnerPressed [Bitmap] which represents the pressed inner movable part of the joystick.
+ * @param rectOuter          [Rect] which represents the outer joystick bounds.
+ * @param rectInner          [Rect] which represents the inner joystick bounds.
+ * @param joystickId         Identifier for which joystick this is.
+ */
+class InputOverlayDrawableJoystick(
+    res: Resources,
+    bitmapOuter: Bitmap,
+    bitmapInnerDefault: Bitmap,
+    bitmapInnerPressed: Bitmap,
+    rectOuter: Rect,
+    rectInner: Rect,
+    val joystickId: Int
+) {
+    var trackId = -1
+    var xAxis = 0f
+    var yAxis = 0f
+    private var controlPositionX = 0
+    private var controlPositionY = 0
+    private var previousTouchX = 0
+    private var previousTouchY = 0
+    val width: Int
+    val height: Int
+    private var virtBounds: Rect
+    private var origBounds: Rect
+    private val outerBitmap: BitmapDrawable
+    private val defaultStateInnerBitmap: BitmapDrawable
+    private val pressedStateInnerBitmap: BitmapDrawable
+    private val boundsBoxBitmap: BitmapDrawable
+    private var pressedState = false
+
+    var bounds: Rect
+        get() = outerBitmap.bounds
+        set(bounds) {
+            outerBitmap.bounds = bounds
+        }
+
+    init {
+        outerBitmap = BitmapDrawable(res, bitmapOuter)
+        defaultStateInnerBitmap = BitmapDrawable(res, bitmapInnerDefault)
+        pressedStateInnerBitmap = BitmapDrawable(res, bitmapInnerPressed)
+        boundsBoxBitmap = BitmapDrawable(res, bitmapOuter)
+        width = bitmapOuter.width
+        height = bitmapOuter.height
+        bounds = rectOuter
+        defaultStateInnerBitmap.bounds = rectInner
+        pressedStateInnerBitmap.bounds = rectInner
+        virtBounds = bounds
+        origBounds = outerBitmap.copyBounds()
+        boundsBoxBitmap.alpha = 0
+        boundsBoxBitmap.bounds = virtBounds
+        setInnerBounds()
+    }
+
+    fun draw(canvas: Canvas?) {
+        outerBitmap.draw(canvas!!)
+        currentStateBitmapDrawable.draw(canvas)
+        boundsBoxBitmap.draw(canvas)
+    }
+
+    fun updateStatus(event: MotionEvent): Boolean {
+        val pointerIndex = event.actionIndex
+        val xPosition = event.getX(pointerIndex).toInt()
+        val yPosition = event.getY(pointerIndex).toInt()
+        val pointerId = event.getPointerId(pointerIndex)
+        val motionEvent = event.action and MotionEvent.ACTION_MASK
+        val isActionDown =
+            motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
+        val isActionUp =
+            motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
+        if (isActionDown) {
+            if (!bounds.contains(xPosition, yPosition)) {
+                return false
+            }
+            pressedState = true
+            outerBitmap.alpha = 0
+            boundsBoxBitmap.alpha = 255
+            if (EmulationMenuSettings.joystickRelCenter) {
+                virtBounds.offset(
+                    xPosition - virtBounds.centerX(),
+                    yPosition - virtBounds.centerY()
+                )
+            }
+            boundsBoxBitmap.bounds = virtBounds
+            trackId = pointerId
+        }
+        if (isActionUp) {
+            if (trackId != pointerId) {
+                return false
+            }
+            pressedState = false
+            xAxis = 0.0f
+            yAxis = 0.0f
+            outerBitmap.alpha = 255
+            boundsBoxBitmap.alpha = 0
+            virtBounds = Rect(origBounds.left, origBounds.top, origBounds.right, origBounds.bottom)
+            bounds = Rect(origBounds.left, origBounds.top, origBounds.right, origBounds.bottom)
+            setInnerBounds()
+            trackId = -1
+            return true
+        }
+        if (trackId == -1) return false
+        for (i in 0 until event.pointerCount) {
+            if (trackId != event.getPointerId(i)) {
+                continue
+            }
+            var touchX = event.getX(i)
+            var touchY = event.getY(i)
+            var maxY = virtBounds.bottom.toFloat()
+            var maxX = virtBounds.right.toFloat()
+            touchX -= virtBounds.centerX().toFloat()
+            maxX -= virtBounds.centerX().toFloat()
+            touchY -= virtBounds.centerY().toFloat()
+            maxY -= virtBounds.centerY().toFloat()
+            val xAxis = touchX / maxX
+            val yAxis = touchY / maxY
+            val oldXAxis = this.xAxis
+            val oldYAxis = this.yAxis
+
+            // Clamp the circle pad input to a circle
+            val angle = atan2(yAxis.toDouble(), xAxis.toDouble()).toFloat()
+            var radius = sqrt((xAxis * xAxis + yAxis * yAxis).toDouble()).toFloat()
+            if (radius > 1.0f) {
+                radius = 1.0f
+            }
+            this.xAxis = cos(angle.toDouble()).toFloat() * radius
+            this.yAxis = sin(angle.toDouble()).toFloat() * radius
+            setInnerBounds()
+            return oldXAxis != this.xAxis && oldYAxis != this.yAxis
+        }
+        return false
+    }
+
+    fun onConfigureTouch(event: MotionEvent): Boolean {
+        val pointerIndex = event.actionIndex
+        val fingerPositionX = event.getX(pointerIndex).toInt()
+        val fingerPositionY = event.getY(pointerIndex).toInt()
+        var scale = 1
+        if (joystickId == NativeLibrary.ButtonType.STICK_C) {
+            // C-stick is scaled down to be half the size of the circle pad
+            scale = 2
+        }
+        when (event.action) {
+            MotionEvent.ACTION_DOWN -> {
+                previousTouchX = fingerPositionX
+                previousTouchY = fingerPositionY
+            }
+
+            MotionEvent.ACTION_MOVE -> {
+                val deltaX = fingerPositionX - previousTouchX
+                val deltaY = fingerPositionY - previousTouchY
+                controlPositionX += deltaX
+                controlPositionY += deltaY
+                bounds = Rect(
+                    controlPositionX,
+                    controlPositionY,
+                    outerBitmap.intrinsicWidth / scale + controlPositionX,
+                    outerBitmap.intrinsicHeight / scale + controlPositionY
+                )
+                virtBounds = Rect(
+                    controlPositionX,
+                    controlPositionY,
+                    outerBitmap.intrinsicWidth / scale + controlPositionX,
+                    outerBitmap.intrinsicHeight / scale + controlPositionY
+                )
+                setInnerBounds()
+                setOrigBounds(
+                    Rect(
+                        Rect(
+                            controlPositionX,
+                            controlPositionY,
+                            outerBitmap.intrinsicWidth / scale + controlPositionX,
+                            outerBitmap.intrinsicHeight / scale + controlPositionY
+                        )
+                    )
+                )
+                previousTouchX = fingerPositionX
+                previousTouchY = fingerPositionY
+            }
+        }
+        return true
+    }
+
+    private fun setInnerBounds() {
+        var x = virtBounds.centerX() + (xAxis * (virtBounds.width() / 2)).toInt()
+        var y = virtBounds.centerY() + (yAxis * (virtBounds.height() / 2)).toInt()
+        if (x > virtBounds.centerX() + virtBounds.width() / 2) x =
+            virtBounds.centerX() + virtBounds.width() / 2
+        if (x < virtBounds.centerX() - virtBounds.width() / 2) x =
+            virtBounds.centerX() - virtBounds.width() / 2
+        if (y > virtBounds.centerY() + virtBounds.height() / 2) y =
+            virtBounds.centerY() + virtBounds.height() / 2
+        if (y < virtBounds.centerY() - virtBounds.height() / 2) y =
+            virtBounds.centerY() - virtBounds.height() / 2
+        val width = pressedStateInnerBitmap.bounds.width() / 2
+        val height = pressedStateInnerBitmap.bounds.height() / 2
+        defaultStateInnerBitmap.setBounds(x - width, y - height, x + width, y + height)
+        pressedStateInnerBitmap.bounds = defaultStateInnerBitmap.bounds
+    }
+
+    fun setPosition(x: Int, y: Int) {
+        controlPositionX = x
+        controlPositionY = y
+    }
+
+    private val currentStateBitmapDrawable: BitmapDrawable
+        get() = if (pressedState) pressedStateInnerBitmap else defaultStateInnerBitmap
+
+    private fun setOrigBounds(bounds: Rect) {
+        origBounds = bounds
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/DividerItemDecoration.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/DividerItemDecoration.java
deleted file mode 100644
index 96ccc08bb..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/ui/DividerItemDecoration.java
+++ /dev/null
@@ -1,130 +0,0 @@
-package org.citra.citra_emu.ui;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.util.AttributeSet;
-import android.view.View;
-
-import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-
-/**
- * Implementation from:
- * https://gist.github.com/lapastillaroja/858caf1a82791b6c1a36
- */
-public class DividerItemDecoration extends RecyclerView.ItemDecoration {
-
-    private Drawable mDivider;
-    private boolean mShowFirstDivider = false;
-    private boolean mShowLastDivider = false;
-
-    public DividerItemDecoration(Context context, AttributeSet attrs) {
-        final TypedArray a = context
-                .obtainStyledAttributes(attrs, new int[]{android.R.attr.listDivider});
-        mDivider = a.getDrawable(0);
-        a.recycle();
-    }
-
-    public DividerItemDecoration(Context context, AttributeSet attrs, boolean showFirstDivider,
-                                 boolean showLastDivider) {
-        this(context, attrs);
-        mShowFirstDivider = showFirstDivider;
-        mShowLastDivider = showLastDivider;
-    }
-
-    public DividerItemDecoration(Drawable divider) {
-        mDivider = divider;
-    }
-
-    public DividerItemDecoration(Drawable divider, boolean showFirstDivider,
-                                 boolean showLastDivider) {
-        this(divider);
-        mShowFirstDivider = showFirstDivider;
-        mShowLastDivider = showLastDivider;
-    }
-
-    @Override
-    public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent,
-                               @NonNull RecyclerView.State state) {
-        super.getItemOffsets(outRect, view, parent, state);
-        if (mDivider == null) {
-            return;
-        }
-        if (parent.getChildAdapterPosition(view) < 1) {
-            return;
-        }
-
-        if (getOrientation(parent) == LinearLayoutManager.VERTICAL) {
-            outRect.top = mDivider.getIntrinsicHeight();
-        } else {
-            outRect.left = mDivider.getIntrinsicWidth();
-        }
-    }
-
-    @Override
-    public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
-        if (mDivider == null) {
-            super.onDrawOver(c, parent, state);
-            return;
-        }
-
-        // Initialization needed to avoid compiler warning
-        int left = 0, right = 0, top = 0, bottom = 0, size;
-        int orientation = getOrientation(parent);
-        int childCount = parent.getChildCount();
-
-        if (orientation == LinearLayoutManager.VERTICAL) {
-            size = mDivider.getIntrinsicHeight();
-            left = parent.getPaddingLeft();
-            right = parent.getWidth() - parent.getPaddingRight();
-        } else { //horizontal
-            size = mDivider.getIntrinsicWidth();
-            top = parent.getPaddingTop();
-            bottom = parent.getHeight() - parent.getPaddingBottom();
-        }
-
-        for (int i = mShowFirstDivider ? 0 : 1; i < childCount; i++) {
-            View child = parent.getChildAt(i);
-            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
-
-            if (orientation == LinearLayoutManager.VERTICAL) {
-                top = child.getTop() - params.topMargin;
-                bottom = top + size;
-            } else { //horizontal
-                left = child.getLeft() - params.leftMargin;
-                right = left + size;
-            }
-            mDivider.setBounds(left, top, right, bottom);
-            mDivider.draw(c);
-        }
-
-        // show last divider
-        if (mShowLastDivider && childCount > 0) {
-            View child = parent.getChildAt(childCount - 1);
-            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
-            if (orientation == LinearLayoutManager.VERTICAL) {
-                top = child.getBottom() + params.bottomMargin;
-                bottom = top + size;
-            } else { // horizontal
-                left = child.getRight() + params.rightMargin;
-                right = left + size;
-            }
-            mDivider.setBounds(left, top, right, bottom);
-            mDivider.draw(c);
-        }
-    }
-
-    private int getOrientation(RecyclerView parent) {
-        if (parent.getLayoutManager() instanceof LinearLayoutManager) {
-            LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
-            return layoutManager.getOrientation();
-        } else {
-            throw new IllegalStateException(
-                    "DividerItemDecoration can only be used with a LinearLayoutManager.");
-        }
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/TwoPaneOnBackPressedCallback.java b/src/android/app/src/main/java/org/citra/citra_emu/ui/TwoPaneOnBackPressedCallback.java
deleted file mode 100644
index 84ddf1439..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/ui/TwoPaneOnBackPressedCallback.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package org.citra.citra_emu.ui;
-
-import android.content.Context;
-import android.view.View;
-import android.view.inputmethod.InputMethodManager;
-
-import androidx.activity.OnBackPressedCallback;
-import androidx.annotation.NonNull;
-import androidx.slidingpanelayout.widget.SlidingPaneLayout;
-
-public class TwoPaneOnBackPressedCallback extends OnBackPressedCallback
-        implements SlidingPaneLayout.PanelSlideListener {
-    private final SlidingPaneLayout mSlidingPaneLayout;
-
-    public TwoPaneOnBackPressedCallback(@NonNull SlidingPaneLayout slidingPaneLayout) {
-        super(slidingPaneLayout.isSlideable() && slidingPaneLayout.isOpen());
-        mSlidingPaneLayout = slidingPaneLayout;
-        slidingPaneLayout.addPanelSlideListener(this);
-    }
-
-    @Override
-    public void handleOnBackPressed() {
-        mSlidingPaneLayout.close();
-    }
-
-    @Override
-    public void onPanelSlide(@NonNull View panel, float slideOffset) {
-    }
-
-    @Override
-    public void onPanelOpened(@NonNull View panel) {
-        setEnabled(true);
-    }
-
-    @Override
-    public void onPanelClosed(@NonNull View panel) {
-        closeKeyboard();
-        setEnabled(false);
-    }
-
-    private void closeKeyboard() {
-        InputMethodManager manager = (InputMethodManager) mSlidingPaneLayout.getContext()
-                .getSystemService(Context.INPUT_METHOD_SERVICE);
-        manager.hideSoftInputFromWindow(mSlidingPaneLayout.getRootView().getWindowToken(), 0);
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/ui/TwoPaneOnBackPressedCallback.kt b/src/android/app/src/main/java/org/citra/citra_emu/ui/TwoPaneOnBackPressedCallback.kt
new file mode 100644
index 000000000..5174e679a
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/ui/TwoPaneOnBackPressedCallback.kt
@@ -0,0 +1,40 @@
+// 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
+
+import android.content.Context
+import android.view.View
+import android.view.inputmethod.InputMethodManager
+import androidx.activity.OnBackPressedCallback
+import androidx.slidingpanelayout.widget.SlidingPaneLayout
+import androidx.slidingpanelayout.widget.SlidingPaneLayout.PanelSlideListener
+
+class TwoPaneOnBackPressedCallback(private val slidingPaneLayout: SlidingPaneLayout) :
+    OnBackPressedCallback(slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen),
+    PanelSlideListener {
+    init {
+        slidingPaneLayout.addPanelSlideListener(this)
+    }
+
+    override fun handleOnBackPressed() {
+        slidingPaneLayout.close()
+    }
+
+    override fun onPanelSlide(panel: View, slideOffset: Float) {}
+    override fun onPanelOpened(panel: View) {
+        isEnabled = true
+    }
+
+    override fun onPanelClosed(panel: View) {
+        closeKeyboard()
+        isEnabled = false
+    }
+
+    private fun closeKeyboard() {
+        val manager = slidingPaneLayout.context
+            .getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+        manager.hideSoftInputFromWindow(slidingPaneLayout.rootView.windowToken, 0)
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/Action1.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/Action1.java
deleted file mode 100644
index 886846ec5..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/utils/Action1.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package org.citra.citra_emu.utils;
-
-public interface Action1<T> {
-    void call(T t);
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/BiMap.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/BiMap.java
deleted file mode 100644
index dfbab1780..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/utils/BiMap.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.citra.citra_emu.utils;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public class BiMap<K, V> {
-    private Map<K, V> forward = new HashMap<K, V>();
-    private Map<V, K> backward = new HashMap<V, K>();
-
-    public synchronized void add(K key, V value) {
-        forward.put(key, value);
-        backward.put(value, key);
-    }
-
-    public synchronized V getForward(K key) {
-        return forward.get(key);
-    }
-
-    public synchronized K getBackward(V key) {
-        return backward.get(key);
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/BiMap.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/BiMap.kt
new file mode 100644
index 000000000..e444233ee
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/BiMap.kt
@@ -0,0 +1,22 @@
+// 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
+
+class BiMap<K, V> {
+    private val forward: MutableMap<K, V> = HashMap()
+    private val backward: MutableMap<V, K> = HashMap()
+
+    @Synchronized
+    fun add(key: K, value: V) {
+        forward[key] = value
+        backward[value] = key
+    }
+
+    @Synchronized
+    fun getForward(key: K): V? = forward[key]
+
+    @Synchronized
+    fun getBackward(key: V): K? = backward[key]
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/CiaInstallWorker.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/CiaInstallWorker.java
deleted file mode 100644
index 22f58ea4f..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/utils/CiaInstallWorker.java
+++ /dev/null
@@ -1,153 +0,0 @@
-package org.citra.citra_emu.utils;
-
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.core.app.NotificationCompat;
-import androidx.work.ForegroundInfo;
-import androidx.work.Worker;
-import androidx.work.WorkerParameters;
-
-import org.citra.citra_emu.NativeLibrary.InstallStatus;
-import org.citra.citra_emu.R;
-
-public class CiaInstallWorker extends Worker {
-    private final Context mContext = getApplicationContext();
-
-    private final NotificationManager mNotificationManager =
-            mContext.getSystemService(NotificationManager.class);
-
-    static final String GROUP_KEY_CIA_INSTALL_STATUS = "org.citra.citra_emu.CIA_INSTALL_STATUS";
-
-    private final NotificationCompat.Builder mInstallProgressBuilder = new NotificationCompat.Builder(
-            mContext, mContext.getString(R.string.cia_install_notification_channel_id))
-            .setContentTitle(mContext.getString(R.string.install_cia_title))
-            .setContentIntent(PendingIntent.getBroadcast(mContext, 0,
-                    new Intent("CitraDoNothing"), PendingIntent.FLAG_IMMUTABLE))
-            .setSmallIcon(R.drawable.ic_stat_notification_logo);
-
-    private final NotificationCompat.Builder mInstallStatusBuilder = new NotificationCompat.Builder(
-            mContext, mContext.getString(R.string.cia_install_notification_channel_id))
-            .setContentTitle(mContext.getString(R.string.install_cia_title))
-            .setSmallIcon(R.drawable.ic_stat_notification_logo)
-            .setGroup(GROUP_KEY_CIA_INSTALL_STATUS);
-
-    private final Notification mSummaryNotification =
-            new NotificationCompat.Builder(mContext, mContext.getString(R.string.cia_install_notification_channel_id))
-                    .setContentTitle(mContext.getString(R.string.install_cia_title))
-                    .setSmallIcon(R.drawable.ic_stat_notification_logo)
-                    .setGroup(GROUP_KEY_CIA_INSTALL_STATUS)
-                    .setGroupSummary(true)
-                    .build();
-
-    private static long mLastNotifiedTime = 0;
-
-    private static final int SUMMARY_NOTIFICATION_ID = 0xC1A0000;
-    private static final int PROGRESS_NOTIFICATION_ID = SUMMARY_NOTIFICATION_ID + 1;
-    private static int mStatusNotificationId = SUMMARY_NOTIFICATION_ID + 2;
-
-    public CiaInstallWorker(
-            @NonNull Context context,
-            @NonNull WorkerParameters params) {
-        super(context, params);
-    }
-
-    private void notifyInstallStatus(String filename, InstallStatus status) {
-        switch(status){
-            case Success:
-                mInstallStatusBuilder.setContentTitle(
-                        mContext.getString(R.string.cia_install_notification_success_title));
-                mInstallStatusBuilder.setContentText(
-                        mContext.getString(R.string.cia_install_success, filename));
-                break;
-            case ErrorAborted:
-                mInstallStatusBuilder.setContentTitle(
-                        mContext.getString(R.string.cia_install_notification_error_title));
-                mInstallStatusBuilder.setStyle(new NotificationCompat.BigTextStyle()
-                                .bigText(mContext.getString(
-                                         R.string.cia_install_error_aborted, filename)));
-                break;
-            case ErrorInvalid:
-                mInstallStatusBuilder.setContentTitle(
-                        mContext.getString(R.string.cia_install_notification_error_title));
-                mInstallStatusBuilder.setContentText(
-                        mContext.getString(R.string.cia_install_error_invalid, filename));
-                break;
-            case ErrorEncrypted:
-                mInstallStatusBuilder.setContentTitle(
-                        mContext.getString(R.string.cia_install_notification_error_title));
-                mInstallStatusBuilder.setStyle(new NotificationCompat.BigTextStyle()
-                        .bigText(mContext.getString(
-                                 R.string.cia_install_error_encrypted, filename)));
-                break;
-            case ErrorFailedToOpenFile:
-                // TODO:
-            case ErrorFileNotFound:
-                // shouldn't happen
-            default:
-                mInstallStatusBuilder.setContentTitle(
-                        mContext.getString(R.string.cia_install_notification_error_title));
-                mInstallStatusBuilder.setStyle(new NotificationCompat.BigTextStyle()
-                        .bigText(mContext.getString(R.string.cia_install_error_unknown, filename)));
-                break;
-        }
-        // Even if newer versions of Android don't show the group summary text that you design,
-        // you always need to manually set a summary to enable grouped notifications.
-        mNotificationManager.notify(SUMMARY_NOTIFICATION_ID, mSummaryNotification);
-        mNotificationManager.notify(mStatusNotificationId++, mInstallStatusBuilder.build());
-    }
-    @NonNull
-    @Override
-    public Result doWork() {
-        String[] selectedFiles = getInputData().getStringArray("CIA_FILES");
-        assert selectedFiles != null;
-        final CharSequence toastText = mContext.getResources().getQuantityString(R.plurals.cia_install_toast,
-                selectedFiles.length, selectedFiles.length);
-
-        getApplicationContext().getMainExecutor().execute(() -> Toast.makeText(mContext, toastText,
-                Toast.LENGTH_LONG).show());
-
-        // Issue the initial notification with zero progress
-        mInstallProgressBuilder.setOngoing(true);
-        setProgressCallback(100, 0);
-
-        int i = 0;
-        for (String file : selectedFiles) {
-            String filename = FileUtil.getFilename(Uri.parse(file));
-            mInstallProgressBuilder.setContentText(mContext.getString(
-                    R.string.cia_install_notification_installing, filename, ++i, selectedFiles.length));
-            InstallStatus res = installCIA(file);
-            notifyInstallStatus(filename, res);
-        }
-        mNotificationManager.cancel(PROGRESS_NOTIFICATION_ID);
-
-        return Result.success();
-    }
-    public void setProgressCallback(int max, int progress) {
-        long currentTime = System.currentTimeMillis();
-        // Android applies a rate limit when updating a notification.
-        // If you post updates to a single notification too frequently,
-        // such as many in less than one second, the system might drop updates.
-        // TODO: consider moving to C++ side
-        if (currentTime - mLastNotifiedTime < 500 /* ms */){
-            return;
-        }
-        mLastNotifiedTime = currentTime;
-        mInstallProgressBuilder.setProgress(max, progress, false);
-        mNotificationManager.notify(PROGRESS_NOTIFICATION_ID, mInstallProgressBuilder.build());
-    }
-
-    @NonNull
-    @Override
-    public ForegroundInfo getForegroundInfo() {
-        return new ForegroundInfo(PROGRESS_NOTIFICATION_ID, mInstallProgressBuilder.build());
-    }
-
-    private native InstallStatus installCIA(String path);
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/CiaInstallWorker.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/CiaInstallWorker.kt
new file mode 100644
index 000000000..dfb10d310
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/CiaInstallWorker.kt
@@ -0,0 +1,168 @@
+// 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
+
+import android.app.NotificationManager
+import android.content.Context
+import android.net.Uri
+import android.widget.Toast
+import androidx.core.app.NotificationCompat
+import androidx.work.ForegroundInfo
+import androidx.work.Worker
+import androidx.work.WorkerParameters
+import org.citra.citra_emu.NativeLibrary.InstallStatus
+import org.citra.citra_emu.R
+import org.citra.citra_emu.utils.FileUtil.getFilename
+
+class CiaInstallWorker(
+    val context: Context,
+    params: WorkerParameters
+) : Worker(context, params) {
+    private val GROUP_KEY_CIA_INSTALL_STATUS = "org.citra.citra_emu.CIA_INSTALL_STATUS"
+    private var lastNotifiedTime: Long = 0
+    private val SUMMARY_NOTIFICATION_ID = 0xC1A0000
+    private val PROGRESS_NOTIFICATION_ID = SUMMARY_NOTIFICATION_ID + 1
+    private var statusNotificationId = SUMMARY_NOTIFICATION_ID + 2
+
+    private val notificationManager = context.getSystemService(NotificationManager::class.java)
+    private val installProgressBuilder = NotificationCompat.Builder(
+        context,
+        context.getString(R.string.cia_install_notification_channel_id)
+    )
+        .setContentTitle(context.getString(R.string.install_cia_title))
+        .setSmallIcon(R.drawable.ic_stat_notification_logo)
+    private val installStatusBuilder = NotificationCompat.Builder(
+        context,
+        context.getString(R.string.cia_install_notification_channel_id)
+    )
+        .setContentTitle(context.getString(R.string.install_cia_title))
+        .setSmallIcon(R.drawable.ic_stat_notification_logo)
+        .setGroup(GROUP_KEY_CIA_INSTALL_STATUS)
+    private val summaryNotification = NotificationCompat.Builder(
+        context,
+        context.getString(R.string.cia_install_notification_channel_id)
+    )
+        .setContentTitle(context.getString(R.string.install_cia_title))
+        .setSmallIcon(R.drawable.ic_stat_notification_logo)
+        .setGroup(GROUP_KEY_CIA_INSTALL_STATUS)
+        .setGroupSummary(true)
+        .build()
+
+    private fun notifyInstallStatus(filename: String, status: InstallStatus) {
+        when (status) {
+            InstallStatus.Success -> {
+                installStatusBuilder.setContentTitle(
+                    context.getString(R.string.cia_install_notification_success_title)
+                )
+                installStatusBuilder.setContentText(
+                    context.getString(R.string.cia_install_success, filename)
+                )
+            }
+
+            InstallStatus.ErrorAborted -> {
+                installStatusBuilder.setContentTitle(
+                    context.getString(R.string.cia_install_notification_error_title)
+                )
+                installStatusBuilder.setStyle(
+                    NotificationCompat.BigTextStyle()
+                        .bigText(context.getString(R.string.cia_install_error_aborted, filename))
+                )
+            }
+
+            InstallStatus.ErrorInvalid -> {
+                installStatusBuilder.setContentTitle(
+                    context.getString(R.string.cia_install_notification_error_title)
+                )
+                installStatusBuilder.setContentText(
+                    context.getString(R.string.cia_install_error_invalid, filename)
+                )
+            }
+
+            InstallStatus.ErrorEncrypted -> {
+                installStatusBuilder.setContentTitle(
+                    context.getString(R.string.cia_install_notification_error_title)
+                )
+                installStatusBuilder.setStyle(
+                    NotificationCompat.BigTextStyle()
+                        .bigText(context.getString(R.string.cia_install_error_encrypted, filename))
+                )
+            }
+
+            InstallStatus.ErrorFailedToOpenFile, InstallStatus.ErrorFileNotFound -> {
+                installStatusBuilder.setContentTitle(
+                    context.getString(R.string.cia_install_notification_error_title)
+                )
+                installStatusBuilder.setStyle(
+                    NotificationCompat.BigTextStyle()
+                        .bigText(context.getString(R.string.cia_install_error_unknown, filename))
+                )
+            }
+
+            else -> {
+                installStatusBuilder.setContentTitle(
+                    context.getString(R.string.cia_install_notification_error_title)
+                )
+                installStatusBuilder.setStyle(
+                    NotificationCompat.BigTextStyle()
+                        .bigText(context.getString(R.string.cia_install_error_unknown, filename))
+                )
+            }
+        }
+
+        // Even if newer versions of Android don't show the group summary text that you design,
+        // you always need to manually set a summary to enable grouped notifications.
+        notificationManager.notify(SUMMARY_NOTIFICATION_ID, summaryNotification)
+        notificationManager.notify(statusNotificationId++, installStatusBuilder.build())
+    }
+
+    override fun doWork(): Result {
+        val selectedFiles = inputData.getStringArray("CIA_FILES")!!
+        val toastText: CharSequence = context.resources.getQuantityString(
+            R.plurals.cia_install_toast,
+            selectedFiles.size, selectedFiles.size
+        )
+        context.mainExecutor.execute {
+            Toast.makeText(context, toastText, Toast.LENGTH_LONG).show()
+        }
+
+        // Issue the initial notification with zero progress
+        installProgressBuilder.setOngoing(true)
+        setProgressCallback(100, 0)
+        selectedFiles.forEachIndexed { i, file ->
+            val filename = getFilename(Uri.parse(file))
+            installProgressBuilder.setContentText(
+                context.getString(
+                    R.string.cia_install_notification_installing,
+                    filename,
+                    i,
+                    selectedFiles.size
+                )
+            )
+            val res = installCIA(file)
+            notifyInstallStatus(filename, res)
+        }
+        notificationManager.cancel(PROGRESS_NOTIFICATION_ID)
+        return Result.success()
+    }
+
+    fun setProgressCallback(max: Int, progress: Int) {
+        val currentTime = System.currentTimeMillis()
+        // Android applies a rate limit when updating a notification.
+        // If you post updates to a single notification too frequently,
+        // such as many in less than one second, the system might drop updates.
+        // TODO: consider moving to C++ side
+        if (currentTime - lastNotifiedTime < 500 /* ms */) {
+            return
+        }
+        lastNotifiedTime = currentTime
+        installProgressBuilder.setProgress(max, progress, false)
+        notificationManager.notify(PROGRESS_NOTIFICATION_ID, installProgressBuilder.build())
+    }
+
+    override fun getForegroundInfo(): ForegroundInfo =
+        ForegroundInfo(PROGRESS_NOTIFICATION_ID, installProgressBuilder.build())
+
+    private external fun installCIA(path: String): InstallStatus
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java
deleted file mode 100644
index cbdc0742c..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package org.citra.citra_emu.utils;
-
-import android.content.ClipData;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-
-import androidx.annotation.Nullable;
-import androidx.documentfile.provider.DocumentFile;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public final class FileBrowserHelper {
-
-    @Nullable
-    public static String[] getSelectedFiles(Intent result, Context context, List<String> extension) {
-        ClipData clipData = result.getClipData();
-        List<DocumentFile> files = new ArrayList<>();
-        if (clipData == null) {
-            files.add(DocumentFile.fromSingleUri(context, result.getData()));
-        } else {
-            for (int i = 0; i < clipData.getItemCount(); i++) {
-                ClipData.Item item = clipData.getItemAt(i);
-                Uri uri = item.getUri();
-                files.add(DocumentFile.fromSingleUri(context, uri));
-            }
-        }
-        if (!files.isEmpty()) {
-            List<String> filePaths = new ArrayList<>();
-            for (int i = 0; i < files.size(); i++) {
-                DocumentFile file = files.get(i);
-                String filename = file.getName();
-                int extensionStart = filename.lastIndexOf('.');
-                if (extensionStart > 0) {
-                    String fileExtension = filename.substring(extensionStart + 1);
-                    if (extension.contains(fileExtension)) {
-                        filePaths.add(file.getUri().toString());
-                    }
-                }
-            }
-            if (filePaths.isEmpty()) {
-                return null;
-            }
-            return filePaths.toArray(new String[0]);
-        }
-
-        return null;
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.kt
new file mode 100644
index 000000000..423507173
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/FileBrowserHelper.kt
@@ -0,0 +1,44 @@
+// 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
+
+import android.content.Context
+import android.content.Intent
+import androidx.documentfile.provider.DocumentFile
+
+object FileBrowserHelper {
+    fun getSelectedFiles(
+        result: Intent,
+        context: Context,
+        extension: List<String?>
+    ): Array<String>? {
+        val clipData = result.clipData
+        val files: MutableList<DocumentFile?> = ArrayList()
+        if (clipData == null) {
+            files.add(DocumentFile.fromSingleUri(context, result.data!!))
+        } else {
+            for (i in 0 until clipData.itemCount) {
+                val item = clipData.getItemAt(i)
+                files.add(DocumentFile.fromSingleUri(context, item.uri))
+            }
+        }
+        if (files.isNotEmpty()) {
+            val filePaths: MutableList<String> = ArrayList()
+            for (i in files.indices) {
+                val file = files[i]
+                val filename = file?.name
+                val extensionStart = filename?.lastIndexOf('.') ?: 0
+                if (extensionStart > 0) {
+                    val fileExtension = filename?.substring(extensionStart + 1)
+                    if (extension.contains(fileExtension)) {
+                        filePaths.add(file?.uri.toString())
+                    }
+                }
+            }
+            return if (filePaths.isEmpty()) null else filePaths.toTypedArray<String>()
+        }
+        return null
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/InsetsHelper.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/InsetsHelper.java
deleted file mode 100644
index 55f8a463e..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/utils/InsetsHelper.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package org.citra.citra_emu.utils;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.view.ViewGroup;
-
-import androidx.core.graphics.Insets;
-
-import com.google.android.material.appbar.AppBarLayout;
-
-public class InsetsHelper {
-    public static final int THREE_BUTTON_NAVIGATION = 0;
-    public static final int TWO_BUTTON_NAVIGATION = 1;
-    public static final int GESTURE_NAVIGATION = 2;
-
-    public static void insetAppBar(Insets insets, AppBarLayout appBarLayout)
-    {
-        ViewGroup.MarginLayoutParams mlpAppBar =
-                (ViewGroup.MarginLayoutParams) appBarLayout.getLayoutParams();
-        mlpAppBar.leftMargin = insets.left;
-        mlpAppBar.rightMargin = insets.right;
-        appBarLayout.setLayoutParams(mlpAppBar);
-    }
-
-    public static int getSystemGestureType(Context context) {
-        Resources resources = context.getResources();
-        int resourceId = resources.getIdentifier("config_navBarInteractionMode", "integer", "android");
-        if (resourceId != 0) {
-            return resources.getInteger(resourceId);
-        }
-        return 0;
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/InsetsHelper.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/InsetsHelper.kt
new file mode 100644
index 000000000..96ea234e6
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/InsetsHelper.kt
@@ -0,0 +1,25 @@
+// 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
+
+import android.annotation.SuppressLint
+import android.content.Context
+
+object InsetsHelper {
+    const val THREE_BUTTON_NAVIGATION = 0
+    const val TWO_BUTTON_NAVIGATION = 1
+    const val GESTURE_NAVIGATION = 2
+
+    @SuppressLint("DiscouragedApi")
+    fun getSystemGestureType(context: Context): Int {
+        val resources = context.resources
+        val resourceId = resources.getIdentifier(
+            "config_navBarInteractionMode",
+            "integer",
+            "android"
+        )
+        return if (resourceId != 0) resources.getInteger(resourceId) else 0
+    }
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/Log.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/Log.java
deleted file mode 100644
index 096332422..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/utils/Log.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package org.citra.citra_emu.utils;
-
-import org.citra.citra_emu.BuildConfig;
-
-/**
- * Contains methods that call through to {@link android.util.Log}, but
- * with the same TAG automatically provided. Also no-ops VERBOSE and DEBUG log
- * levels in release builds.
- */
-public final class Log {
-    // Tracks whether we should share the old log or the current log
-    public static boolean gameLaunched = false;
-
-    private static final String TAG = "Citra Frontend";
-
-    private Log() {
-    }
-
-    public static void verbose(String message) {
-        if (BuildConfig.DEBUG) {
-            android.util.Log.v(TAG, message);
-        }
-    }
-
-    public static void debug(String message) {
-        if (BuildConfig.DEBUG) {
-            android.util.Log.d(TAG, message);
-        }
-    }
-
-    public static void info(String message) {
-        android.util.Log.i(TAG, message);
-    }
-
-    public static void warning(String message) {
-        android.util.Log.w(TAG, message);
-    }
-
-    public static void error(String message) {
-        android.util.Log.e(TAG, message);
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/Log.kt b/src/android/app/src/main/java/org/citra/citra_emu/utils/Log.kt
new file mode 100644
index 000000000..26c41bc98
--- /dev/null
+++ b/src/android/app/src/main/java/org/citra/citra_emu/utils/Log.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.utils
+
+import android.util.Log
+import org.citra.citra_emu.BuildConfig
+
+/**
+ * Contains methods that call through to [android.util.Log], but
+ * with the same TAG automatically provided. Also no-ops VERBOSE and DEBUG log
+ * levels in release builds.
+ */
+object Log {
+    // Tracks whether we should share the old log or the current log
+    var gameLaunched = false
+    private const val TAG = "Citra Frontend"
+
+    fun verbose(message: String?) {
+        if (BuildConfig.DEBUG) {
+            Log.v(TAG, message!!)
+        }
+    }
+
+    fun debug(message: String?) {
+        if (BuildConfig.DEBUG) {
+            Log.d(TAG, message!!)
+        }
+    }
+
+    fun info(message: String?) = Log.i(TAG, message!!)
+
+    fun warning(message: String?) = Log.w(TAG, message!!)
+
+    fun error(message: String?) = Log.e(TAG, message!!)
+}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoUtils.java b/src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoUtils.java
deleted file mode 100644
index 74e282beb..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/utils/PicassoUtils.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package org.citra.citra_emu.utils;
-
-import android.graphics.Bitmap;
-import android.net.Uri;
-
-import com.squareup.picasso.Picasso;
-
-import java.io.IOException;
-
-import androidx.annotation.Nullable;
-
-public class PicassoUtils {
-    // Blocking call. Load image from file and crop/resize it to fit in width x height.
-    @Nullable
-    public static Bitmap LoadBitmapFromFile(String uri, int width, int height) {
-        try {
-            return Picasso.get()
-                    .load(Uri.parse(uri))
-                    .config(Bitmap.Config.ARGB_8888)
-                    .centerCrop()
-                    .resize(width, height)
-                    .get();
-        } catch (IOException e) {
-            return null;
-        }
-    }
-}
diff --git a/src/android/app/src/main/java/org/citra/citra_emu/viewholders/GameViewHolder.java b/src/android/app/src/main/java/org/citra/citra_emu/viewholders/GameViewHolder.java
deleted file mode 100644
index 50dbcbe18..000000000
--- a/src/android/app/src/main/java/org/citra/citra_emu/viewholders/GameViewHolder.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package org.citra.citra_emu.viewholders;
-
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.recyclerview.widget.RecyclerView;
-
-import org.citra.citra_emu.R;
-
-/**
- * A simple class that stores references to views so that the GameAdapter doesn't need to
- * keep calling findViewById(), which is expensive.
- */
-public class GameViewHolder extends RecyclerView.ViewHolder {
-    private View itemView;
-    public ImageView imageIcon;
-    public TextView textGameTitle;
-    public TextView textCompany;
-    public TextView textFileName;
-
-    public String gameId;
-
-    // TODO Not need any of this stuff. Currently only the properties dialog needs it.
-    public String path;
-    public String title;
-    public String description;
-    public String regions;
-    public String company;
-
-    public GameViewHolder(View itemView) {
-        super(itemView);
-
-        this.itemView = itemView;
-        itemView.setTag(this);
-
-        imageIcon = itemView.findViewById(R.id.image_game_screen);
-        textGameTitle = itemView.findViewById(R.id.text_game_title);
-        textCompany = itemView.findViewById(R.id.text_company);
-        textFileName = itemView.findViewById(R.id.text_filename);
-    }
-
-    public View getItemView() {
-        return itemView;
-    }
-}
diff --git a/src/android/app/src/main/jni/applets/mii_selector.cpp b/src/android/app/src/main/jni/applets/mii_selector.cpp
index 2bb48db9a..3a9c66ef8 100644
--- a/src/android/app/src/main/jni/applets/mii_selector.cpp
+++ b/src/android/app/src/main/jni/applets/mii_selector.cpp
@@ -23,14 +23,13 @@ void AndroidMiiSelector::Setup(const Frontend::MiiSelectorConfig& config) {
     // Create the Java MiiSelectorConfig object
     jobject java_config = env->AllocObject(s_mii_selector_config_class);
     env->SetBooleanField(java_config,
-                         env->GetFieldID(s_mii_selector_config_class, "enable_cancel_button", "Z"),
+                         env->GetFieldID(s_mii_selector_config_class, "enableCancelButton", "Z"),
                          static_cast<jboolean>(config.enable_cancel_button));
     env->SetObjectField(java_config,
                         env->GetFieldID(s_mii_selector_config_class, "title", "Ljava/lang/String;"),
                         ToJString(env, config.title));
     env->SetLongField(
-        java_config,
-        env->GetFieldID(s_mii_selector_config_class, "initially_selected_mii_index", "J"),
+        java_config, env->GetFieldID(s_mii_selector_config_class, "initiallySelectedMiiIndex", "J"),
         static_cast<jlong>(config.initially_selected_mii_index));
 
     // List mii names
@@ -44,14 +43,14 @@ void AndroidMiiSelector::Setup(const Frontend::MiiSelectorConfig& config) {
     }
     env->SetObjectField(
         java_config,
-        env->GetFieldID(s_mii_selector_config_class, "mii_names", "[Ljava/lang/String;"), array);
+        env->GetFieldID(s_mii_selector_config_class, "miiNames", "[Ljava/lang/String;"), array);
 
     // Invoke backend Execute method
     jobject data =
         env->CallStaticObjectMethod(s_mii_selector_class, s_mii_selector_execute, java_config);
 
     const u32 return_code = static_cast<u32>(
-        env->GetLongField(data, env->GetFieldID(s_mii_selector_data_class, "return_code", "J")));
+        env->GetLongField(data, env->GetFieldID(s_mii_selector_data_class, "returnCode", "J")));
     if (return_code == 1) {
         Finalize(return_code, Mii::MiiData{});
         return;
diff --git a/src/android/app/src/main/jni/applets/swkbd.cpp b/src/android/app/src/main/jni/applets/swkbd.cpp
index e373c9e27..0a8337672 100644
--- a/src/android/app/src/main/jni/applets/swkbd.cpp
+++ b/src/android/app/src/main/jni/applets/swkbd.cpp
@@ -23,14 +23,14 @@ namespace SoftwareKeyboard {
 static jobject ToJavaKeyboardConfig(const Frontend::KeyboardConfig& config) {
     JNIEnv* env = IDCache::GetEnvForThread();
     jobject object = env->AllocObject(s_keyboard_config_class);
-    env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "button_config", "I"),
+    env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "buttonConfig", "I"),
                      static_cast<jint>(config.button_config));
-    env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "max_text_length", "I"),
+    env->SetIntField(object, env->GetFieldID(s_keyboard_config_class, "maxTextLength", "I"),
                      static_cast<jint>(config.max_text_length));
-    env->SetBooleanField(object, env->GetFieldID(s_keyboard_config_class, "multiline_mode", "Z"),
+    env->SetBooleanField(object, env->GetFieldID(s_keyboard_config_class, "multilineMode", "Z"),
                          static_cast<jboolean>(config.multiline_mode));
     env->SetObjectField(object,
-                        env->GetFieldID(s_keyboard_config_class, "hint_text", "Ljava/lang/String;"),
+                        env->GetFieldID(s_keyboard_config_class, "hintText", "Ljava/lang/String;"),
                         ToJString(env, config.hint_text));
 
     const jclass string_class = reinterpret_cast<jclass>(env->FindClass("java/lang/String"));
@@ -42,7 +42,7 @@ static jobject ToJavaKeyboardConfig(const Frontend::KeyboardConfig& config) {
                                    ToJString(env, config.button_text[i]));
     }
     env->SetObjectField(
-        object, env->GetFieldID(s_keyboard_config_class, "button_text", "[Ljava/lang/String;"),
+        object, env->GetFieldID(s_keyboard_config_class, "buttonText", "[Ljava/lang/String;"),
         array);
 
     return object;
diff --git a/src/android/app/src/main/res/drawable/button_home.xml b/src/android/app/src/main/res/drawable/button_home.xml
new file mode 100644
index 000000000..c6797da12
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/button_home.xml
@@ -0,0 +1,16 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="100dp"
+    android:height="100dp"
+    android:viewportWidth="99.27"
+    android:viewportHeight="99.27">
+    <path
+        android:fillAlpha="0.5"
+        android:fillColor="#eaeaea"
+        android:pathData="M49.64,49.64m-49.64,0a49.64,49.64 0,1 1,99.28 0a49.64,49.64 0,1 1,-99.28 0"
+        android:strokeAlpha="0.5" />
+    <path
+        android:fillAlpha="0.75"
+        android:fillColor="#FF000000"
+        android:pathData="m75.99,45.27l-25.31,-23.18c-0.58,-0.56 -1.5,-0.56 -2.08,0l-25.31,23.18c-0.95,0.94 -0.3,2.56 1.04,2.56h4.3c0.53,0 0.96,0.43 0.96,0.96v21.33c0,0.82 0.67,1.49 1.49,1.49h37.14c0.82,0 1.49,-0.67 1.49,-1.49v-21.33c0,-0.53 0.43,-0.96 0.96,-0.96h4.3c1.34,0 1.99,-1.62 1.04,-2.56ZM57.81,60.01c0,0.66 -0.53,1.19 -1.19,1.19h-13.96c-0.66,0 -1.19,-0.53 -1.19,-1.19v-10.99c0,-0.66 0.53,-1.19 1.19,-1.19h13.96c0.66,0 1.19,0.53 1.19,1.19v10.99Z"
+        android:strokeAlpha="0.75" />
+</vector>
diff --git a/src/android/app/src/main/res/drawable/button_home_pressed.xml b/src/android/app/src/main/res/drawable/button_home_pressed.xml
new file mode 100644
index 000000000..45878eae9
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/button_home_pressed.xml
@@ -0,0 +1,16 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="100dp"
+    android:height="100dp"
+    android:viewportWidth="99.27"
+    android:viewportHeight="99.27">
+    <path
+        android:fillAlpha="0.5"
+        android:fillColor="#151515"
+        android:pathData="M49.64,49.64m-49.64,0a49.64,49.64 0,1 1,99.28 0a49.64,49.64 0,1 1,-99.28 0"
+        android:strokeAlpha="0.5" />
+    <path
+        android:fillAlpha="0.75"
+        android:fillColor="#fff"
+        android:pathData="m75.99,45.27l-25.31,-23.18c-0.58,-0.56 -1.5,-0.56 -2.08,0l-25.31,23.18c-0.95,0.94 -0.3,2.56 1.04,2.56h4.3c0.53,0 0.96,0.43 0.96,0.96v21.33c0,0.82 0.67,1.49 1.49,1.49h37.14c0.82,0 1.49,-0.67 1.49,-1.49v-21.33c0,-0.53 0.43,-0.96 0.96,-0.96h4.3c1.34,0 1.99,-1.62 1.04,-2.56ZM57.81,60.01c0,0.66 -0.53,1.19 -1.19,1.19h-13.96c-0.66,0 -1.19,-0.53 -1.19,-1.19v-10.99c0,-0.66 0.53,-1.19 1.19,-1.19h13.96c0.66,0 1.19,0.53 1.19,1.19v10.99Z"
+        android:strokeAlpha="0.75" />
+</vector>
diff --git a/src/android/app/src/main/res/layout-ldrtl/list_item_cheat.xml b/src/android/app/src/main/res/layout-ldrtl/list_item_cheat.xml
index ec9942cc5..1ea85d56a 100644
--- a/src/android/app/src/main/res/layout-ldrtl/list_item_cheat.xml
+++ b/src/android/app/src/main/res/layout-ldrtl/list_item_cheat.xml
@@ -1,37 +1,41 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout 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/root"
+    android:id="@+id/cheat_container"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
+    android:background="?attr/selectableItemBackground"
+    android:paddingVertical="16dp"
+    android:paddingHorizontal="20dp"
     android:focusable="true"
-    android:nextFocusLeft="@id/checkbox">
+    android:nextFocusLeft="@id/cheat_switch">
 
     <TextView
         android:id="@+id/text_name"
+        style="@style/TextAppearance.AppCompat.Headline"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
-        android:textSize="16sp"
         android:layout_margin="@dimen/spacing_large"
-        style="@style/TextAppearance.AppCompat.Headline"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toStartOf="@id/checkbox"
-        app:layout_constraintTop_toTopOf="parent"
+        android:textSize="16sp"
         app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/cheat_switch"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
         tools:text="Max Lives after losing 1" />
 
-    <CheckBox
-        android:id="@+id/checkbox"
-        android:layout_width="48dp"
-        android:layout_height="64dp"
+    <com.google.android.material.materialswitch.MaterialSwitch
+        android:id="@+id/cheat_switch"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
         android:focusable="true"
         android:gravity="center"
-        android:nextFocusRight="@id/root"
+        android:nextFocusRight="@id/cheat_container"
+        android:paddingEnd="8dp"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toEndOf="@id/text_name"
-        app:layout_constraintTop_toTopOf="parent" />
+        app:layout_constraintTop_toTopOf="parent"
+        tools:ignore="RtlSymmetry" />
 
 </androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/activity_cheats.xml b/src/android/app/src/main/res/layout/activity_cheats.xml
index 0d2a92f68..2155f7c58 100644
--- a/src/android/app/src/main/res/layout/activity_cheats.xml
+++ b/src/android/app/src/main/res/layout/activity_cheats.xml
@@ -1,60 +1,26 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
-    android:background="?attr/colorSurface">
+    android:layout_height="match_parent">
 
-    <androidx.coordinatorlayout.widget.CoordinatorLayout
-        android:id="@+id/coordinator_cheats"
+    <androidx.fragment.app.FragmentContainerView
+        android:id="@+id/fragment_container"
+        android:name="androidx.navigation.fragment.NavHostFragment"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent">
+        android:layout_height="match_parent"
+        android:keepScreenOn="true"
+        app:defaultNavHost="true" />
 
-        <com.google.android.material.appbar.AppBarLayout
-            android:id="@+id/appbar_cheats"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:fitsSystemWindows="true">
-
-            <com.google.android.material.appbar.MaterialToolbar
-                android:id="@+id/toolbar_cheats"
-                android:layout_width="match_parent"
-                android:layout_height="?attr/actionBarSize" />
-
-        </com.google.android.material.appbar.AppBarLayout>
-
-    </androidx.coordinatorlayout.widget.CoordinatorLayout>
-
-    <androidx.slidingpanelayout.widget.SlidingPaneLayout
-        android:id="@+id/sliding_pane_layout"
-        android:layout_width="match_parent"
-        android:layout_height="0dp"
+    <View
+        android:id="@+id/navigation_bar_shade"
+        android:layout_width="0dp"
+        android:layout_height="1px"
+        android:background="@android:color/transparent"
+        android:clickable="false"
+        android:focusable="false"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/coordinator_cheats">
-
-        <androidx.fragment.app.FragmentContainerView
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:id="@+id/cheat_list_container"
-            android:name="org.citra.citra_emu.features.cheats.ui.CheatListFragment"
-            tools:layout="@layout/fragment_cheat_list" />
-
-        <androidx.fragment.app.FragmentContainerView
-            android:layout_width="match_parent"
-            android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:id="@+id/cheat_details_container"
-            android:name="org.citra.citra_emu.features.cheats.ui.CheatDetailsFragment"
-            tools:layout="@layout/fragment_cheat_details" />
-
-    </androidx.slidingpanelayout.widget.SlidingPaneLayout>
+        app:layout_constraintStart_toStartOf="parent" />
 
 </androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/card_home_option.xml b/src/android/app/src/main/res/layout/card_home_option.xml
index 6457c02c8..d18d17d9b 100644
--- a/src/android/app/src/main/res/layout/card_home_option.xml
+++ b/src/android/app/src/main/res/layout/card_home_option.xml
@@ -6,8 +6,8 @@
     android:id="@+id/option_card"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:layout_marginVertical="12dp"
-    android:layout_marginHorizontal="16dp"
+    android:layout_marginBottom="24dp"
+    android:layout_marginHorizontal="12dp"
     android:background="?attr/selectableItemBackground"
     android:backgroundTint="?attr/colorSurfaceVariant"
     android:clickable="true"
@@ -16,7 +16,8 @@
     <LinearLayout
         android:id="@+id/option_layout"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content">
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical">
 
         <ImageView
             android:id="@+id/option_icon"
@@ -44,7 +45,7 @@
                 tools:text="@string/about" />
 
             <com.google.android.material.textview.MaterialTextView
-                style="@style/TextAppearance.Material3.LabelMedium"
+                style="@style/TextAppearance.Material3.BodySmall"
                 android:id="@+id/option_description"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
@@ -67,7 +68,8 @@
                 android:requiresFadingEdge="horizontal"
                 android:layout_marginTop="5dp"
                 android:visibility="gone"
-                tools:text="@string/about_description" />
+                tools:visibility="visible"
+                tools:text="/tree/primary:Games" />
 
         </LinearLayout>
 
diff --git a/src/android/app/src/main/res/layout/fragment_about.xml b/src/android/app/src/main/res/layout/fragment_about.xml
index 456084449..7bd26f246 100644
--- a/src/android/app/src/main/res/layout/fragment_about.xml
+++ b/src/android/app/src/main/res/layout/fragment_about.xml
@@ -38,8 +38,8 @@
 
             <ImageView
                 android:id="@+id/image_logo"
-                android:layout_width="175dp"
-                android:layout_height="175dp"
+                android:layout_width="104dp"
+                android:layout_height="104dp"
                 android:layout_marginTop="20dp"
                 android:layout_gravity="center_horizontal"
                 android:src="@drawable/ic_citra_full" />
diff --git a/src/android/app/src/main/res/layout/fragment_cheat_details.xml b/src/android/app/src/main/res/layout/fragment_cheat_details.xml
index 25b1a268a..dd16dd9be 100644
--- a/src/android/app/src/main/res/layout/fragment_cheat_details.xml
+++ b/src/android/app/src/main/res/layout/fragment_cheat_details.xml
@@ -1,163 +1,177 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout 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/root"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
-    <ScrollView
-        android:id="@+id/scroll_view"
+    <androidx.coordinatorlayout.widget.CoordinatorLayout
         android:layout_width="match_parent"
         android:layout_height="0dp"
-        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintBottom_toTopOf="@id/button_layout"
         app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toTopOf="@id/barrier">
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
 
-        <androidx.constraintlayout.widget.ConstraintLayout
+        <com.google.android.material.appbar.AppBarLayout
+            android:id="@+id/appbar_cheat_details"
             android:layout_width="match_parent"
-            android:layout_height="wrap_content">
+            android:layout_height="wrap_content"
+            android:fitsSystemWindows="true">
 
-            <TextView
-                android:id="@+id/label_name"
+            <com.google.android.material.appbar.MaterialToolbar
+                android:id="@+id/toolbar_cheat_details"
+                android:layout_width="match_parent"
+                android:layout_height="?attr/actionBarSize"
+                app:navigationIcon="@drawable/ic_back"
+                app:title="@string/cheats" />
+
+        </com.google.android.material.appbar.AppBarLayout>
+
+        <androidx.core.widget.NestedScrollView
+            android:id="@+id/scroll_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+            <LinearLayout
+                android:id="@+id/input_layout"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                style="@style/TextAppearance.MaterialComponents.Headline5"
-                android:textSize="18sp"
-                android:text="@string/cheats_name"
+                android:orientation="vertical">
+
+                <com.google.android.material.textfield.TextInputLayout
+                    android:id="@+id/edit_name"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginHorizontal="@dimen/spacing_large"
+                    android:layout_marginVertical="@dimen/spacing_small"
+                    android:hint="@string/cheats_name"
+                    android:paddingTop="@dimen/spacing_medlarge"
+                    app:errorEnabled="true">
+
+                    <com.google.android.material.textfield.TextInputEditText
+                        android:id="@+id/edit_name_input"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:importantForAutofill="no"
+                        android:inputType="text"
+                        android:minHeight="48dp"
+                        android:textAlignment="viewStart"
+                        android:nextFocusDown="@id/edit_notes_input"
+                        tools:text="Hyrule Field Speed Hack" />
+
+                </com.google.android.material.textfield.TextInputLayout>
+
+                <com.google.android.material.textfield.TextInputLayout
+                    android:id="@+id/edit_notes"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginHorizontal="@dimen/spacing_large"
+                    android:layout_marginBottom="24dp"
+                    android:hint="@string/cheats_notes">
+
+                    <com.google.android.material.textfield.TextInputEditText
+                        android:id="@+id/edit_notes_input"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:importantForAutofill="no"
+                        android:inputType="textMultiLine"
+                        android:minHeight="48dp"
+                        android:textAlignment="viewStart"
+                        android:nextFocusDown="@id/edit_code_input" />
+
+                </com.google.android.material.textfield.TextInputLayout>
+
+                <com.google.android.material.textfield.TextInputLayout
+                    android:id="@+id/edit_code"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginHorizontal="@dimen/spacing_large"
+                    android:layout_marginVertical="@dimen/spacing_small"
+                    android:hint="@string/cheats_code"
+                    app:errorEnabled="true">
+
+                    <com.google.android.material.textfield.TextInputEditText
+                        android:id="@+id/edit_code_input"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:gravity="start"
+                        android:importantForAutofill="no"
+                        android:inputType="textMultiLine"
+                        android:minHeight="108sp"
+                        android:textAlignment="viewStart"
+                        android:typeface="monospace"
+                        android:nextFocusDown="@id/button_cancel"
+                        tools:text="0x8003d63c:dword:0x60000000\n0x8003d658:dword:0x60000000" />
+
+                </com.google.android.material.textfield.TextInputLayout>
+
+            </LinearLayout>
+
+        </androidx.core.widget.NestedScrollView>
+
+    </androidx.coordinatorlayout.widget.CoordinatorLayout>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/button_layout"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="@android:color/transparent"
+        app:layout_constraintBottom_toBottomOf="parent">
+
+        <com.google.android.material.divider.MaterialDivider
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <LinearLayout
+            android:id="@+id/button_container"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent">
+
+            <Button
+                android:id="@+id/button_delete"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
                 android:layout_margin="@dimen/spacing_large"
-                android:labelFor="@id/edit_name"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintTop_toTopOf="parent"
-                app:layout_constraintBottom_toTopOf="@id/edit_name" />
+                android:layout_weight="1"
+                android:nextFocusUp="@id/appbar_cheat_details"
+                android:text="@string/cheats_delete" />
 
-            <EditText
-                android:id="@+id/edit_name"
-                android:layout_width="match_parent"
+            <Button
+                android:id="@+id/button_edit"
+                android:layout_width="0dp"
                 android:layout_height="wrap_content"
-                android:minHeight="48dp"
-                android:layout_marginHorizontal="@dimen/spacing_large"
-                android:importantForAutofill="no"
-                android:inputType="text"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintTop_toBottomOf="@id/label_name"
-                app:layout_constraintBottom_toTopOf="@id/label_notes"
-                tools:text="Max Lives after losing 1" />
-
-            <TextView
-                android:id="@+id/label_notes"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                style="@style/TextAppearance.MaterialComponents.Headline5"
-                android:textSize="18sp"
-                android:text="@string/cheats_notes"
                 android:layout_margin="@dimen/spacing_large"
-                android:labelFor="@id/edit_notes"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintTop_toBottomOf="@id/edit_name"
-                app:layout_constraintBottom_toTopOf="@id/edit_notes" />
+                android:layout_weight="1"
+                android:nextFocusUp="@id/appbar_cheat_details"
+                android:text="@string/cheats_edit" />
 
-            <EditText
-                android:id="@+id/edit_notes"
-                android:layout_width="match_parent"
+            <Button
+                android:id="@+id/button_cancel"
+                android:layout_width="0dp"
                 android:layout_height="wrap_content"
-                android:minHeight="48dp"
-                android:layout_marginHorizontal="@dimen/spacing_large"
-                android:importantForAutofill="no"
-                android:inputType="textMultiLine"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintTop_toBottomOf="@id/label_notes"
-                app:layout_constraintBottom_toTopOf="@id/label_code" />
-
-            <TextView
-                android:id="@+id/label_code"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                style="@style/TextAppearance.MaterialComponents.Headline5"
-                android:textSize="18sp"
-                android:text="@string/cheats_code"
                 android:layout_margin="@dimen/spacing_large"
-                android:labelFor="@id/edit_code"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintTop_toBottomOf="@id/edit_notes"
-                app:layout_constraintBottom_toTopOf="@id/edit_code" />
+                android:layout_weight="1"
+                android:nextFocusUp="@id/edit_code_input"
+                android:text="@android:string/cancel" />
 
-            <EditText
-                android:id="@+id/edit_code"
-                android:layout_width="match_parent"
+            <Button
+                android:id="@+id/button_ok"
+                android:layout_width="0dp"
                 android:layout_height="wrap_content"
-                android:minHeight="108sp"
-                android:layout_marginHorizontal="@dimen/spacing_large"
-                android:importantForAutofill="no"
-                android:inputType="textMultiLine"
-                android:typeface="monospace"
-                android:gravity="start"
-                app:layout_constraintStart_toStartOf="parent"
-                app:layout_constraintEnd_toEndOf="parent"
-                app:layout_constraintTop_toBottomOf="@id/label_code"
-                app:layout_constraintBottom_toBottomOf="parent"
-                tools:text="D3000000 00000000\n00138C78 E1C023BE" />
+                android:layout_margin="@dimen/spacing_large"
+                android:layout_weight="1"
+                android:nextFocusUp="@id/edit_code_input"
+                android:text="@android:string/ok" />
 
-        </androidx.constraintlayout.widget.ConstraintLayout>
+        </LinearLayout>
 
-    </ScrollView>
-
-    <androidx.constraintlayout.widget.Barrier
-        android:id="@+id/barrier"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        app:barrierDirection="top"
-        app:constraint_referenced_ids="button_delete,button_edit,button_cancel,button_ok" />
-
-    <Button
-        android:id="@+id/button_delete"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_margin="@dimen/spacing_large"
-        android:text="@string/cheats_delete"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toStartOf="@id/button_edit"
-        app:layout_constraintTop_toBottomOf="@id/barrier"
-        app:layout_constraintBottom_toBottomOf="parent" />
-
-    <Button
-        android:id="@+id/button_edit"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_margin="@dimen/spacing_large"
-        android:text="@string/cheats_edit"
-        app:layout_constraintStart_toEndOf="@id/button_delete"
-        app:layout_constraintEnd_toStartOf="@id/button_cancel"
-        app:layout_constraintTop_toBottomOf="@id/barrier"
-        app:layout_constraintBottom_toBottomOf="parent" />
-
-    <Button
-        android:id="@+id/button_cancel"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_margin="@dimen/spacing_large"
-        android:text="@android:string/cancel"
-        app:layout_constraintStart_toEndOf="@id/button_edit"
-        app:layout_constraintEnd_toStartOf="@id/button_ok"
-        app:layout_constraintTop_toBottomOf="@id/barrier"
-        app:layout_constraintBottom_toBottomOf="parent" />
-
-    <Button
-        android:id="@+id/button_ok"
-        android:layout_width="0dp"
-        android:layout_height="wrap_content"
-        android:layout_margin="@dimen/spacing_large"
-        android:text="@android:string/ok"
-        app:layout_constraintStart_toEndOf="@id/button_cancel"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/barrier"
-        app:layout_constraintBottom_toBottomOf="parent" />
+    </androidx.constraintlayout.widget.ConstraintLayout>
 
 </androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_cheat_list.xml b/src/android/app/src/main/res/layout/fragment_cheat_list.xml
index 04bf76ffe..29c30f55b 100644
--- a/src/android/app/src/main/res/layout/fragment_cheat_list.xml
+++ b/src/android/app/src/main/res/layout/fragment_cheat_list.xml
@@ -5,15 +5,36 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
-    <androidx.recyclerview.widget.RecyclerView
-        android:id="@+id/cheat_list"
+    <androidx.coordinatorlayout.widget.CoordinatorLayout
         android:layout_width="match_parent"
-        android:layout_height="0dp"
-        android:clipToPadding="false"
-        app:layout_constraintStart_toStartOf="parent"
+        android:layout_height="match_parent"
         app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintBottom_toBottomOf="parent" />
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <com.google.android.material.appbar.AppBarLayout
+            android:id="@+id/appbar_cheat_list"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:fitsSystemWindows="true">
+
+            <com.google.android.material.appbar.MaterialToolbar
+                android:id="@+id/toolbar_cheat_list"
+                android:layout_width="match_parent"
+                android:layout_height="?attr/actionBarSize"
+                app:title="@string/cheats"
+                app:navigationIcon="@drawable/ic_back" />
+
+        </com.google.android.material.appbar.AppBarLayout>
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/cheat_list"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:clipToPadding="false"
+            app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+
+    </androidx.coordinatorlayout.widget.CoordinatorLayout>
 
     <com.google.android.material.floatingactionbutton.FloatingActionButton
         android:id="@+id/fab"
@@ -21,7 +42,6 @@
         android:layout_height="wrap_content"
         android:src="@drawable/ic_add"
         android:contentDescription="@string/cheats_add"
-        android:layout_margin="16dp"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintBottom_toBottomOf="parent" />
 
diff --git a/src/android/app/src/main/res/layout/fragment_cheats.xml b/src/android/app/src/main/res/layout/fragment_cheats.xml
new file mode 100644
index 000000000..ddc709f4c
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_cheats.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.slidingpanelayout.widget.SlidingPaneLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/sliding_pane_layout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="?attr/colorSurface">
+
+    <androidx.fragment.app.FragmentContainerView
+        android:id="@+id/cheat_list_container"
+        android:name="org.citra.citra_emu.features.cheats.ui.CheatListFragment"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        tools:layout="@layout/fragment_cheat_list" />
+
+    <androidx.fragment.app.FragmentContainerView
+        android:id="@+id/cheat_details_container"
+        android:name="org.citra.citra_emu.features.cheats.ui.CheatDetailsFragment"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_weight="1"
+        tools:layout="@layout/fragment_cheat_details" />
+
+</androidx.slidingpanelayout.widget.SlidingPaneLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_home_settings.xml b/src/android/app/src/main/res/layout/fragment_home_settings.xml
index 65a58724d..714152a7e 100644
--- a/src/android/app/src/main/res/layout/fragment_home_settings.xml
+++ b/src/android/app/src/main/res/layout/fragment_home_settings.xml
@@ -18,9 +18,9 @@
 
         <ImageView
             android:id="@+id/logo_image"
-            android:layout_width="175dp"
-            android:layout_height="175dp"
-            android:layout_margin="64dp"
+            android:layout_width="104dp"
+            android:layout_height="104dp"
+            android:layout_margin="32dp"
             android:layout_gravity="center_horizontal"
             android:src="@drawable/ic_citra_full" />
 
diff --git a/src/android/app/src/main/res/layout/list_item_cheat.xml b/src/android/app/src/main/res/layout/list_item_cheat.xml
index 5afa11794..c2c3217e9 100644
--- a/src/android/app/src/main/res/layout/list_item_cheat.xml
+++ b/src/android/app/src/main/res/layout/list_item_cheat.xml
@@ -1,35 +1,37 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout 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/root"
+    android:id="@+id/cheat_container"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
+    android:background="?attr/selectableItemBackground"
+    android:paddingVertical="16dp"
+    android:paddingHorizontal="20dp"
     android:focusable="true"
-    android:nextFocusRight="@id/checkbox">
+    android:nextFocusRight="@id/cheat_switch">
 
     <TextView
         android:id="@+id/text_name"
+        style="@style/TextAppearance.AppCompat.Headline"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
+        android:layout_marginVertical="16dp"
+        android:layout_marginEnd="16dp"
         android:textSize="16sp"
-        android:layout_margin="@dimen/spacing_large"
-        style="@style/TextAppearance.AppCompat.Headline"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintEnd_toStartOf="@id/checkbox"
-        app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@id/cheat_switch"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
         tools:text="Max Lives after losing 1" />
 
-    <CheckBox
-        android:id="@+id/checkbox"
+    <com.google.android.material.materialswitch.MaterialSwitch
+        android:id="@+id/cheat_switch"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:focusable="true"
         android:gravity="center"
-        android:nextFocusLeft="@id/root"
-        android:layout_marginEnd="8dp"
+        android:nextFocusLeft="@id/cheat_container"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toEndOf="@id/text_name"
diff --git a/src/android/app/src/main/res/navigation/cheats_navigation.xml b/src/android/app/src/main/res/navigation/cheats_navigation.xml
new file mode 100644
index 000000000..c1a8c993a
--- /dev/null
+++ b/src/android/app/src/main/res/navigation/cheats_navigation.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<navigation 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/cheats_navigation"
+    app:startDestination="@id/cheatsFragment">
+
+    <fragment
+        android:id="@+id/cheatsFragment"
+        android:name="org.citra.citra_emu.features.cheats.ui.CheatsFragment"
+        android:label="fragment_cheats"
+        tools:layout="@layout/fragment_cheats">
+        <argument
+            android:name="titleId"
+            app:argType="long"
+            android:defaultValue="-1L" />
+    </fragment>
+
+</navigation>
diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml
index 5770ad58d..ec8a02afe 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -75,6 +75,20 @@
         android:name="org.citra.citra_emu.fragments.SystemFilesFragment"
         android:label="SystemFilesFragment" />
 
+    <fragment
+        android:id="@+id/cheatsFragment"
+        android:name="org.citra.citra_emu.features.cheats.ui.CheatsFragment"
+        android:label="CheatsFragment" >
+        <argument
+            android:name="titleId"
+            app:argType="long"
+            android:defaultValue="-1L" />
+    </fragment>
+
+    <action
+        android:id="@+id/action_global_cheatsFragment"
+        app:destination="@id/cheatsFragment" />
+
     <fragment
         android:id="@+id/driverManagerFragment"
         android:name="org.citra.citra_emu.fragments.DriverManagerFragment"
diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
index 46c3e2282..cd542bae8 100644
--- a/src/android/app/src/main/res/values/arrays.xml
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -77,6 +77,7 @@
         <item>@string/controller_dpad</item>
         <item>@string/controller_circlepad</item>
         <item>@string/controller_c</item>
+        <item>@string/button_home</item>
     </string-array>
 
     <string-array name="cameraImageSourceNames">
diff --git a/src/android/app/src/main/res/values/integers.xml b/src/android/app/src/main/res/values/integers.xml
index 9f6d8492e..211234c77 100644
--- a/src/android/app/src/main/res/values/integers.xml
+++ b/src/android/app/src/main/res/values/integers.xml
@@ -29,7 +29,7 @@
     <integer name="N3DS_BUTTON_SELECT_Y">850</integer>
     <integer name="N3DS_BUTTON_START_X">550</integer>
     <integer name="N3DS_BUTTON_START_Y">850</integer>
-    <integer name="N3DS_BUTTON_HOME_X">450</integer>
+    <integer name="N3DS_BUTTON_HOME_X">510</integer>
     <integer name="N3DS_BUTTON_HOME_Y">850</integer>
 
     <!-- Default N3DS portrait layout -->
@@ -55,8 +55,8 @@
     <integer name="N3DS_STICK_C_PORTRAIT_Y">710</integer>
     <integer name="N3DS_STICK_MAIN_PORTRAIT_X">80</integer>
     <integer name="N3DS_STICK_MAIN_PORTRAIT_Y">840</integer>
-    <integer name="N3DS_BUTTON_HOME_PORTRAIT_X">360</integer>
-    <integer name="N3DS_BUTTON_HOME_PORTRAIT_Y">794</integer>
+    <integer name="N3DS_BUTTON_HOME_PORTRAIT_X">460</integer>
+    <integer name="N3DS_BUTTON_HOME_PORTRAIT_Y">840</integer>
     <integer name="N3DS_BUTTON_SELECT_PORTRAIT_X">400</integer>
     <integer name="N3DS_BUTTON_SELECT_PORTRAIT_Y">794</integer>
     <integer name="N3DS_BUTTON_START_PORTRAIT_X">520</integer>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index b5afd71f0..62a69b1d3 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -376,6 +376,7 @@
     <string name="max_length_exceeded">Text is too long (should be no more than %d characters)</string>
     <string name="blank_input_not_allowed">Blank input is not allowed</string>
     <string name="empty_input_not_allowed">Empty input is not allowed</string>
+    <string name="invalid_input">Invalid input</string>
 
     <!-- Mii Selector -->
     <string name="mii_selector">Mii Selector</string>