From 70c3d36536ad98a5569d51f1d0f68a6f01890f11 Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Sun, 10 Dec 2023 20:10:36 -0500
Subject: [PATCH 01/20] android: Refactor settings to expose more options

In AbstractSetting, this removes the category, androidDefault, and valueAsString properties as they are no longer needed and have replacements. isSwitchable, global, and getValueAsString are all exposed and give better options for working with global/per-game settings.
---
 .../yuzu_emu/activities/EmulationActivity.kt  |  20 ++-
 .../settings/model/AbstractBooleanSetting.kt  |   3 +-
 .../settings/model/AbstractByteSetting.kt     |   3 +-
 .../settings/model/AbstractFloatSetting.kt    |   3 +-
 .../settings/model/AbstractIntSetting.kt      |   3 +-
 .../settings/model/AbstractLongSetting.kt     |   3 +-
 .../settings/model/AbstractSetting.kt         |  14 +-
 .../settings/model/AbstractShortSetting.kt    |   3 +-
 .../settings/model/AbstractStringSetting.kt   |   3 +-
 .../features/settings/model/BooleanSetting.kt |  50 ++++---
 .../features/settings/model/ByteSetting.kt    |  22 +--
 .../features/settings/model/FloatSetting.kt   |  22 +--
 .../features/settings/model/IntSetting.kt     |  49 ++++---
 .../features/settings/model/LongSetting.kt    |  22 +--
 .../features/settings/model/Settings.kt       |  51 -------
 .../features/settings/model/ShortSetting.kt   |  22 +--
 .../features/settings/model/StringSetting.kt  |  23 ++--
 .../settings/model/view/DateTimeSetting.kt    |   5 +-
 .../settings/model/view/SettingsItem.kt       |  24 +++-
 .../model/view/SingleChoiceSetting.kt         |  17 +--
 .../settings/model/view/SliderSetting.kt      |  30 ++---
 .../model/view/StringSingleChoiceSetting.kt   |   7 +-
 .../settings/model/view/SwitchSetting.kt      |  24 ++--
 .../features/settings/ui/SettingsAdapter.kt   |  11 +-
 .../settings/ui/SettingsFragmentPresenter.kt  |  29 ++--
 .../ui/viewholder/DateTimeViewHolder.kt       |   2 +-
 .../ui/viewholder/SingleChoiceViewHolder.kt   |   4 +-
 .../ui/viewholder/SliderViewHolder.kt         |   2 +-
 .../ui/viewholder/SwitchSettingViewHolder.kt  |   2 +-
 .../yuzu_emu/fragments/EmulationFragment.kt   |   6 +-
 .../fragments/SettingsDialogFragment.kt       |  12 +-
 .../org/yuzu/yuzu_emu/utils/NativeConfig.kt   |  47 +++++--
 .../app/src/main/jni/native_config.cpp        | 126 ++++++++----------
 src/common/settings_setting.h                 |   5 +-
 34 files changed, 320 insertions(+), 349 deletions(-)

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
index f41d7bdbfa..9b08f008d1 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
@@ -172,7 +172,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
 
     override fun onUserLeaveHint() {
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
-            if (BooleanSetting.PICTURE_IN_PICTURE.boolean && !isInPictureInPictureMode) {
+            if (BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && !isInPictureInPictureMode) {
                 val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
                     .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
                 enterPictureInPictureMode(pictureInPictureParamsBuilder.build())
@@ -284,7 +284,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
 
     private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder():
         PictureInPictureParams.Builder {
-        val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.int) {
+        val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.getInt()) {
             0 -> Rational(16, 9)
             1 -> Rational(4, 3)
             2 -> Rational(21, 9)
@@ -331,7 +331,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
             pictureInPictureActions.add(pauseRemoteAction)
         }
 
-        if (BooleanSetting.AUDIO_MUTED.boolean) {
+        if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
             val unmuteIcon = Icon.createWithResource(
                 this@EmulationActivity,
                 R.drawable.ic_pip_unmute
@@ -376,7 +376,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
             val isEmulationActive = emulationViewModel.emulationStarted.value &&
                 !emulationViewModel.isEmulationStopping.value
             pictureInPictureParamsBuilder.setAutoEnterEnabled(
-                BooleanSetting.PICTURE_IN_PICTURE.boolean && isEmulationActive
+                BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && isEmulationActive
             )
         }
         setPictureInPictureParams(pictureInPictureParamsBuilder.build())
@@ -390,9 +390,13 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
                 if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation()
             }
             if (intent.action == actionUnmute) {
-                if (BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(false)
+                if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
+                    BooleanSetting.AUDIO_MUTED.setBoolean(false)
+                }
             } else if (intent.action == actionMute) {
-                if (!BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(true)
+                if (!BooleanSetting.AUDIO_MUTED.getBoolean()) {
+                    BooleanSetting.AUDIO_MUTED.setBoolean(true)
+                }
             }
             buildPictureInPictureParams()
         }
@@ -423,7 +427,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
             } catch (ignored: Exception) {
             }
             // Always resume audio, since there is no UI button
-            if (BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(false)
+            if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
+                BooleanSetting.AUDIO_MUTED.setBoolean(false)
+            }
         }
     }
 
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt
index aeda8d2220..0ba4653562 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt
@@ -4,7 +4,6 @@
 package org.yuzu.yuzu_emu.features.settings.model
 
 interface AbstractBooleanSetting : AbstractSetting {
-    val boolean: Boolean
-
+    fun getBoolean(needsGlobal: Boolean = false): Boolean
     fun setBoolean(value: Boolean)
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt
index 606519ad84..cf63005359 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt
@@ -4,7 +4,6 @@
 package org.yuzu.yuzu_emu.features.settings.model
 
 interface AbstractByteSetting : AbstractSetting {
-    val byte: Byte
-
+    fun getByte(needsGlobal: Boolean = false): Byte
     fun setByte(value: Byte)
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt
index 974925eeda..c6c0bcf348 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt
@@ -4,7 +4,6 @@
 package org.yuzu.yuzu_emu.features.settings.model
 
 interface AbstractFloatSetting : AbstractSetting {
-    val float: Float
-
+    fun getFloat(needsGlobal: Boolean = false): Float
     fun setFloat(value: Float)
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt
index 89b285b108..826402c343 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt
@@ -4,7 +4,6 @@
 package org.yuzu.yuzu_emu.features.settings.model
 
 interface AbstractIntSetting : AbstractSetting {
-    val int: Int
-
+    fun getInt(needsGlobal: Boolean = false): Int
     fun setInt(value: Int)
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt
index 4873942db7..2b62cc06b5 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt
@@ -4,7 +4,6 @@
 package org.yuzu.yuzu_emu.features.settings.model
 
 interface AbstractLongSetting : AbstractSetting {
-    val long: Long
-
+    fun getLong(needsGlobal: Boolean = false): Long
     fun setLong(value: Long)
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt
index 8b6d29fe5b..e384c78c25 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt
@@ -7,12 +7,7 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
 
 interface AbstractSetting {
     val key: String
-    val category: Settings.Category
     val defaultValue: Any
-    val androidDefault: Any?
-        get() = null
-    val valueAsString: String
-        get() = ""
 
     val isRuntimeModifiable: Boolean
         get() = NativeConfig.getIsRuntimeModifiable(key)
@@ -20,5 +15,14 @@ interface AbstractSetting {
     val pairedSettingKey: String
         get() = NativeConfig.getPairedSettingKey(key)
 
+    val isSwitchable: Boolean
+        get() = NativeConfig.getIsSwitchable(key)
+
+    var global: Boolean
+        get() = NativeConfig.usingGlobal(key)
+        set(value) = NativeConfig.setGlobal(key, value)
+
+    fun getValueAsString(needsGlobal: Boolean = false): String
+
     fun reset()
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt
index 91407ccbb4..8bfa81e4ac 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt
@@ -4,7 +4,6 @@
 package org.yuzu.yuzu_emu.features.settings.model
 
 interface AbstractShortSetting : AbstractSetting {
-    val short: Short
-
+    fun getShort(needsGlobal: Boolean = false): Short
     fun setShort(value: Short)
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt
index c8935cc48c..6ff8fd3f9a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt
@@ -4,7 +4,6 @@
 package org.yuzu.yuzu_emu.features.settings.model
 
 interface AbstractStringSetting : AbstractSetting {
-    val string: String
-
+    fun getString(needsGlobal: Boolean = false): String
     fun setString(value: String)
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
index 8476ce8671..16f06cd0af 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
@@ -5,36 +5,34 @@ package org.yuzu.yuzu_emu.features.settings.model
 
 import org.yuzu.yuzu_emu.utils.NativeConfig
 
-enum class BooleanSetting(
-    override val key: String,
-    override val category: Settings.Category,
-    override val androidDefault: Boolean? = null
-) : AbstractBooleanSetting {
-    AUDIO_MUTED("audio_muted", Settings.Category.Audio),
-    CPU_DEBUG_MODE("cpu_debug_mode", Settings.Category.Cpu),
-    FASTMEM("cpuopt_fastmem", Settings.Category.Cpu),
-    FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives", Settings.Category.Cpu),
-    RENDERER_USE_SPEED_LIMIT("use_speed_limit", Settings.Category.Core),
-    USE_DOCKED_MODE("use_docked_mode", Settings.Category.System, false),
-    RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache", Settings.Category.Renderer),
-    RENDERER_FORCE_MAX_CLOCK("force_max_clock", Settings.Category.Renderer),
-    RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders", Settings.Category.Renderer),
-    RENDERER_REACTIVE_FLUSHING("use_reactive_flushing", Settings.Category.Renderer, false),
-    RENDERER_DEBUG("debug", Settings.Category.Renderer),
-    PICTURE_IN_PICTURE("picture_in_picture", Settings.Category.Android),
-    USE_CUSTOM_RTC("custom_rtc_enabled", Settings.Category.System);
+enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
+    AUDIO_MUTED("audio_muted"),
+    CPU_DEBUG_MODE("cpu_debug_mode"),
+    FASTMEM("cpuopt_fastmem"),
+    FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives"),
+    RENDERER_USE_SPEED_LIMIT("use_speed_limit"),
+    USE_DOCKED_MODE("use_docked_mode"),
+    RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache"),
+    RENDERER_FORCE_MAX_CLOCK("force_max_clock"),
+    RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
+    RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
+    RENDERER_DEBUG("debug"),
+    PICTURE_IN_PICTURE("picture_in_picture"),
+    USE_CUSTOM_RTC("custom_rtc_enabled");
 
-    override val boolean: Boolean
-        get() = NativeConfig.getBoolean(key, false)
+    override fun getBoolean(needsGlobal: Boolean): Boolean =
+        NativeConfig.getBoolean(key, needsGlobal)
 
-    override fun setBoolean(value: Boolean) = NativeConfig.setBoolean(key, value)
-
-    override val defaultValue: Boolean by lazy {
-        androidDefault ?: NativeConfig.getBoolean(key, true)
+    override fun setBoolean(value: Boolean) {
+        if (NativeConfig.isPerGameConfigLoaded()) {
+            global = false
+        }
+        NativeConfig.setBoolean(key, value)
     }
 
-    override val valueAsString: String
-        get() = if (boolean) "1" else "0"
+    override val defaultValue: Boolean by lazy { NativeConfig.getDefaultToString(key).toBoolean() }
+
+    override fun getValueAsString(needsGlobal: Boolean): String = getBoolean(needsGlobal).toString()
 
     override fun reset() = NativeConfig.setBoolean(key, defaultValue)
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt
index 6ec0a765ef..7b7fac2112 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt
@@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
 
 import org.yuzu.yuzu_emu.utils.NativeConfig
 
-enum class ByteSetting(
-    override val key: String,
-    override val category: Settings.Category
-) : AbstractByteSetting {
-    AUDIO_VOLUME("volume", Settings.Category.Audio);
+enum class ByteSetting(override val key: String) : AbstractByteSetting {
+    AUDIO_VOLUME("volume");
 
-    override val byte: Byte
-        get() = NativeConfig.getByte(key, false)
+    override fun getByte(needsGlobal: Boolean): Byte = NativeConfig.getByte(key, needsGlobal)
 
-    override fun setByte(value: Byte) = NativeConfig.setByte(key, value)
+    override fun setByte(value: Byte) {
+        if (NativeConfig.isPerGameConfigLoaded()) {
+            global = false
+        }
+        NativeConfig.setByte(key, value)
+    }
 
-    override val defaultValue: Byte by lazy { NativeConfig.getByte(key, true) }
+    override val defaultValue: Byte by lazy { NativeConfig.getDefaultToString(key).toByte() }
 
-    override val valueAsString: String
-        get() = byte.toString()
+    override fun getValueAsString(needsGlobal: Boolean): String = getByte(needsGlobal).toString()
 
     override fun reset() = NativeConfig.setByte(key, defaultValue)
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt
index 0181d06f21..4644824d8a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt
@@ -5,22 +5,22 @@ package org.yuzu.yuzu_emu.features.settings.model
 
 import org.yuzu.yuzu_emu.utils.NativeConfig
 
-enum class FloatSetting(
-    override val key: String,
-    override val category: Settings.Category
-) : AbstractFloatSetting {
+enum class FloatSetting(override val key: String) : AbstractFloatSetting {
     // No float settings currently exist
-    EMPTY_SETTING("", Settings.Category.UiGeneral);
+    EMPTY_SETTING("");
 
-    override val float: Float
-        get() = NativeConfig.getFloat(key, false)
+    override fun getFloat(needsGlobal: Boolean): Float = NativeConfig.getFloat(key, false)
 
-    override fun setFloat(value: Float) = NativeConfig.setFloat(key, value)
+    override fun setFloat(value: Float) {
+        if (NativeConfig.isPerGameConfigLoaded()) {
+            global = false
+        }
+        NativeConfig.setFloat(key, value)
+    }
 
-    override val defaultValue: Float by lazy { NativeConfig.getFloat(key, true) }
+    override val defaultValue: Float by lazy { NativeConfig.getDefaultToString(key).toFloat() }
 
-    override val valueAsString: String
-        get() = float.toString()
+    override fun getValueAsString(needsGlobal: Boolean): String = getFloat(needsGlobal).toString()
 
     override fun reset() = NativeConfig.setFloat(key, defaultValue)
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
index ef10b209fd..21e4e1afd5 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
@@ -5,36 +5,33 @@ package org.yuzu.yuzu_emu.features.settings.model
 
 import org.yuzu.yuzu_emu.utils.NativeConfig
 
-enum class IntSetting(
-    override val key: String,
-    override val category: Settings.Category,
-    override val androidDefault: Int? = null
-) : AbstractIntSetting {
-    CPU_BACKEND("cpu_backend", Settings.Category.Cpu),
-    CPU_ACCURACY("cpu_accuracy", Settings.Category.Cpu),
-    REGION_INDEX("region_index", Settings.Category.System),
-    LANGUAGE_INDEX("language_index", Settings.Category.System),
-    RENDERER_BACKEND("backend", Settings.Category.Renderer),
-    RENDERER_ACCURACY("gpu_accuracy", Settings.Category.Renderer, 0),
-    RENDERER_RESOLUTION("resolution_setup", Settings.Category.Renderer),
-    RENDERER_VSYNC("use_vsync", Settings.Category.Renderer),
-    RENDERER_SCALING_FILTER("scaling_filter", Settings.Category.Renderer),
-    RENDERER_ANTI_ALIASING("anti_aliasing", Settings.Category.Renderer),
-    RENDERER_SCREEN_LAYOUT("screen_layout", Settings.Category.Android),
-    RENDERER_ASPECT_RATIO("aspect_ratio", Settings.Category.Renderer),
-    AUDIO_OUTPUT_ENGINE("output_engine", Settings.Category.Audio);
+enum class IntSetting(override val key: String) : AbstractIntSetting {
+    CPU_BACKEND("cpu_backend"),
+    CPU_ACCURACY("cpu_accuracy"),
+    REGION_INDEX("region_index"),
+    LANGUAGE_INDEX("language_index"),
+    RENDERER_BACKEND("backend"),
+    RENDERER_ACCURACY("gpu_accuracy"),
+    RENDERER_RESOLUTION("resolution_setup"),
+    RENDERER_VSYNC("use_vsync"),
+    RENDERER_SCALING_FILTER("scaling_filter"),
+    RENDERER_ANTI_ALIASING("anti_aliasing"),
+    RENDERER_SCREEN_LAYOUT("screen_layout"),
+    RENDERER_ASPECT_RATIO("aspect_ratio"),
+    AUDIO_OUTPUT_ENGINE("output_engine");
 
-    override val int: Int
-        get() = NativeConfig.getInt(key, false)
+    override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal)
 
-    override fun setInt(value: Int) = NativeConfig.setInt(key, value)
-
-    override val defaultValue: Int by lazy {
-        androidDefault ?: NativeConfig.getInt(key, true)
+    override fun setInt(value: Int) {
+        if (NativeConfig.isPerGameConfigLoaded()) {
+            global = false
+        }
+        NativeConfig.setInt(key, value)
     }
 
-    override val valueAsString: String
-        get() = int.toString()
+    override val defaultValue: Int by lazy { NativeConfig.getDefaultToString(key).toInt() }
+
+    override fun getValueAsString(needsGlobal: Boolean): String = getInt(needsGlobal).toString()
 
     override fun reset() = NativeConfig.setInt(key, defaultValue)
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt
index c526fc4cfa..e3efd516c0 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt
@@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
 
 import org.yuzu.yuzu_emu.utils.NativeConfig
 
-enum class LongSetting(
-    override val key: String,
-    override val category: Settings.Category
-) : AbstractLongSetting {
-    CUSTOM_RTC("custom_rtc", Settings.Category.System);
+enum class LongSetting(override val key: String) : AbstractLongSetting {
+    CUSTOM_RTC("custom_rtc");
 
-    override val long: Long
-        get() = NativeConfig.getLong(key, false)
+    override fun getLong(needsGlobal: Boolean): Long = NativeConfig.getLong(key, needsGlobal)
 
-    override fun setLong(value: Long) = NativeConfig.setLong(key, value)
+    override fun setLong(value: Long) {
+        if (NativeConfig.isPerGameConfigLoaded()) {
+            global = false
+        }
+        NativeConfig.setLong(key, value)
+    }
 
-    override val defaultValue: Long by lazy { NativeConfig.getLong(key, true) }
+    override val defaultValue: Long by lazy { NativeConfig.getDefaultToString(key).toLong() }
 
-    override val valueAsString: String
-        get() = long.toString()
+    override fun getValueAsString(needsGlobal: Boolean): String = getLong(needsGlobal).toString()
 
     override fun reset() = NativeConfig.setLong(key, defaultValue)
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
index e3cd661859..9551fc05e4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
@@ -6,62 +6,11 @@ package org.yuzu.yuzu_emu.features.settings.model
 import org.yuzu.yuzu_emu.R
 
 object Settings {
-    enum class Category {
-        Android,
-        Audio,
-        Core,
-        Cpu,
-        CpuDebug,
-        CpuUnsafe,
-        Renderer,
-        RendererAdvanced,
-        RendererDebug,
-        System,
-        SystemAudio,
-        DataStorage,
-        Debugging,
-        DebuggingGraphics,
-        Miscellaneous,
-        Network,
-        WebService,
-        AddOns,
-        Controls,
-        Ui,
-        UiGeneral,
-        UiLayout,
-        UiGameList,
-        Screenshots,
-        Shortcuts,
-        Multiplayer,
-        Services,
-        Paths,
-        MaxEnum
-    }
-
-    val settingsList = listOf<AbstractSetting>(
-        *BooleanSetting.values(),
-        *ByteSetting.values(),
-        *ShortSetting.values(),
-        *IntSetting.values(),
-        *FloatSetting.values(),
-        *LongSetting.values(),
-        *StringSetting.values()
-    )
-
-    const val SECTION_GENERAL = "General"
-    const val SECTION_SYSTEM = "System"
-    const val SECTION_RENDERER = "Renderer"
-    const val SECTION_AUDIO = "Audio"
-    const val SECTION_CPU = "Cpu"
-    const val SECTION_THEME = "Theme"
-    const val SECTION_DEBUG = "Debug"
-
     enum class MenuTag(val titleId: Int) {
         SECTION_ROOT(R.string.advanced_settings),
         SECTION_SYSTEM(R.string.preferences_system),
         SECTION_RENDERER(R.string.preferences_graphics),
         SECTION_AUDIO(R.string.preferences_audio),
-        SECTION_CPU(R.string.cpu),
         SECTION_THEME(R.string.preferences_theme),
         SECTION_DEBUG(R.string.preferences_debug);
     }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt
index c9a0c664cb..16eb4ffdd5 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt
@@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
 
 import org.yuzu.yuzu_emu.utils.NativeConfig
 
-enum class ShortSetting(
-    override val key: String,
-    override val category: Settings.Category
-) : AbstractShortSetting {
-    RENDERER_SPEED_LIMIT("speed_limit", Settings.Category.Core);
+enum class ShortSetting(override val key: String) : AbstractShortSetting {
+    RENDERER_SPEED_LIMIT("speed_limit");
 
-    override val short: Short
-        get() = NativeConfig.getShort(key, false)
+    override fun getShort(needsGlobal: Boolean): Short = NativeConfig.getShort(key, needsGlobal)
 
-    override fun setShort(value: Short) = NativeConfig.setShort(key, value)
+    override fun setShort(value: Short) {
+        if (NativeConfig.isPerGameConfigLoaded()) {
+            global = false
+        }
+        NativeConfig.setShort(key, value)
+    }
 
-    override val defaultValue: Short by lazy { NativeConfig.getShort(key, true) }
+    override val defaultValue: Short by lazy { NativeConfig.getDefaultToString(key).toShort() }
 
-    override val valueAsString: String
-        get() = short.toString()
+    override fun getValueAsString(needsGlobal: Boolean): String = getShort(needsGlobal).toString()
 
     override fun reset() = NativeConfig.setShort(key, defaultValue)
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt
index 9bb3e66d4f..a0d8cfedea 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt
@@ -5,22 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
 
 import org.yuzu.yuzu_emu.utils.NativeConfig
 
-enum class StringSetting(
-    override val key: String,
-    override val category: Settings.Category
-) : AbstractStringSetting {
-    // No string settings currently exist
-    EMPTY_SETTING("", Settings.Category.UiGeneral);
+enum class StringSetting(override val key: String) : AbstractStringSetting {
+    DRIVER_PATH("driver_path");
 
-    override val string: String
-        get() = NativeConfig.getString(key, false)
+    override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal)
 
-    override fun setString(value: String) = NativeConfig.setString(key, value)
+    override fun setString(value: String) {
+        if (NativeConfig.isPerGameConfigLoaded()) {
+            global = false
+        }
+        NativeConfig.setString(key, value)
+    }
 
-    override val defaultValue: String by lazy { NativeConfig.getString(key, true) }
+    override val defaultValue: String by lazy { NativeConfig.getDefaultToString(key) }
 
-    override val valueAsString: String
-        get() = string
+    override fun getValueAsString(needsGlobal: Boolean): String = getString(needsGlobal)
 
     override fun reset() = NativeConfig.setString(key, defaultValue)
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt
index 8bc1641978..1d81f5f2b4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt
@@ -12,7 +12,6 @@ class DateTimeSetting(
 ) : SettingsItem(longSetting, titleId, descriptionId) {
     override val type = TYPE_DATETIME_SETTING
 
-    var value: Long
-        get() = longSetting.long
-        set(value) = (setting as AbstractLongSetting).setLong(value)
+    fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal)
+    fun setValue(value: Long) = (setting as AbstractLongSetting).setLong(value)
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
index e198b18a02..3845272946 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
@@ -11,7 +11,6 @@ import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
 import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
 import org.yuzu.yuzu_emu.features.settings.model.IntSetting
 import org.yuzu.yuzu_emu.features.settings.model.LongSetting
-import org.yuzu.yuzu_emu.features.settings.model.Settings
 import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
 
 /**
@@ -48,8 +47,8 @@ abstract class SettingsItem(
 
         val emptySetting = object : AbstractSetting {
             override val key: String = ""
-            override val category: Settings.Category = Settings.Category.Ui
             override val defaultValue: Any = false
+            override fun getValueAsString(needsGlobal: Boolean): String = ""
             override fun reset() {}
         }
 
@@ -270,9 +269,9 @@ abstract class SettingsItem(
             )
 
             val fastmem = object : AbstractBooleanSetting {
-                override val boolean: Boolean
-                    get() =
-                        BooleanSetting.FASTMEM.boolean && BooleanSetting.FASTMEM_EXCLUSIVES.boolean
+                override fun getBoolean(needsGlobal: Boolean): Boolean =
+                    BooleanSetting.FASTMEM.getBoolean() &&
+                        BooleanSetting.FASTMEM_EXCLUSIVES.getBoolean()
 
                 override fun setBoolean(value: Boolean) {
                     BooleanSetting.FASTMEM.setBoolean(value)
@@ -280,9 +279,22 @@ abstract class SettingsItem(
                 }
 
                 override val key: String = FASTMEM_COMBINED
-                override val category = Settings.Category.Cpu
                 override val isRuntimeModifiable: Boolean = false
                 override val defaultValue: Boolean = true
+                override val isSwitchable: Boolean = true
+                override var global: Boolean
+                    get() {
+                        return BooleanSetting.FASTMEM.global &&
+                            BooleanSetting.FASTMEM_EXCLUSIVES.global
+                    }
+                    set(value) {
+                        BooleanSetting.FASTMEM.global = value
+                        BooleanSetting.FASTMEM_EXCLUSIVES.global = value
+                    }
+
+                override fun getValueAsString(needsGlobal: Boolean): String =
+                    getBoolean().toString()
+
                 override fun reset() = setBoolean(defaultValue)
             }
             put(SwitchSetting(fastmem, R.string.fastmem, 0))
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt
index 705527a733..97a5a9e596 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt
@@ -15,16 +15,11 @@ class SingleChoiceSetting(
 ) : SettingsItem(setting, titleId, descriptionId) {
     override val type = TYPE_SINGLE_CHOICE
 
-    var selectedValue: Int
-        get() {
-            return when (setting) {
-                is AbstractIntSetting -> setting.int
-                else -> -1
-            }
-        }
-        set(value) {
-            when (setting) {
-                is AbstractIntSetting -> setting.setInt(value)
-            }
+    fun getSelectedValue(needsGlobal: Boolean = false) =
+        when (setting) {
+            is AbstractIntSetting -> setting.getInt(needsGlobal)
+            else -> -1
         }
+
+    fun setSelectedValue(value: Int) = (setting as AbstractIntSetting).setInt(value)
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt
index c3b5df02c9..b9b709bf7b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt
@@ -20,22 +20,20 @@ class SliderSetting(
 ) : SettingsItem(setting, titleId, descriptionId) {
     override val type = TYPE_SLIDER
 
-    var selectedValue: Int
-        get() {
-            return when (setting) {
-                is AbstractByteSetting -> setting.byte.toInt()
-                is AbstractShortSetting -> setting.short.toInt()
-                is AbstractIntSetting -> setting.int
-                is AbstractFloatSetting -> setting.float.roundToInt()
-                else -> -1
-            }
+    fun getSelectedValue(needsGlobal: Boolean = false) =
+        when (setting) {
+            is AbstractByteSetting -> setting.getByte(needsGlobal).toInt()
+            is AbstractShortSetting -> setting.getShort(needsGlobal).toInt()
+            is AbstractIntSetting -> setting.getInt(needsGlobal)
+            is AbstractFloatSetting -> setting.getFloat(needsGlobal).roundToInt()
+            else -> -1
         }
-        set(value) {
-            when (setting) {
-                is AbstractByteSetting -> setting.setByte(value.toByte())
-                is AbstractShortSetting -> setting.setShort(value.toShort())
-                is AbstractIntSetting -> setting.setInt(value)
-                is AbstractFloatSetting -> setting.setFloat(value.toFloat())
-            }
+
+    fun setSelectedValue(value: Int) =
+        when (setting) {
+            is AbstractByteSetting -> setting.setByte(value.toByte())
+            is AbstractShortSetting -> setting.setShort(value.toShort())
+            is AbstractFloatSetting -> setting.setFloat(value.toFloat())
+            else -> (setting as AbstractIntSetting).setInt(value)
         }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt
index 871dab4f3b..ba7920f50b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt
@@ -17,14 +17,13 @@ class StringSingleChoiceSetting(
     fun getValueAt(index: Int): String =
         if (index >= 0 && index < values.size) values[index] else ""
 
-    var selectedValue: String
-        get() = stringSetting.string
-        set(value) = stringSetting.setString(value)
+    fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal)
+    fun setSelectedValue(value: String) = stringSetting.setString(value)
 
     val selectValueIndex: Int
         get() {
             for (i in values.indices) {
-                if (values[i] == selectedValue) {
+                if (values[i] == getSelectedValue()) {
                     return i
                 }
             }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt
index 416967e645..44d47dd69d 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt
@@ -14,18 +14,18 @@ class SwitchSetting(
 ) : SettingsItem(setting, titleId, descriptionId) {
     override val type = TYPE_SWITCH
 
-    var checked: Boolean
-        get() {
-            return when (setting) {
-                is AbstractIntSetting -> setting.int == 1
-                is AbstractBooleanSetting -> setting.boolean
-                else -> false
-            }
+    fun getIsChecked(needsGlobal: Boolean = false): Boolean {
+        return when (setting) {
+            is AbstractIntSetting -> setting.getInt(needsGlobal) == 1
+            is AbstractBooleanSetting -> setting.getBoolean(needsGlobal)
+            else -> false
         }
-        set(value) {
-            when (setting) {
-                is AbstractIntSetting -> setting.setInt(if (value) 1 else 0)
-                is AbstractBooleanSetting -> setting.setBoolean(value)
-            }
+    }
+
+    fun setChecked(value: Boolean) {
+        when (setting) {
+            is AbstractIntSetting -> setting.setInt(if (value) 1 else 0)
+            is AbstractBooleanSetting -> setting.setBoolean(value)
         }
+    }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
index af2c1e5820..3f23c064e2 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
@@ -102,8 +102,9 @@ class SettingsAdapter(
         return currentList[position].type
     }
 
-    fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
-        item.checked = checked
+    fun onBooleanClick(item: SwitchSetting, checked: Boolean, position: Int) {
+        item.setChecked(checked)
+        notifyItemChanged(position)
         settingsViewModel.setShouldReloadSettingsList(true)
     }
 
@@ -126,7 +127,7 @@ class SettingsAdapter(
     }
 
     fun onDateTimeClick(item: DateTimeSetting, position: Int) {
-        val storedTime = item.value * 1000
+        val storedTime = item.getValue() * 1000
 
         // Helper to extract hour and minute from epoch time
         val calendar: Calendar = Calendar.getInstance()
@@ -159,9 +160,9 @@ class SettingsAdapter(
             var epochTime: Long = datePicker.selection!! / 1000
             epochTime += timePicker.hour.toLong() * 60 * 60
             epochTime += timePicker.minute.toLong() * 60
-            if (item.value != epochTime) {
+            if (item.getValue() != epochTime) {
                 notifyItemChanged(position)
-                item.value = epochTime
+                item.setValue(epochTime)
             }
         }
         datePicker.show(
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
index 7425728c60..12a389b37a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -36,7 +36,14 @@ class SettingsFragmentPresenter(
         val item = SettingsItem.settingsItems[key]!!
         val pairedSettingKey = item.setting.pairedSettingKey
         if (pairedSettingKey.isNotEmpty()) {
-            val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false)
+            val pairedSettingValue = NativeConfig.getBoolean(
+                pairedSettingKey,
+                if (NativeLibrary.isRunning() && !NativeConfig.isPerGameConfigLoaded()) {
+                    !NativeConfig.usingGlobal(pairedSettingKey)
+                } else {
+                    NativeConfig.usingGlobal(pairedSettingKey)
+                }
+            )
             if (!pairedSettingValue) return
         }
         add(item)
@@ -153,8 +160,8 @@ class SettingsFragmentPresenter(
     private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
         sl.apply {
             val theme: AbstractIntSetting = object : AbstractIntSetting {
-                override val int: Int
-                    get() = preferences.getInt(Settings.PREF_THEME, 0)
+                override fun getInt(needsGlobal: Boolean): Int =
+                    preferences.getInt(Settings.PREF_THEME, 0)
 
                 override fun setInt(value: Int) {
                     preferences.edit()
@@ -164,8 +171,8 @@ class SettingsFragmentPresenter(
                 }
 
                 override val key: String = Settings.PREF_THEME
-                override val category = Settings.Category.UiGeneral
                 override val isRuntimeModifiable: Boolean = false
+                override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
                 override val defaultValue: Int = 0
                 override fun reset() {
                     preferences.edit()
@@ -197,8 +204,8 @@ class SettingsFragmentPresenter(
             }
 
             val themeMode: AbstractIntSetting = object : AbstractIntSetting {
-                override val int: Int
-                    get() = preferences.getInt(Settings.PREF_THEME_MODE, -1)
+                override fun getInt(needsGlobal: Boolean): Int =
+                    preferences.getInt(Settings.PREF_THEME_MODE, -1)
 
                 override fun setInt(value: Int) {
                     preferences.edit()
@@ -208,8 +215,8 @@ class SettingsFragmentPresenter(
                 }
 
                 override val key: String = Settings.PREF_THEME_MODE
-                override val category = Settings.Category.UiGeneral
                 override val isRuntimeModifiable: Boolean = false
+                override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
                 override val defaultValue: Int = -1
                 override fun reset() {
                     preferences.edit()
@@ -230,8 +237,8 @@ class SettingsFragmentPresenter(
             )
 
             val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting {
-                override val boolean: Boolean
-                    get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
+                override fun getBoolean(needsGlobal: Boolean): Boolean =
+                    preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
 
                 override fun setBoolean(value: Boolean) {
                     preferences.edit()
@@ -241,8 +248,10 @@ class SettingsFragmentPresenter(
                 }
 
                 override val key: String = Settings.PREF_BLACK_BACKGROUNDS
-                override val category = Settings.Category.UiGeneral
                 override val isRuntimeModifiable: Boolean = false
+                override fun getValueAsString(needsGlobal: Boolean): String =
+                    getBoolean().toString()
+
                 override val defaultValue: Boolean = false
                 override fun reset() {
                     preferences.edit()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
index 525f013f8b..4e159a7997 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
@@ -29,7 +29,7 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
         }
 
         binding.textSettingValue.visibility = View.VISIBLE
-        val epochTime = setting.value
+        val epochTime = setting.getValue()
         val instant = Instant.ofEpochMilli(epochTime * 1000)
         val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
         val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
index 80d1b22c1a..28c4d17775 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
@@ -29,14 +29,14 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
             val resMgr = binding.textSettingValue.context.resources
             val values = resMgr.getIntArray(item.valuesId)
             for (i in values.indices) {
-                if (values[i] == item.selectedValue) {
+                if (values[i] == item.getSelectedValue()) {
                     binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i]
                     break
                 }
             }
         } else if (item is StringSingleChoiceSetting) {
             for (i in item.values.indices) {
-                if (item.values[i] == item.selectedValue) {
+                if (item.values[i] == item.getSelectedValue()) {
                     binding.textSettingValue.text = item.choices[i]
                     break
                 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
index b83c901006..67432f88ef 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
@@ -26,7 +26,7 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
         binding.textSettingValue.visibility = View.VISIBLE
         binding.textSettingValue.text = String.format(
             binding.textSettingValue.context.getString(R.string.value_with_units),
-            setting.selectedValue,
+            setting.getSelectedValue(),
             setting.units
         )
 
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
index 57fdeaa208..98ed888cb5 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
@@ -27,7 +27,7 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
         }
 
         binding.switchWidget.setOnCheckedChangeListener(null)
-        binding.switchWidget.isChecked = setting.checked
+        binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal)
         binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
             adapter.onBooleanClick(item, binding.switchWidget.isChecked)
         }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
index 734c1d5ca7..b09df7db34 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
@@ -435,7 +435,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
     @SuppressLint("SourceLockedOrientationActivity")
     private fun updateOrientation() {
         emulationActivity?.let {
-            it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) {
+            it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.getInt()) {
                 Settings.LayoutOption_MobileLandscape ->
                     ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
                 Settings.LayoutOption_MobilePortrait ->
@@ -617,7 +617,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
     @SuppressLint("SourceLockedOrientationActivity")
     private fun startConfiguringControls() {
         // Lock the current orientation to prevent editing inconsistencies
-        if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
+        if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) {
             emulationActivity?.let {
                 it.requestedOrientation =
                     if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
@@ -635,7 +635,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
         binding.doneControlConfig.visibility = View.GONE
         binding.surfaceInputOverlay.setIsInEditMode(false)
         // Unlock the orientation if it was locked for editing
-        if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
+        if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) {
             emulationActivity?.let {
                 it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
             }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt
index b88d2c0381..60e029f34f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt
@@ -70,7 +70,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
                 sliderBinding = DialogSliderBinding.inflate(layoutInflater)
                 val item = settingsViewModel.clickedItem as SliderSetting
 
-                settingsViewModel.setSliderTextValue(item.selectedValue.toFloat(), item.units)
+                settingsViewModel.setSliderTextValue(item.getSelectedValue().toFloat(), item.units)
                 sliderBinding.slider.apply {
                     valueFrom = item.min.toFloat()
                     valueTo = item.max.toFloat()
@@ -136,18 +136,18 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
             is SingleChoiceSetting -> {
                 val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
                 val value = getValueForSingleChoiceSelection(scSetting, which)
-                scSetting.selectedValue = value
+                scSetting.setSelectedValue(value)
             }
 
             is StringSingleChoiceSetting -> {
                 val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
                 val value = scSetting.getValueAt(which)
-                scSetting.selectedValue = value
+                scSetting.setSelectedValue(value)
             }
 
             is SliderSetting -> {
                 val sliderSetting = settingsViewModel.clickedItem as SliderSetting
-                sliderSetting.selectedValue = settingsViewModel.sliderProgress.value
+                sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value)
             }
         }
         closeDialog()
@@ -171,7 +171,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
     }
 
     private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
-        val value = item.selectedValue
+        val value = item.getSelectedValue()
         val valuesId = item.valuesId
         if (valuesId > 0) {
             val valuesArray = requireContext().resources.getIntArray(valuesId)
@@ -211,7 +211,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
                     throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!")
 
                 SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress(
-                    (clickedItem as SliderSetting).selectedValue.toFloat()
+                    (clickedItem as SliderSetting).getSelectedValue().toFloat()
                 )
             }
             settingsViewModel.clickedItem = clickedItem
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
index f4e1bb13fc..4c7316ba39 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
@@ -31,32 +31,63 @@ object NativeConfig {
     external fun saveSettings()
 
     external fun getBoolean(key: String, getDefault: Boolean): Boolean
+
+    @Synchronized
+    external fun getBoolean(key: String, needsGlobal: Boolean): Boolean
+
+    @Synchronized
     external fun setBoolean(key: String, value: Boolean)
 
-    external fun getByte(key: String, getDefault: Boolean): Byte
+    @Synchronized
+    external fun getByte(key: String, needsGlobal: Boolean): Byte
+
+    @Synchronized
     external fun setByte(key: String, value: Byte)
 
-    external fun getShort(key: String, getDefault: Boolean): Short
+    @Synchronized
+    external fun getShort(key: String, needsGlobal: Boolean): Short
+
+    @Synchronized
     external fun setShort(key: String, value: Short)
 
-    external fun getInt(key: String, getDefault: Boolean): Int
+    @Synchronized
+    external fun getInt(key: String, needsGlobal: Boolean): Int
+
+    @Synchronized
     external fun setInt(key: String, value: Int)
 
-    external fun getFloat(key: String, getDefault: Boolean): Float
+    @Synchronized
+    external fun getFloat(key: String, needsGlobal: Boolean): Float
+
+    @Synchronized
     external fun setFloat(key: String, value: Float)
 
-    external fun getLong(key: String, getDefault: Boolean): Long
+    @Synchronized
+    external fun getLong(key: String, needsGlobal: Boolean): Long
+
+    @Synchronized
     external fun setLong(key: String, value: Long)
 
-    external fun getString(key: String, getDefault: Boolean): String
+    @Synchronized
+    external fun getString(key: String, needsGlobal: Boolean): String
+
+    @Synchronized
     external fun setString(key: String, value: String)
 
     external fun getIsRuntimeModifiable(key: String): Boolean
 
-    external fun getConfigHeader(category: Int): String
-
     external fun getPairedSettingKey(key: String): String
 
+    external fun getIsSwitchable(key: String): Boolean
+
+    @Synchronized
+    external fun usingGlobal(key: String): Boolean
+
+    @Synchronized
+    external fun setGlobal(key: String, global: Boolean)
+
+    external fun getDefaultToString(key: String): String
+
     /**
      * Gets every [GameDir] in AndroidSettings::values.game_dirs
      */
diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp
index 763b2164c5..9439d11e14 100644
--- a/src/android/app/src/main/jni/native_config.cpp
+++ b/src/android/app/src/main/jni/native_config.cpp
@@ -49,18 +49,12 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveSettings(JNIEnv* env, jobjec
 }
 
 jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getBoolean(JNIEnv* env, jobject obj,
-                                                               jstring jkey, jboolean getDefault) {
+                                                               jstring jkey, jboolean needGlobal) {
     auto setting = getSetting<bool>(env, jkey);
     if (setting == nullptr) {
         return false;
     }
-    setting->SetGlobal(true);
-
-    if (static_cast<bool>(getDefault)) {
-        return setting->GetDefault();
-    }
-
-    return setting->GetValue();
+    return setting->GetValue(static_cast<bool>(needGlobal));
 }
 
 void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setBoolean(JNIEnv* env, jobject obj, jstring jkey,
@@ -69,23 +63,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setBoolean(JNIEnv* env, jobject
     if (setting == nullptr) {
         return;
     }
-    setting->SetGlobal(true);
     setting->SetValue(static_cast<bool>(value));
 }
 
 jbyte Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getByte(JNIEnv* env, jobject obj, jstring jkey,
-                                                         jboolean getDefault) {
+                                                         jboolean needGlobal) {
     auto setting = getSetting<u8>(env, jkey);
     if (setting == nullptr) {
         return -1;
     }
-    setting->SetGlobal(true);
-
-    if (static_cast<bool>(getDefault)) {
-        return setting->GetDefault();
-    }
-
-    return setting->GetValue();
+    return setting->GetValue(static_cast<bool>(needGlobal));
 }
 
 void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setByte(JNIEnv* env, jobject obj, jstring jkey,
@@ -94,23 +81,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setByte(JNIEnv* env, jobject obj
     if (setting == nullptr) {
         return;
     }
-    setting->SetGlobal(true);
     setting->SetValue(value);
 }
 
 jshort Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getShort(JNIEnv* env, jobject obj, jstring jkey,
-                                                           jboolean getDefault) {
+                                                           jboolean needGlobal) {
     auto setting = getSetting<u16>(env, jkey);
     if (setting == nullptr) {
         return -1;
     }
-    setting->SetGlobal(true);
-
-    if (static_cast<bool>(getDefault)) {
-        return setting->GetDefault();
-    }
-
-    return setting->GetValue();
+    return setting->GetValue(static_cast<bool>(needGlobal));
 }
 
 void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setShort(JNIEnv* env, jobject obj, jstring jkey,
@@ -119,23 +99,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setShort(JNIEnv* env, jobject ob
     if (setting == nullptr) {
         return;
     }
-    setting->SetGlobal(true);
     setting->SetValue(value);
 }
 
 jint Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInt(JNIEnv* env, jobject obj, jstring jkey,
-                                                       jboolean getDefault) {
+                                                       jboolean needGlobal) {
     auto setting = getSetting<int>(env, jkey);
     if (setting == nullptr) {
         return -1;
     }
-    setting->SetGlobal(true);
-
-    if (static_cast<bool>(getDefault)) {
-        return setting->GetDefault();
-    }
-
-    return setting->GetValue();
+    return setting->GetValue(needGlobal);
 }
 
 void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInt(JNIEnv* env, jobject obj, jstring jkey,
@@ -144,23 +117,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInt(JNIEnv* env, jobject obj,
     if (setting == nullptr) {
         return;
     }
-    setting->SetGlobal(true);
     setting->SetValue(value);
 }
 
 jfloat Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getFloat(JNIEnv* env, jobject obj, jstring jkey,
-                                                           jboolean getDefault) {
+                                                           jboolean needGlobal) {
     auto setting = getSetting<float>(env, jkey);
     if (setting == nullptr) {
         return -1;
     }
-    setting->SetGlobal(true);
-
-    if (static_cast<bool>(getDefault)) {
-        return setting->GetDefault();
-    }
-
-    return setting->GetValue();
+    return setting->GetValue(static_cast<bool>(needGlobal));
 }
 
 void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setFloat(JNIEnv* env, jobject obj, jstring jkey,
@@ -169,23 +135,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setFloat(JNIEnv* env, jobject ob
     if (setting == nullptr) {
         return;
     }
-    setting->SetGlobal(true);
     setting->SetValue(value);
 }
 
 jlong Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getLong(JNIEnv* env, jobject obj, jstring jkey,
-                                                         jboolean getDefault) {
-    auto setting = getSetting<long>(env, jkey);
+                                                         jboolean needGlobal) {
+    auto setting = getSetting<s64>(env, jkey);
     if (setting == nullptr) {
         return -1;
     }
-    setting->SetGlobal(true);
-
-    if (static_cast<bool>(getDefault)) {
-        return setting->GetDefault();
-    }
-
-    return setting->GetValue();
+    return setting->GetValue(static_cast<bool>(needGlobal));
 }
 
 void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setLong(JNIEnv* env, jobject obj, jstring jkey,
@@ -194,23 +153,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setLong(JNIEnv* env, jobject obj
     if (setting == nullptr) {
         return;
     }
-    setting->SetGlobal(true);
     setting->SetValue(value);
 }
 
 jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getString(JNIEnv* env, jobject obj, jstring jkey,
-                                                             jboolean getDefault) {
+                                                             jboolean needGlobal) {
     auto setting = getSetting<std::string>(env, jkey);
     if (setting == nullptr) {
         return ToJString(env, "");
     }
-    setting->SetGlobal(true);
-
-    if (static_cast<bool>(getDefault)) {
-        return ToJString(env, setting->GetDefault());
-    }
-
-    return ToJString(env, setting->GetValue());
+    return ToJString(env, setting->GetValue(static_cast<bool>(needGlobal)));
 }
 
 void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject obj, jstring jkey,
@@ -220,27 +172,18 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject o
         return;
     }
 
-    setting->SetGlobal(true);
     setting->SetValue(GetJString(env, value));
 }
 
 jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsRuntimeModifiable(JNIEnv* env, jobject obj,
                                                                            jstring jkey) {
-    auto key = GetJString(env, jkey);
-    auto setting = Settings::values.linkage.by_key[key];
-    if (setting != 0) {
+    auto setting = getSetting<std::string>(env, jkey);
+    if (setting != nullptr) {
         return setting->RuntimeModfiable();
     }
-    LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key);
     return true;
 }
 
-jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getConfigHeader(JNIEnv* env, jobject obj,
-                                                                   jint jcategory) {
-    auto category = static_cast<Settings::Category>(jcategory);
-    return ToJString(env, Settings::TranslateCategory(category));
-}
-
 jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* env, jobject obj,
                                                                        jstring jkey) {
     auto setting = getSetting<std::string>(env, jkey);
@@ -254,6 +197,41 @@ jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* e
     return ToJString(env, setting->PairedSetting()->GetLabel());
 }
 
+jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsSwitchable(JNIEnv* env, jobject obj,
+                                                                    jstring jkey) {
+    auto setting = getSetting<std::string>(env, jkey);
+    if (setting != nullptr) {
+        return setting->Switchable();
+    }
+    return false;
+}
+
+jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_usingGlobal(JNIEnv* env, jobject obj,
+                                                                jstring jkey) {
+    auto setting = getSetting<std::string>(env, jkey);
+    if (setting != nullptr) {
+        return setting->UsingGlobal();
+    }
+    return true;
+}
+
+void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setGlobal(JNIEnv* env, jobject obj, jstring jkey,
+                                                          jboolean global) {
+    auto setting = getSetting<std::string>(env, jkey);
+    if (setting != nullptr) {
+        setting->SetGlobal(static_cast<bool>(global));
+    }
+}
+
+jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getDefaultToString(JNIEnv* env, jobject obj,
+                                                                      jstring jkey) {
+    auto setting = getSetting<std::string>(env, jkey);
+    if (setting != nullptr) {
+        return ToJString(env, setting->DefaultToString());
+    }
+    return ToJString(env, "");
+}
+
 jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getGameDirs(JNIEnv* env, jobject obj) {
     jclass gameDirClass = IDCache::GetGameDirClass();
     jmethodID gameDirConstructor = IDCache::GetGameDirConstructor();
diff --git a/src/common/settings_setting.h b/src/common/settings_setting.h
index 3175ab07d1..0b18ca5ecc 100644
--- a/src/common/settings_setting.h
+++ b/src/common/settings_setting.h
@@ -81,6 +81,9 @@ public:
     [[nodiscard]] virtual const Type& GetValue() const {
         return value;
     }
+    [[nodiscard]] virtual const Type& GetValue(bool need_global) const {
+        return value;
+    }
 
     /**
      * Sets the setting to the given value.
@@ -353,7 +356,7 @@ public:
         }
         return custom;
     }
-    [[nodiscard]] const Type& GetValue(bool need_global) const {
+    [[nodiscard]] const Type& GetValue(bool need_global) const override final {
         if (use_global || need_global) {
             return this->value;
         }

From 6b5fb2063f316e7eaf169d7c12c595ae7fbbcc2b Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Sun, 10 Dec 2023 20:12:05 -0500
Subject: [PATCH 02/20] frontend_common: Fix settings reload bug

This clears the touch_from_button_maps array before we read new data into it because this read duplicate data on a reload otherwise.
---
 src/frontend_common/config.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/frontend_common/config.cpp b/src/frontend_common/config.cpp
index 1a0491c2c1..d9f99148bc 100644
--- a/src/frontend_common/config.cpp
+++ b/src/frontend_common/config.cpp
@@ -214,6 +214,7 @@ void Config::ReadControlValues() {
 }
 
 void Config::ReadMotionTouchValues() {
+    Settings::values.touch_from_button_maps.clear();
     int num_touch_from_button_maps = BeginArray(std::string("touch_from_button_maps"));
 
     if (num_touch_from_button_maps > 0) {

From e975f3cde9d4dcb1d9e2bbce116f9a9ba99bf03f Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Sun, 10 Dec 2023 20:27:50 -0500
Subject: [PATCH 03/20] android: Add Game properties

This commit has the UI for viewing a game's properties on long-press and some links to useful tools like
- Game info
- Shortcut to settings (global in this commit)
- Addon manager with installer
- Save data manager
- Option to clear all save data
- Option to clear shader cache
---
 .../java/org/yuzu/yuzu_emu/NativeLibrary.kt   |  35 +-
 .../yuzu/yuzu_emu/adapters/AddonAdapter.kt    |  52 +++
 .../yuzu/yuzu_emu/adapters/AppletAdapter.kt   |   6 +-
 .../org/yuzu/yuzu_emu/adapters/GameAdapter.kt |  16 +-
 .../adapters/GamePropertiesAdapter.kt         | 133 ++++++
 .../yuzu/yuzu_emu/fragments/AddonsFragment.kt | 214 +++++++++
 .../ContentTypeSelectionDialogFragment.kt     |  68 +++
 .../yuzu_emu/fragments/GameInfoFragment.kt    | 148 +++++++
 .../fragments/GamePropertiesFragment.kt       | 418 ++++++++++++++++++
 .../IndeterminateProgressDialogFragment.kt    |   2 +-
 .../fragments/LaunchGameDialogFragment.kt     |  61 +++
 .../fragments/MessageDialogFragment.kt        |  37 +-
 .../yuzu/yuzu_emu/fragments/SearchFragment.kt |   4 +-
 .../java/org/yuzu/yuzu_emu/model/Addon.kt     |  10 +
 .../org/yuzu/yuzu_emu/model/AddonViewModel.kt |  83 ++++
 .../main/java/org/yuzu/yuzu_emu/model/Game.kt |  42 +-
 .../org/yuzu/yuzu_emu/model/GameProperties.kt |  34 ++
 .../org/yuzu/yuzu_emu/model/HomeViewModel.kt  |  15 +
 .../yuzu_emu/model/MessageDialogViewModel.kt  |   4 +-
 .../org/yuzu/yuzu_emu/model/TaskViewModel.kt  |   2 +-
 .../org/yuzu/yuzu_emu/ui/GamesFragment.kt     |  11 +-
 .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 363 +++++++--------
 .../java/org/yuzu/yuzu_emu/utils/AddonUtil.kt |   8 +
 .../java/org/yuzu/yuzu_emu/utils/FileUtil.kt  |  32 ++
 .../org/yuzu/yuzu_emu/utils/NativeConfig.kt   |  19 +
 src/android/app/src/main/jni/id_cache.cpp     |  40 ++
 src/android/app/src/main/jni/id_cache.h       |   6 +
 src/android/app/src/main/jni/native.cpp       | 108 ++++-
 src/android/app/src/main/jni/native.h         |   2 +
 .../app/src/main/jni/native_config.cpp        |  26 ++
 .../fragment_game_properties.xml              |  99 +++++
 .../src/main/res/layout/card_installable.xml  |   3 +-
 ...et_option.xml => card_simple_outlined.xml} |  20 +-
 .../src/main/res/layout/fragment_addons.xml   |  47 ++
 .../main/res/layout/fragment_game_info.xml    | 125 ++++++
 .../res/layout/fragment_game_properties.xml   |  86 ++++
 .../src/main/res/layout/list_item_addon.xml   |  57 +++
 .../main/res/navigation/home_navigation.xml   |  33 ++
 .../app/src/main/res/values/dimens.xml        |   2 +-
 .../app/src/main/res/values/strings.xml       |  45 ++
 40 files changed, 2245 insertions(+), 271 deletions(-)
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ContentTypeSelectionDialogFragment.kt
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddonUtil.kt
 create mode 100644 src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml
 rename src/android/app/src/main/res/layout/{card_applet_option.xml => card_simple_outlined.xml} (71%)
 create mode 100644 src/android/app/src/main/res/layout/fragment_addons.xml
 create mode 100644 src/android/app/src/main/res/layout/fragment_game_info.xml
 create mode 100644 src/android/app/src/main/res/layout/fragment_game_properties.xml
 create mode 100644 src/android/app/src/main/res/layout/list_item_addon.xml

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
index e0f01127c7..95b98798db 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
@@ -230,8 +230,6 @@ object NativeLibrary {
      */
     external fun onTouchReleased(finger_id: Int)
 
-    external fun initGameIni(gameID: String?)
-
     external fun setAppDirectory(directory: String)
 
     /**
@@ -241,6 +239,8 @@ object NativeLibrary {
      */
     external fun installFileToNand(filename: String, extension: String): Int
 
+    external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean
+
     external fun initializeGpuDriver(
         hookLibDir: String?,
         customDriverDir: String?,
@@ -252,18 +252,11 @@ object NativeLibrary {
 
     external fun initializeSystem(reload: Boolean)
 
-    external fun defaultCPUCore(): Int
-
     /**
      * Begins emulation.
      */
     external fun run(path: String?)
 
-    /**
-     * Begins emulation from the specified savestate.
-     */
-    external fun run(path: String?, savestatePath: String?, deleteSavestate: Boolean)
-
     // Surface Handling
     external fun surfaceChanged(surf: Surface?)
 
@@ -304,10 +297,9 @@ object NativeLibrary {
      */
     external fun getCpuBackend(): String
 
-    /**
-     * Notifies the core emulation that the orientation has changed.
-     */
-    external fun notifyOrientationChange(layout_option: Int, rotation: Int)
+    external fun applySettings()
+
+    external fun logSettings()
 
     enum class CoreError {
         ErrorSystemFiles,
@@ -538,6 +530,23 @@ object NativeLibrary {
      */
     external fun isFirmwareAvailable(): Boolean
 
+    /**
+     * Checks the PatchManager for any addons that are available
+     *
+     * @param path Path to game file. Can be a [Uri].
+     * @param programId String representation of a game's program ID
+     * @return Array of pairs where the first value is the name of an addon and the second is the version
+     */
+    external fun getAddonsForFile(path: String, programId: String): Array<Pair<String, String>>?
+
+    /**
+     * Gets the save location for a specific game
+     *
+     * @param programId String representation of a game's program ID
+     * @return Save data path that may not exist yet
+     */
+    external fun getSavePath(programId: String): String
+
     /**
      * Button type for use in onTouchEvent
      */
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt
new file mode 100644
index 0000000000..15c7ca3c98
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt
@@ -0,0 +1,52 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.adapters
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.recyclerview.widget.AsyncDifferConfig
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding
+import org.yuzu.yuzu_emu.model.Addon
+
+class AddonAdapter : ListAdapter<Addon, AddonAdapter.AddonViewHolder>(
+    AsyncDifferConfig.Builder(DiffCallback()).build()
+) {
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder {
+        ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+            .also { return AddonViewHolder(it) }
+    }
+
+    override fun getItemCount(): Int = currentList.size
+
+    override fun onBindViewHolder(holder: AddonViewHolder, position: Int) =
+        holder.bind(currentList[position])
+
+    inner class AddonViewHolder(val binding: ListItemAddonBinding) :
+        RecyclerView.ViewHolder(binding.root) {
+        fun bind(addon: Addon) {
+            binding.root.setOnClickListener {
+                binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked
+            }
+            binding.title.text = addon.title
+            binding.version.text = addon.version
+            binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
+                addon.enabled = checked
+            }
+            binding.addonSwitch.isChecked = addon.enabled
+        }
+    }
+
+    private class DiffCallback : DiffUtil.ItemCallback<Addon>() {
+        override fun areItemsTheSame(oldItem: Addon, newItem: Addon): Boolean {
+            return oldItem == newItem
+        }
+
+        override fun areContentsTheSame(oldItem: Addon, newItem: Addon): Boolean {
+            return oldItem == newItem
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt
index a21a705c16..4a05c5be94 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt
@@ -15,7 +15,7 @@ import org.yuzu.yuzu_emu.HomeNavigationDirections
 import org.yuzu.yuzu_emu.NativeLibrary
 import org.yuzu.yuzu_emu.R
 import org.yuzu.yuzu_emu.YuzuApplication
-import org.yuzu.yuzu_emu.databinding.CardAppletOptionBinding
+import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
 import org.yuzu.yuzu_emu.model.Applet
 import org.yuzu.yuzu_emu.model.AppletInfo
 import org.yuzu.yuzu_emu.model.Game
@@ -28,7 +28,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) :
         parent: ViewGroup,
         viewType: Int
     ): AppletAdapter.AppletViewHolder {
-        CardAppletOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+        CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false)
             .apply { root.setOnClickListener(this@AppletAdapter) }
             .also { return AppletViewHolder(it) }
     }
@@ -65,7 +65,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) :
         view.findNavController().navigate(action)
     }
 
-    inner class AppletViewHolder(val binding: CardAppletOptionBinding) :
+    inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) :
         RecyclerView.ViewHolder(binding.root) {
         lateinit var applet: Applet
 
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
index 2ef6385597..928bfe5a70 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
@@ -44,19 +44,20 @@ import org.yuzu.yuzu_emu.utils.GameIconUtils
 
 class GameAdapter(private val activity: AppCompatActivity) :
     ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
-    View.OnClickListener {
+    View.OnClickListener,
+    View.OnLongClickListener {
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
         // Create a new view.
         val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
         binding.cardGame.setOnClickListener(this)
+        binding.cardGame.setOnLongClickListener(this)
 
         // Use that view to create a ViewHolder.
         return GameViewHolder(binding)
     }
 
-    override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
+    override fun onBindViewHolder(holder: GameViewHolder, position: Int) =
         holder.bind(currentList[position])
-    }
 
     override fun getItemCount(): Int = currentList.size
 
@@ -125,10 +126,17 @@ class GameAdapter(private val activity: AppCompatActivity) :
             }
         }
 
-        val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game)
+        val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game, true)
         view.findNavController().navigate(action)
     }
 
+    override fun onLongClick(view: View): Boolean {
+        val holder = view.tag as GameViewHolder
+        val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(holder.game)
+        view.findNavController().navigate(action)
+        return true
+    }
+
     inner class GameViewHolder(val binding: CardGameBinding) :
         RecyclerView.ViewHolder(binding.root) {
         lateinit var game: Game
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt
new file mode 100644
index 0000000000..ff6270fa87
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt
@@ -0,0 +1,133 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.adapters
+
+import android.text.TextUtils
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.res.ResourcesCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.recyclerview.widget.RecyclerView
+import kotlinx.coroutines.launch
+import org.yuzu.yuzu_emu.databinding.CardInstallableBinding
+import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
+import org.yuzu.yuzu_emu.model.GameProperty
+import org.yuzu.yuzu_emu.model.InstallableProperty
+import org.yuzu.yuzu_emu.model.SubmenuProperty
+
+class GamePropertiesAdapter(
+    private val viewLifecycle: LifecycleOwner,
+    private var properties: List<GameProperty>
+) :
+    RecyclerView.Adapter<GamePropertiesAdapter.GamePropertyViewHolder>() {
+    override fun onCreateViewHolder(
+        parent: ViewGroup,
+        viewType: Int
+    ): GamePropertyViewHolder {
+        val inflater = LayoutInflater.from(parent.context)
+        return when (viewType) {
+            PropertyType.Submenu.ordinal -> {
+                SubmenuPropertyViewHolder(
+                    CardSimpleOutlinedBinding.inflate(
+                        inflater,
+                        parent,
+                        false
+                    )
+                )
+            }
+
+            else -> InstallablePropertyViewHolder(
+                CardInstallableBinding.inflate(
+                    inflater,
+                    parent,
+                    false
+                )
+            )
+        }
+    }
+
+    override fun getItemCount(): Int = properties.size
+
+    override fun onBindViewHolder(holder: GamePropertyViewHolder, position: Int) =
+        holder.bind(properties[position])
+
+    override fun getItemViewType(position: Int): Int {
+        return when (properties[position]) {
+            is SubmenuProperty -> PropertyType.Submenu.ordinal
+            else -> PropertyType.Installable.ordinal
+        }
+    }
+
+    sealed class GamePropertyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+        abstract fun bind(property: GameProperty)
+    }
+
+    inner class SubmenuPropertyViewHolder(val binding: CardSimpleOutlinedBinding) :
+        GamePropertyViewHolder(binding.root) {
+        override fun bind(property: GameProperty) {
+            val submenuProperty = property as SubmenuProperty
+
+            binding.root.setOnClickListener {
+                submenuProperty.action.invoke()
+            }
+
+            binding.title.setText(submenuProperty.titleId)
+            binding.description.setText(submenuProperty.descriptionId)
+            binding.icon.setImageDrawable(
+                ResourcesCompat.getDrawable(
+                    binding.icon.context.resources,
+                    submenuProperty.iconId,
+                    binding.icon.context.theme
+                )
+            )
+
+            binding.details.postDelayed({
+                binding.details.isSelected = true
+                binding.details.ellipsize = TextUtils.TruncateAt.MARQUEE
+            }, 3000)
+
+            if (submenuProperty.details != null) {
+                binding.details.visibility = View.VISIBLE
+                binding.details.text = submenuProperty.details.invoke()
+            } else if (submenuProperty.detailsFlow != null) {
+                binding.details.visibility = View.VISIBLE
+                viewLifecycle.lifecycleScope.launch {
+                    viewLifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+                        submenuProperty.detailsFlow.collect { binding.details.text = it }
+                    }
+                }
+            } else {
+                binding.details.visibility = View.GONE
+            }
+        }
+    }
+
+    inner class InstallablePropertyViewHolder(val binding: CardInstallableBinding) :
+        GamePropertyViewHolder(binding.root) {
+        override fun bind(property: GameProperty) {
+            val installableProperty = property as InstallableProperty
+
+            binding.title.setText(installableProperty.titleId)
+            binding.description.setText(installableProperty.descriptionId)
+
+            if (installableProperty.install != null) {
+                binding.buttonInstall.visibility = View.VISIBLE
+                binding.buttonInstall.setOnClickListener { installableProperty.install.invoke() }
+            }
+            if (installableProperty.export != null) {
+                binding.buttonExport.visibility = View.VISIBLE
+                binding.buttonExport.setOnClickListener { installableProperty.export.invoke() }
+            }
+        }
+    }
+
+    enum class PropertyType {
+        Submenu,
+        Installable
+    }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
new file mode 100644
index 0000000000..0dce8ad8dc
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
@@ -0,0 +1,214 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.documentfile.provider.DocumentFile
+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.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.transition.MaterialSharedAxis
+import kotlinx.coroutines.launch
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.adapters.AddonAdapter
+import org.yuzu.yuzu_emu.databinding.FragmentAddonsBinding
+import org.yuzu.yuzu_emu.model.AddonViewModel
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.utils.AddonUtil
+import org.yuzu.yuzu_emu.utils.FileUtil.copyFilesTo
+import java.io.File
+
+class AddonsFragment : Fragment() {
+    private var _binding: FragmentAddonsBinding? = null
+    private val binding get() = _binding!!
+
+    private val homeViewModel: HomeViewModel by activityViewModels()
+    private val addonViewModel: AddonViewModel by activityViewModels()
+
+    private val args by navArgs<AddonsFragmentArgs>()
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        addonViewModel.onOpenAddons(args.game)
+        enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
+        returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+        reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        _binding = FragmentAddonsBinding.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 = false)
+        homeViewModel.setStatusBarShadeVisibility(false)
+
+        binding.toolbarAddons.setNavigationOnClickListener {
+            binding.root.findNavController().popBackStack()
+        }
+
+        binding.toolbarAddons.title = getString(R.string.addons_game, args.game.title)
+
+        binding.listAddons.apply {
+            layoutManager = LinearLayoutManager(requireContext())
+            adapter = AddonAdapter()
+        }
+
+        viewLifecycleOwner.lifecycleScope.apply {
+            launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    addonViewModel.addonList.collect {
+                        (binding.listAddons.adapter as AddonAdapter).submitList(it)
+                    }
+                }
+            }
+            launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    addonViewModel.showModInstallPicker.collect {
+                        if (it) {
+                            installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
+                            addonViewModel.showModInstallPicker(false)
+                        }
+                    }
+                }
+            }
+            launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    addonViewModel.showModNoticeDialog.collect {
+                        if (it) {
+                            MessageDialogFragment.newInstance(
+                                requireActivity(),
+                                titleId = R.string.addon_notice,
+                                descriptionId = R.string.addon_notice_description,
+                                positiveAction = { addonViewModel.showModInstallPicker(true) }
+                            ).show(parentFragmentManager, MessageDialogFragment.TAG)
+                            addonViewModel.showModNoticeDialog(false)
+                        }
+                    }
+                }
+            }
+        }
+
+        binding.buttonInstall.setOnClickListener {
+            ContentTypeSelectionDialogFragment().show(
+                parentFragmentManager,
+                ContentTypeSelectionDialogFragment.TAG
+            )
+        }
+
+        setInsets()
+    }
+
+    override fun onResume() {
+        super.onResume()
+        addonViewModel.refreshAddons()
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        addonViewModel.onCloseAddons()
+    }
+
+    val installAddon =
+        registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
+            if (result == null) {
+                return@registerForActivityResult
+            }
+
+            val externalAddonDirectory = DocumentFile.fromTreeUri(requireContext(), result)
+            if (externalAddonDirectory == null) {
+                MessageDialogFragment.newInstance(
+                    requireActivity(),
+                    titleId = R.string.invalid_directory,
+                    descriptionId = R.string.invalid_directory_description
+                ).show(parentFragmentManager, MessageDialogFragment.TAG)
+                return@registerForActivityResult
+            }
+
+            val isValid = externalAddonDirectory.listFiles()
+                .any { AddonUtil.validAddonDirectories.contains(it.name) }
+            val errorMessage = MessageDialogFragment.newInstance(
+                requireActivity(),
+                titleId = R.string.invalid_directory,
+                descriptionId = R.string.invalid_directory_description
+            )
+            if (isValid) {
+                IndeterminateProgressDialogFragment.newInstance(
+                    requireActivity(),
+                    R.string.installing_game_content,
+                    false
+                ) {
+                    val parentDirectoryName = externalAddonDirectory.name
+                    val internalAddonDirectory =
+                        File(args.game.addonDir + parentDirectoryName)
+                    try {
+                        externalAddonDirectory.copyFilesTo(internalAddonDirectory)
+                    } catch (_: Exception) {
+                        return@newInstance errorMessage
+                    }
+                    addonViewModel.refreshAddons()
+                    return@newInstance getString(R.string.addon_installed_successfully)
+                }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
+            } else {
+                errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG)
+            }
+        }
+
+    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 mlpToolbar = binding.toolbarAddons.layoutParams as ViewGroup.MarginLayoutParams
+            mlpToolbar.leftMargin = leftInsets
+            mlpToolbar.rightMargin = rightInsets
+            binding.toolbarAddons.layoutParams = mlpToolbar
+
+            val mlpAddonsList = binding.listAddons.layoutParams as ViewGroup.MarginLayoutParams
+            mlpAddonsList.leftMargin = leftInsets
+            mlpAddonsList.rightMargin = rightInsets
+            binding.listAddons.layoutParams = mlpAddonsList
+            binding.listAddons.updatePadding(
+                bottom = barInsets.bottom +
+                    resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
+            )
+
+            val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
+            val mlpFab =
+                binding.buttonInstall.layoutParams as ViewGroup.MarginLayoutParams
+            mlpFab.leftMargin = leftInsets + fabSpacing
+            mlpFab.rightMargin = rightInsets + fabSpacing
+            mlpFab.bottomMargin = barInsets.bottom + fabSpacing
+            binding.buttonInstall.layoutParams = mlpFab
+
+            windowInsets
+        }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ContentTypeSelectionDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ContentTypeSelectionDialogFragment.kt
new file mode 100644
index 0000000000..c1d8b9ea5f
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ContentTypeSelectionDialogFragment.kt
@@ -0,0 +1,68 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import androidx.preference.PreferenceManager
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.model.AddonViewModel
+import org.yuzu.yuzu_emu.ui.main.MainActivity
+
+class ContentTypeSelectionDialogFragment : DialogFragment() {
+    private val addonViewModel: AddonViewModel by activityViewModels()
+
+    private val preferences get() =
+        PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+
+    private var selectedItem = 0
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        val launchOptions =
+            arrayOf(getString(R.string.updates_and_dlc), getString(R.string.mods_and_cheats))
+
+        if (savedInstanceState != null) {
+            selectedItem = savedInstanceState.getInt(SELECTED_ITEM)
+        }
+
+        val mainActivity = requireActivity() as MainActivity
+        return MaterialAlertDialogBuilder(requireContext())
+            .setTitle(R.string.select_content_type)
+            .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
+                when (selectedItem) {
+                    0 -> mainActivity.installGameUpdate.launch(arrayOf("*/*"))
+                    else -> {
+                        if (!preferences.getBoolean(MOD_NOTICE_SEEN, false)) {
+                            preferences.edit().putBoolean(MOD_NOTICE_SEEN, true).apply()
+                            addonViewModel.showModNoticeDialog(true)
+                            return@setPositiveButton
+                        }
+                        addonViewModel.showModInstallPicker(true)
+                    }
+                }
+            }
+            .setSingleChoiceItems(launchOptions, 0) { _: DialogInterface, i: Int ->
+                selectedItem = i
+            }
+            .setNegativeButton(android.R.string.cancel, null)
+            .show()
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        super.onSaveInstanceState(outState)
+        outState.putInt(SELECTED_ITEM, selectedItem)
+    }
+
+    companion object {
+        const val TAG = "ContentTypeSelectionDialogFragment"
+
+        private const val SELECTED_ITEM = "SelectedItem"
+        private const val MOD_NOTICE_SEEN = "ModNoticeSeen"
+    }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt
new file mode 100644
index 0000000000..fa2a4c9f9b
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt
@@ -0,0 +1,148 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+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.navigation.findNavController
+import androidx.navigation.fragment.navArgs
+import com.google.android.material.transition.MaterialSharedAxis
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.FragmentGameInfoBinding
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.utils.GameMetadata
+
+class GameInfoFragment : Fragment() {
+    private var _binding: FragmentGameInfoBinding? = null
+    private val binding get() = _binding!!
+
+    private val homeViewModel: HomeViewModel by activityViewModels()
+
+    private val args by navArgs<GameInfoFragmentArgs>()
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
+        returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+        reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+
+        // Check for an up-to-date version string
+        args.game.version = GameMetadata.getVersion(args.game.path, true)
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        _binding = FragmentGameInfoBinding.inflate(inflater)
+        return binding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        homeViewModel.setNavigationVisibility(visible = false, animated = false)
+        homeViewModel.setStatusBarShadeVisibility(false)
+
+        binding.apply {
+            toolbarInfo.title = args.game.title
+            toolbarInfo.setNavigationOnClickListener {
+                view.findNavController().popBackStack()
+            }
+
+            val pathString = Uri.parse(args.game.path).path ?: ""
+            path.setHint(R.string.path)
+            pathField.setText(pathString)
+            pathField.setOnClickListener { copyToClipboard(getString(R.string.path), pathString) }
+
+            programId.setHint(R.string.program_id)
+            programIdField.setText(args.game.programIdHex)
+            programIdField.setOnClickListener {
+                copyToClipboard(getString(R.string.program_id), args.game.programIdHex)
+            }
+
+            if (args.game.developer.isNotEmpty()) {
+                developer.setHint(R.string.developer)
+                developerField.setText(args.game.developer)
+                developerField.setOnClickListener {
+                    copyToClipboard(getString(R.string.developer), args.game.developer)
+                }
+            } else {
+                developer.visibility = View.GONE
+            }
+
+            version.setHint(R.string.version)
+            versionField.setText(args.game.version)
+            versionField.setOnClickListener {
+                copyToClipboard(getString(R.string.version), args.game.version)
+            }
+
+            buttonCopy.setOnClickListener {
+                val details = """
+                    ${args.game.title}
+                    ${getString(R.string.path)} - $pathString
+                    ${getString(R.string.program_id)} - ${args.game.programIdHex}
+                    ${getString(R.string.developer)} - ${args.game.developer}
+                    ${getString(R.string.version)} - ${args.game.version}
+                """.trimIndent()
+                copyToClipboard(args.game.title, details)
+            }
+        }
+
+        setInsets()
+    }
+
+    private fun copyToClipboard(label: String, body: String) {
+        val clipBoard =
+            requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+        val clip = ClipData.newPlainText(label, body)
+        clipBoard.setPrimaryClip(clip)
+
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+            Toast.makeText(
+                requireContext(),
+                R.string.copied_to_clipboard,
+                Toast.LENGTH_SHORT
+            ).show()
+        }
+    }
+
+    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 mlpToolbar = binding.toolbarInfo.layoutParams as ViewGroup.MarginLayoutParams
+            mlpToolbar.leftMargin = leftInsets
+            mlpToolbar.rightMargin = rightInsets
+            binding.toolbarInfo.layoutParams = mlpToolbar
+
+            val mlpScrollAbout = binding.scrollInfo.layoutParams as ViewGroup.MarginLayoutParams
+            mlpScrollAbout.leftMargin = leftInsets
+            mlpScrollAbout.rightMargin = rightInsets
+            binding.scrollInfo.layoutParams = mlpScrollAbout
+
+            binding.contentInfo.updatePadding(bottom = barInsets.bottom)
+
+            windowInsets
+        }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
new file mode 100644
index 0000000000..485989e2e3
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
@@ -0,0 +1,418 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+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.navigation.fragment.navArgs
+import androidx.recyclerview.widget.GridLayoutManager
+import com.google.android.material.transition.MaterialSharedAxis
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.yuzu.yuzu_emu.HomeNavigationDirections
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.adapters.GamePropertiesAdapter
+import org.yuzu.yuzu_emu.databinding.FragmentGamePropertiesBinding
+import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.model.DriverViewModel
+import org.yuzu.yuzu_emu.model.GameProperty
+import org.yuzu.yuzu_emu.model.GamesViewModel
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.model.InstallableProperty
+import org.yuzu.yuzu_emu.model.SubmenuProperty
+import org.yuzu.yuzu_emu.model.TaskState
+import org.yuzu.yuzu_emu.utils.DirectoryInitialization
+import org.yuzu.yuzu_emu.utils.FileUtil
+import org.yuzu.yuzu_emu.utils.GameIconUtils
+import org.yuzu.yuzu_emu.utils.GpuDriverHelper
+import org.yuzu.yuzu_emu.utils.MemoryUtil
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.io.File
+
+class GamePropertiesFragment : Fragment() {
+    private var _binding: FragmentGamePropertiesBinding? = null
+    private val binding get() = _binding!!
+
+    private val homeViewModel: HomeViewModel by activityViewModels()
+    private val gamesViewModel: GamesViewModel by activityViewModels()
+    private val driverViewModel: DriverViewModel by activityViewModels()
+
+    private val args by navArgs<GamePropertiesFragmentArgs>()
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        enterTransition = MaterialSharedAxis(MaterialSharedAxis.Y, true)
+        returnTransition = MaterialSharedAxis(MaterialSharedAxis.Y, false)
+        reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        _binding = FragmentGamePropertiesBinding.inflate(layoutInflater)
+        return binding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        homeViewModel.setNavigationVisibility(visible = false, animated = true)
+        homeViewModel.setStatusBarShadeVisibility(true)
+
+        binding.buttonBack.setOnClickListener {
+            view.findNavController().popBackStack()
+        }
+
+        GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen)
+        binding.title.text = args.game.title
+        binding.title.postDelayed(
+            {
+                binding.title.ellipsize = TextUtils.TruncateAt.MARQUEE
+                binding.title.isSelected = true
+            },
+            3000
+        )
+
+        binding.buttonStart.setOnClickListener {
+            LaunchGameDialogFragment.newInstance(args.game)
+                .show(childFragmentManager, LaunchGameDialogFragment.TAG)
+        }
+
+        reloadList()
+
+        viewLifecycleOwner.lifecycleScope.launch {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                homeViewModel.openImportSaves.collect {
+                    if (it) {
+                        importSaves.launch(arrayOf("application/zip"))
+                        homeViewModel.setOpenImportSaves(false)
+                    }
+                }
+            }
+        }
+
+        setInsets()
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        gamesViewModel.reloadGames(true)
+    }
+
+    private fun reloadList() {
+        _binding ?: return
+
+        driverViewModel.updateDriverNameForGame(args.game)
+        val properties = mutableListOf<GameProperty>().apply {
+            add(
+                SubmenuProperty(
+                    R.string.info,
+                    R.string.info_description,
+                    R.drawable.ic_info_outline
+                ) {
+                    val action = GamePropertiesFragmentDirections
+                        .actionPerGamePropertiesFragmentToGameInfoFragment(args.game)
+                    binding.root.findNavController().navigate(action)
+                }
+            )
+            add(
+                SubmenuProperty(
+                    R.string.preferences_settings,
+                    R.string.per_game_settings_description,
+                    R.drawable.ic_settings
+                ) {
+                    val action = HomeNavigationDirections.actionGlobalSettingsActivity(
+                        args.game,
+                        Settings.MenuTag.SECTION_ROOT
+                    )
+                    binding.root.findNavController().navigate(action)
+                }
+            )
+
+            if (!args.game.isHomebrew) {
+                add(
+                    SubmenuProperty(
+                        R.string.add_ons,
+                        R.string.add_ons_description,
+                        R.drawable.ic_edit
+                    ) {
+                        val action = GamePropertiesFragmentDirections
+                            .actionPerGamePropertiesFragmentToAddonsFragment(args.game)
+                        binding.root.findNavController().navigate(action)
+                    }
+                )
+                add(
+                    InstallableProperty(
+                        R.string.save_data,
+                        R.string.save_data_description,
+                        {
+                            MessageDialogFragment.newInstance(
+                                requireActivity(),
+                                titleId = R.string.import_save_warning,
+                                descriptionId = R.string.import_save_warning_description,
+                                positiveAction = { homeViewModel.setOpenImportSaves(true) }
+                            ).show(parentFragmentManager, MessageDialogFragment.TAG)
+                        },
+                        if (File(args.game.saveDir).exists()) {
+                            { exportSaves.launch(args.game.saveZipName) }
+                        } else {
+                            null
+                        }
+                    )
+                )
+
+                val saveDirFile = File(args.game.saveDir)
+                if (saveDirFile.exists()) {
+                    add(
+                        SubmenuProperty(
+                            R.string.delete_save_data,
+                            R.string.delete_save_data_description,
+                            R.drawable.ic_delete,
+                            action = {
+                                MessageDialogFragment.newInstance(
+                                    requireActivity(),
+                                    titleId = R.string.delete_save_data,
+                                    descriptionId = R.string.delete_save_data_warning_description,
+                                    positiveAction = {
+                                        File(args.game.saveDir).deleteRecursively()
+                                        Toast.makeText(
+                                            YuzuApplication.appContext,
+                                            R.string.save_data_deleted_successfully,
+                                            Toast.LENGTH_SHORT
+                                        ).show()
+                                        reloadList()
+                                    }
+                                ).show(parentFragmentManager, MessageDialogFragment.TAG)
+                            }
+                        )
+                    )
+                }
+
+                val shaderCacheDir = File(
+                    DirectoryInitialization.userDirectory +
+                        "/shader/" + args.game.settingsName.lowercase()
+                )
+                if (shaderCacheDir.exists()) {
+                    add(
+                        SubmenuProperty(
+                            R.string.clear_shader_cache,
+                            R.string.clear_shader_cache_description,
+                            R.drawable.ic_delete,
+                            {
+                                if (shaderCacheDir.exists()) {
+                                    val bytes = shaderCacheDir.walkTopDown().filter { it.isFile }
+                                        .map { it.length() }.sum()
+                                    MemoryUtil.bytesToSizeUnit(bytes.toFloat())
+                                } else {
+                                    MemoryUtil.bytesToSizeUnit(0f)
+                                }
+                            }
+                        ) {
+                            shaderCacheDir.deleteRecursively()
+                            Toast.makeText(
+                                YuzuApplication.appContext,
+                                R.string.cleared_shaders_successfully,
+                                Toast.LENGTH_SHORT
+                            ).show()
+                            reloadList()
+                        }
+                    )
+                }
+            }
+        }
+        binding.listProperties.apply {
+            layoutManager =
+                GridLayoutManager(requireContext(), resources.getInteger(R.integer.grid_columns))
+            adapter = GamePropertiesAdapter(viewLifecycleOwner, properties)
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        driverViewModel.updateDriverNameForGame(args.game)
+    }
+
+    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 smallLayout = resources.getBoolean(R.bool.small_layout)
+            if (smallLayout) {
+                val mlpListAll =
+                    binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
+                mlpListAll.leftMargin = leftInsets
+                mlpListAll.rightMargin = rightInsets
+                binding.listAll.layoutParams = mlpListAll
+            } else {
+                if (ViewCompat.getLayoutDirection(binding.root) ==
+                    ViewCompat.LAYOUT_DIRECTION_LTR
+                ) {
+                    val mlpListAll =
+                        binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
+                    mlpListAll.rightMargin = rightInsets
+                    binding.listAll.layoutParams = mlpListAll
+
+                    val mlpIconLayout =
+                        binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams
+                    mlpIconLayout.topMargin = barInsets.top
+                    mlpIconLayout.leftMargin = leftInsets
+                    binding.iconLayout!!.layoutParams = mlpIconLayout
+                } else {
+                    val mlpListAll =
+                        binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
+                    mlpListAll.leftMargin = leftInsets
+                    binding.listAll.layoutParams = mlpListAll
+
+                    val mlpIconLayout =
+                        binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams
+                    mlpIconLayout.topMargin = barInsets.top
+                    mlpIconLayout.rightMargin = rightInsets
+                    binding.iconLayout!!.layoutParams = mlpIconLayout
+                }
+            }
+
+            val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
+            val mlpFab =
+                binding.buttonStart.layoutParams as ViewGroup.MarginLayoutParams
+            mlpFab.leftMargin = leftInsets + fabSpacing
+            mlpFab.rightMargin = rightInsets + fabSpacing
+            mlpFab.bottomMargin = barInsets.bottom + fabSpacing
+            binding.buttonStart.layoutParams = mlpFab
+
+            binding.layoutAll.updatePadding(
+                top = barInsets.top,
+                bottom = barInsets.bottom +
+                    resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
+            )
+
+            windowInsets
+        }
+
+    private val importSaves =
+        registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+            if (result == null) {
+                return@registerForActivityResult
+            }
+
+            val inputZip = requireContext().contentResolver.openInputStream(result)
+            val savesFolder = File(args.game.saveDir)
+            val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
+            cacheSaveDir.mkdir()
+
+            if (inputZip == null) {
+                Toast.makeText(
+                    YuzuApplication.appContext,
+                    getString(R.string.fatal_error),
+                    Toast.LENGTH_LONG
+                ).show()
+                return@registerForActivityResult
+            }
+
+            IndeterminateProgressDialogFragment.newInstance(
+                requireActivity(),
+                R.string.save_files_importing,
+                false
+            ) {
+                try {
+                    FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
+                    val files = cacheSaveDir.listFiles()
+                    var savesFolderFile: File? = null
+                    if (files != null) {
+                        val savesFolderName = args.game.programIdHex
+                        for (file in files) {
+                            if (file.isDirectory && file.name == savesFolderName) {
+                                savesFolderFile = file
+                                break
+                            }
+                        }
+                    }
+
+                    if (savesFolderFile != null) {
+                        savesFolder.deleteRecursively()
+                        savesFolder.mkdir()
+                        savesFolderFile.copyRecursively(savesFolder)
+                        savesFolderFile.deleteRecursively()
+                    }
+
+                    withContext(Dispatchers.Main) {
+                        if (savesFolderFile == null) {
+                            MessageDialogFragment.newInstance(
+                                requireActivity(),
+                                titleId = R.string.save_file_invalid_zip_structure,
+                                descriptionId = R.string.save_file_invalid_zip_structure_description
+                            ).show(parentFragmentManager, MessageDialogFragment.TAG)
+                            return@withContext
+                        }
+                        Toast.makeText(
+                            YuzuApplication.appContext,
+                            getString(R.string.save_file_imported_success),
+                            Toast.LENGTH_LONG
+                        ).show()
+                        reloadList()
+                    }
+
+                    cacheSaveDir.deleteRecursively()
+                } catch (e: Exception) {
+                    Toast.makeText(
+                        YuzuApplication.appContext,
+                        getString(R.string.fatal_error),
+                        Toast.LENGTH_LONG
+                    ).show()
+                }
+            }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
+        }
+
+    /**
+     * Exports the save file located in the given folder path by creating a zip file and opening a
+     * file picker to save.
+     */
+    private val exportSaves = registerForActivityResult(
+        ActivityResultContracts.CreateDocument("application/zip")
+    ) { result ->
+        if (result == null) {
+            return@registerForActivityResult
+        }
+
+        IndeterminateProgressDialogFragment.newInstance(
+            requireActivity(),
+            R.string.save_files_exporting,
+            false
+        ) {
+            val saveLocation = args.game.saveDir
+            val zipResult = FileUtil.zipFromInternalStorage(
+                File(saveLocation),
+                saveLocation.replaceAfterLast("/", ""),
+                BufferedOutputStream(requireContext().contentResolver.openOutputStream(result))
+            )
+            return@newInstance when (zipResult) {
+                TaskState.Completed -> getString(R.string.export_success)
+                TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
+            }
+        }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
+    }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
index 7e467814d9..8847e5531a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
@@ -122,7 +122,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
             activity: FragmentActivity,
             titleId: Int,
             cancellable: Boolean = false,
-            task: () -> Any
+            task: suspend () -> Any
         ): IndeterminateProgressDialogFragment {
             val dialog = IndeterminateProgressDialogFragment()
             val args = Bundle()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt
new file mode 100644
index 0000000000..f653826a61
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt
@@ -0,0 +1,61 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import androidx.navigation.fragment.findNavController
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.HomeNavigationDirections
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.model.Game
+import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
+
+class LaunchGameDialogFragment : DialogFragment() {
+    private var selectedItem = 0
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        val game = requireArguments().parcelable<Game>(GAME)
+        val launchOptions = arrayOf(getString(R.string.global), getString(R.string.custom))
+
+        if (savedInstanceState != null) {
+            selectedItem = savedInstanceState.getInt(SELECTED_ITEM)
+        }
+
+        return MaterialAlertDialogBuilder(requireContext())
+            .setTitle(R.string.launch_options)
+            .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
+                val action = HomeNavigationDirections
+                    .actionGlobalEmulationActivity(game, selectedItem != 0)
+                requireParentFragment().findNavController().navigate(action)
+            }
+            .setSingleChoiceItems(launchOptions, 0) { _: DialogInterface, i: Int ->
+                selectedItem = i
+            }
+            .setNegativeButton(android.R.string.cancel, null)
+            .show()
+    }
+
+    override fun onSaveInstanceState(outState: Bundle) {
+        super.onSaveInstanceState(outState)
+        outState.putInt(SELECTED_ITEM, selectedItem)
+    }
+
+    companion object {
+        const val TAG = "LaunchGameDialogFragment"
+
+        const val GAME = "Game"
+        const val SELECTED_ITEM = "SelectedItem"
+
+        fun newInstance(game: Game): LaunchGameDialogFragment {
+            val args = Bundle()
+            args.putParcelable(GAME, game)
+            val fragment = LaunchGameDialogFragment()
+            fragment.arguments = args
+            return fragment
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
index a6183d19eb..32062b6fee 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
@@ -27,30 +27,31 @@ class MessageDialogFragment : DialogFragment() {
         val descriptionString = requireArguments().getString(DESCRIPTION_STRING)!!
         val helpLinkId = requireArguments().getInt(HELP_LINK)
 
-        val dialog = MaterialAlertDialogBuilder(requireContext())
-            .setPositiveButton(R.string.close, null)
+        val builder = MaterialAlertDialogBuilder(requireContext())
 
-        if (titleId != 0) dialog.setTitle(titleId)
-        if (titleString.isNotEmpty()) dialog.setTitle(titleString)
+        if (messageDialogViewModel.positiveAction == null) {
+            builder.setPositiveButton(R.string.close, null)
+        } else {
+            builder.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
+                messageDialogViewModel.positiveAction?.invoke()
+            }.setNegativeButton(android.R.string.cancel, null)
+        }
+
+        if (titleId != 0) builder.setTitle(titleId)
+        if (titleString.isNotEmpty()) builder.setTitle(titleString)
 
         if (descriptionId != 0) {
-            dialog.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY))
+            builder.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY))
         }
-        if (descriptionString.isNotEmpty()) dialog.setMessage(descriptionString)
+        if (descriptionString.isNotEmpty()) builder.setMessage(descriptionString)
 
         if (helpLinkId != 0) {
-            dialog.setNeutralButton(R.string.learn_more) { _, _ ->
+            builder.setNeutralButton(R.string.learn_more) { _, _ ->
                 openLink(getString(helpLinkId))
             }
         }
 
-        return dialog.show()
-    }
-
-    override fun onDismiss(dialog: DialogInterface) {
-        super.onDismiss(dialog)
-        messageDialogViewModel.dismissAction.invoke()
-        messageDialogViewModel.clear()
+        return builder.show()
     }
 
     private fun openLink(link: String) {
@@ -74,7 +75,7 @@ class MessageDialogFragment : DialogFragment() {
             descriptionId: Int = 0,
             descriptionString: String = "",
             helpLinkId: Int = 0,
-            dismissAction: () -> Unit = {}
+            positiveAction: (() -> Unit)? = null
         ): MessageDialogFragment {
             val dialog = MessageDialogFragment()
             val bundle = Bundle()
@@ -85,8 +86,10 @@ class MessageDialogFragment : DialogFragment() {
                 putString(DESCRIPTION_STRING, descriptionString)
                 putInt(HELP_LINK, helpLinkId)
             }
-            ViewModelProvider(activity)[MessageDialogViewModel::class.java].dismissAction =
-                dismissAction
+            ViewModelProvider(activity)[MessageDialogViewModel::class.java].apply {
+                clear()
+                this.positiveAction = positiveAction
+            }
             dialog.arguments = bundle
             return dialog
         }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
index 2dbca76a59..3ac054d8fa 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
@@ -60,7 +60,9 @@ class SearchFragment : Fragment() {
     // This is using the correct scope, lint is just acting up
     @SuppressLint("UnsafeRepeatOnLifecycleDetector")
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
-        homeViewModel.setNavigationVisibility(visible = true, animated = false)
+        super.onViewCreated(view, savedInstanceState)
+        homeViewModel.setNavigationVisibility(visible = true, animated = true)
+        homeViewModel.setStatusBarShadeVisibility(true)
         preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
 
         if (savedInstanceState != null) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt
new file mode 100644
index 0000000000..ed79a8b028
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt
@@ -0,0 +1,10 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+data class Addon(
+    var enabled: Boolean,
+    val title: String,
+    val version: String
+)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt
new file mode 100644
index 0000000000..075252f5bc
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt
@@ -0,0 +1,83 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.utils.NativeConfig
+import java.util.concurrent.atomic.AtomicBoolean
+
+class AddonViewModel : ViewModel() {
+    private val _addonList = MutableStateFlow(mutableListOf<Addon>())
+    val addonList get() = _addonList.asStateFlow()
+
+    private val _showModInstallPicker = MutableStateFlow(false)
+    val showModInstallPicker get() = _showModInstallPicker.asStateFlow()
+
+    private val _showModNoticeDialog = MutableStateFlow(false)
+    val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow()
+
+    var game: Game? = null
+
+    private val isRefreshing = AtomicBoolean(false)
+
+    fun onOpenAddons(game: Game) {
+        this.game = game
+        refreshAddons()
+    }
+
+    fun refreshAddons() {
+        if (isRefreshing.get() || game == null) {
+            return
+        }
+        isRefreshing.set(true)
+        viewModelScope.launch {
+            withContext(Dispatchers.IO) {
+                val addonList = mutableListOf<Addon>()
+                val disabledAddons = NativeConfig.getDisabledAddons(game!!.programId)
+                NativeLibrary.getAddonsForFile(game!!.path, game!!.programId)?.forEach {
+                    val name = it.first.replace("[D] ", "")
+                    addonList.add(Addon(!disabledAddons.contains(name), name, it.second))
+                }
+                addonList.sortBy { it.title }
+                _addonList.value = addonList
+                isRefreshing.set(false)
+            }
+        }
+    }
+
+    fun onCloseAddons() {
+        if (_addonList.value.isEmpty()) {
+            return
+        }
+
+        NativeConfig.setDisabledAddons(
+            game!!.programId,
+            _addonList.value.mapNotNull {
+                if (it.enabled) {
+                    null
+                } else {
+                    it.title
+                }
+            }.toTypedArray()
+        )
+        NativeConfig.saveGlobalConfig()
+        _addonList.value.clear()
+        game = null
+    }
+
+    fun showModInstallPicker(install: Boolean) {
+        _showModInstallPicker.value = install
+    }
+
+    fun showModNoticeDialog(show: Boolean) {
+        _showModNoticeDialog.value = show
+    }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
index 2fa3ab31bb..ac642c16e4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
@@ -3,10 +3,18 @@
 
 package org.yuzu.yuzu_emu.model
 
+import android.net.Uri
 import android.os.Parcelable
 import java.util.HashSet
 import kotlinx.parcelize.Parcelize
 import kotlinx.serialization.Serializable
+import org.yuzu.yuzu_emu.NativeLibrary
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.utils.DirectoryInitialization
+import org.yuzu.yuzu_emu.utils.FileUtil
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
 
 @Parcelize
 @Serializable
@@ -15,12 +23,44 @@ class Game(
     val path: String,
     val programId: String = "",
     val developer: String = "",
-    val version: String = "",
+    var version: String = "",
     val isHomebrew: Boolean = false
 ) : Parcelable {
     val keyAddedToLibraryTime get() = "${path}_AddedToLibraryTime"
     val keyLastPlayedTime get() = "${path}_LastPlayed"
 
+    val settingsName: String
+        get() {
+            val programIdLong = programId.toLong()
+            return if (programIdLong == 0L) {
+                FileUtil.getFilename(Uri.parse(path))
+            } else {
+                "0" + programIdLong.toString(16).uppercase()
+            }
+        }
+
+    val programIdHex: String
+        get() {
+            val programIdLong = programId.toLong()
+            return if (programIdLong == 0L) {
+                "0"
+            } else {
+                "0" + programIdLong.toString(16).uppercase()
+            }
+        }
+
+    val saveZipName: String
+        get() = "$title ${YuzuApplication.appContext.getString(R.string.save_data).lowercase()} - ${
+        LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
+        }.zip"
+
+    val saveDir: String
+        get() = DirectoryInitialization.userDirectory + "/nand" +
+            NativeLibrary.getSavePath(programId)
+
+    val addonDir: String
+        get() = DirectoryInitialization.userDirectory + "/load/" + programIdHex + "/"
+
     override fun equals(other: Any?): Boolean {
         if (other !is Game) {
             return false
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt
new file mode 100644
index 0000000000..bb3df5bd00
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt
@@ -0,0 +1,34 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import kotlinx.coroutines.flow.StateFlow
+
+interface GameProperty {
+    @get:StringRes
+    val titleId: Int
+        get() = -1
+
+    @get:StringRes
+    val descriptionId: Int
+        get() = -1
+}
+
+data class SubmenuProperty(
+    override val titleId: Int,
+    override val descriptionId: Int,
+    @DrawableRes val iconId: Int,
+    val details: (() -> String)? = null,
+    val detailsFlow: StateFlow<String>? = null,
+    val action: () -> Unit
+) : GameProperty
+
+data class InstallableProperty(
+    override val titleId: Int,
+    override val descriptionId: Int,
+    val install: (() -> Unit)? = null,
+    val export: (() -> Unit)? = null
+) : GameProperty
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
index 07e65b028d..d801db1054 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
@@ -3,6 +3,7 @@
 
 package org.yuzu.yuzu_emu.model
 
+import android.net.Uri
 import androidx.lifecycle.ViewModel
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
@@ -21,6 +22,12 @@ class HomeViewModel : ViewModel() {
     private val _gamesDirSelected = MutableStateFlow(false)
     val gamesDirSelected get() = _gamesDirSelected.asStateFlow()
 
+    private val _openImportSaves = MutableStateFlow(false)
+    val openImportSaves get() = _openImportSaves.asStateFlow()
+
+    private val _contentToInstall = MutableStateFlow<List<Uri>?>(null)
+    val contentToInstall get() = _contentToInstall.asStateFlow()
+
     var navigatedToSetup = false
 
     fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
@@ -44,4 +51,12 @@ class HomeViewModel : ViewModel() {
     fun setGamesDirSelected(selected: Boolean) {
         _gamesDirSelected.value = selected
     }
+
+    fun setOpenImportSaves(import: Boolean) {
+        _openImportSaves.value = import
+    }
+
+    fun setContentToInstall(documents: List<Uri>?) {
+        _contentToInstall.value = documents
+    }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt
index 36ffd08d28..641c5cb177 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt
@@ -6,9 +6,9 @@ package org.yuzu.yuzu_emu.model
 import androidx.lifecycle.ViewModel
 
 class MessageDialogViewModel : ViewModel() {
-    var dismissAction: () -> Unit = {}
+    var positiveAction: (() -> Unit)? = null
 
     fun clear() {
-        dismissAction = {}
+        positiveAction = null
     }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
index 16a794deeb..e59c957335 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
@@ -23,7 +23,7 @@ class TaskViewModel : ViewModel() {
     val cancelled: StateFlow<Boolean> get() = _cancelled
     private val _cancelled = MutableStateFlow(false)
 
-    lateinit var task: () -> Any
+    lateinit var task: suspend () -> Any
 
     fun clear() {
         _result.value = Any()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
index 805b89b31d..d5acf84791 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
@@ -19,7 +19,7 @@ import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import com.google.android.material.color.MaterialColors
-import com.google.android.material.transition.MaterialFadeThrough
+import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.launch
 import org.yuzu.yuzu_emu.R
 import org.yuzu.yuzu_emu.adapters.GameAdapter
@@ -35,11 +35,6 @@ class GamesFragment : Fragment() {
     private val gamesViewModel: GamesViewModel by activityViewModels()
     private val homeViewModel: HomeViewModel by activityViewModels()
 
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        enterTransition = MaterialFadeThrough()
-    }
-
     override fun onCreateView(
         inflater: LayoutInflater,
         container: ViewGroup?,
@@ -52,7 +47,9 @@ class GamesFragment : Fragment() {
     // This is using the correct scope, lint is just acting up
     @SuppressLint("UnsafeRepeatOnLifecycleDetector")
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
-        homeViewModel.setNavigationVisibility(visible = true, animated = false)
+        super.onViewCreated(view, savedInstanceState)
+        homeViewModel.setNavigationVisibility(visible = true, animated = true)
+        homeViewModel.setStatusBarShadeVisibility(true)
 
         binding.gridGames.apply {
             layoutManager = AutofitGridLayoutManager(
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
index 16323a316a..09ddd1bbd0 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
@@ -43,7 +43,7 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings
 import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
 import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
 import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
-import org.yuzu.yuzu_emu.getPublicFilesDir
+import org.yuzu.yuzu_emu.model.AddonViewModel
 import org.yuzu.yuzu_emu.model.GamesViewModel
 import org.yuzu.yuzu_emu.model.HomeViewModel
 import org.yuzu.yuzu_emu.model.TaskState
@@ -60,15 +60,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
     private val homeViewModel: HomeViewModel by viewModels()
     private val gamesViewModel: GamesViewModel by viewModels()
     private val taskViewModel: TaskViewModel by viewModels()
+    private val addonViewModel: AddonViewModel by viewModels()
 
     override var themeId: Int = 0
 
-    private val savesFolder
-        get() = "${getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000"
-
-    // Get first subfolder in saves folder (should be the user folder)
-    val savesFolderRoot get() = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: ""
-
     override fun onCreate(savedInstanceState: Bundle?) {
         val splashScreen = installSplashScreen()
         splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
@@ -145,6 +140,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                     homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) }
                 }
             }
+            launch {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    homeViewModel.contentToInstall.collect {
+                        if (it != null) {
+                            installContent(it)
+                            homeViewModel.setContentToInstall(null)
+                        }
+                    }
+                }
+            }
         }
 
         // Dismiss previous notifications (should not happen unless a crash occurred)
@@ -468,110 +473,150 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
     val installGameUpdate = registerForActivityResult(
         ActivityResultContracts.OpenMultipleDocuments()
     ) { documents: List<Uri> ->
-        if (documents.isNotEmpty()) {
-            IndeterminateProgressDialogFragment.newInstance(
-                this@MainActivity,
-                R.string.installing_game_content
-            ) {
-                var installSuccess = 0
-                var installOverwrite = 0
-                var errorBaseGame = 0
-                var errorExtension = 0
-                var errorOther = 0
-                documents.forEach {
-                    when (
-                        NativeLibrary.installFileToNand(
-                            it.toString(),
-                            FileUtil.getExtension(it)
-                        )
-                    ) {
-                        NativeLibrary.InstallFileToNandResult.Success -> {
-                            installSuccess += 1
-                        }
-
-                        NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> {
-                            installOverwrite += 1
-                        }
-
-                        NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> {
-                            errorBaseGame += 1
-                        }
-
-                        NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> {
-                            errorExtension += 1
-                        }
-
-                        else -> {
-                            errorOther += 1
-                        }
-                    }
-                }
-
-                val separator = System.getProperty("line.separator") ?: "\n"
-                val installResult = StringBuilder()
-                if (installSuccess > 0) {
-                    installResult.append(
-                        getString(
-                            R.string.install_game_content_success_install,
-                            installSuccess
-                        )
-                    )
-                    installResult.append(separator)
-                }
-                if (installOverwrite > 0) {
-                    installResult.append(
-                        getString(
-                            R.string.install_game_content_success_overwrite,
-                            installOverwrite
-                        )
-                    )
-                    installResult.append(separator)
-                }
-                val errorTotal: Int = errorBaseGame + errorExtension + errorOther
-                if (errorTotal > 0) {
-                    installResult.append(separator)
-                    installResult.append(
-                        getString(
-                            R.string.install_game_content_failed_count,
-                            errorTotal
-                        )
-                    )
-                    installResult.append(separator)
-                    if (errorBaseGame > 0) {
-                        installResult.append(separator)
-                        installResult.append(
-                            getString(R.string.install_game_content_failure_base)
-                        )
-                        installResult.append(separator)
-                    }
-                    if (errorExtension > 0) {
-                        installResult.append(separator)
-                        installResult.append(
-                            getString(R.string.install_game_content_failure_file_extension)
-                        )
-                        installResult.append(separator)
-                    }
-                    if (errorOther > 0) {
-                        installResult.append(
-                            getString(R.string.install_game_content_failure_description)
-                        )
-                        installResult.append(separator)
-                    }
-                    return@newInstance MessageDialogFragment.newInstance(
-                        this,
-                        titleId = R.string.install_game_content_failure,
-                        descriptionString = installResult.toString().trim(),
-                        helpLinkId = R.string.install_game_content_help_link
-                    )
-                } else {
-                    return@newInstance MessageDialogFragment.newInstance(
-                        this,
-                        titleId = R.string.install_game_content_success,
-                        descriptionString = installResult.toString().trim()
-                    )
-                }
-            }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
+        if (documents.isEmpty()) {
+            return@registerForActivityResult
         }
+
+        if (addonViewModel.game == null) {
+            installContent(documents)
+            return@registerForActivityResult
+        }
+
+        IndeterminateProgressDialogFragment.newInstance(
+            this@MainActivity,
+            R.string.verifying_content,
+            false
+        ) {
+            var updatesMatchProgram = true
+            for (document in documents) {
+                val valid = NativeLibrary.doesUpdateMatchProgram(
+                    addonViewModel.game!!.programId,
+                    document.toString()
+                )
+                if (!valid) {
+                    updatesMatchProgram = false
+                    break
+                }
+            }
+
+            if (updatesMatchProgram) {
+                homeViewModel.setContentToInstall(documents)
+            } else {
+                MessageDialogFragment.newInstance(
+                    this@MainActivity,
+                    titleId = R.string.content_install_notice,
+                    descriptionId = R.string.content_install_notice_description,
+                    positiveAction = { homeViewModel.setContentToInstall(documents) }
+                )
+            }
+        }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
+    }
+
+    private fun installContent(documents: List<Uri>) {
+        IndeterminateProgressDialogFragment.newInstance(
+            this@MainActivity,
+            R.string.installing_game_content
+        ) {
+            var installSuccess = 0
+            var installOverwrite = 0
+            var errorBaseGame = 0
+            var errorExtension = 0
+            var errorOther = 0
+            documents.forEach {
+                when (
+                    NativeLibrary.installFileToNand(
+                        it.toString(),
+                        FileUtil.getExtension(it)
+                    )
+                ) {
+                    NativeLibrary.InstallFileToNandResult.Success -> {
+                        installSuccess += 1
+                    }
+
+                    NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> {
+                        installOverwrite += 1
+                    }
+
+                    NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> {
+                        errorBaseGame += 1
+                    }
+
+                    NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> {
+                        errorExtension += 1
+                    }
+
+                    else -> {
+                        errorOther += 1
+                    }
+                }
+            }
+
+            addonViewModel.refreshAddons()
+
+            val separator = System.getProperty("line.separator") ?: "\n"
+            val installResult = StringBuilder()
+            if (installSuccess > 0) {
+                installResult.append(
+                    getString(
+                        R.string.install_game_content_success_install,
+                        installSuccess
+                    )
+                )
+                installResult.append(separator)
+            }
+            if (installOverwrite > 0) {
+                installResult.append(
+                    getString(
+                        R.string.install_game_content_success_overwrite,
+                        installOverwrite
+                    )
+                )
+                installResult.append(separator)
+            }
+            val errorTotal: Int = errorBaseGame + errorExtension + errorOther
+            if (errorTotal > 0) {
+                installResult.append(separator)
+                installResult.append(
+                    getString(
+                        R.string.install_game_content_failed_count,
+                        errorTotal
+                    )
+                )
+                installResult.append(separator)
+                if (errorBaseGame > 0) {
+                    installResult.append(separator)
+                    installResult.append(
+                        getString(R.string.install_game_content_failure_base)
+                    )
+                    installResult.append(separator)
+                }
+                if (errorExtension > 0) {
+                    installResult.append(separator)
+                    installResult.append(
+                        getString(R.string.install_game_content_failure_file_extension)
+                    )
+                    installResult.append(separator)
+                }
+                if (errorOther > 0) {
+                    installResult.append(
+                        getString(R.string.install_game_content_failure_description)
+                    )
+                    installResult.append(separator)
+                }
+                return@newInstance MessageDialogFragment.newInstance(
+                    this,
+                    titleId = R.string.install_game_content_failure,
+                    descriptionString = installResult.toString().trim(),
+                    helpLinkId = R.string.install_game_content_help_link
+                )
+            } else {
+                return@newInstance MessageDialogFragment.newInstance(
+                    this,
+                    titleId = R.string.install_game_content_success,
+                    descriptionString = installResult.toString().trim()
+                )
+            }
+        }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
     }
 
     val exportUserData = registerForActivityResult(
@@ -657,102 +702,4 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                 return@newInstance getString(R.string.user_data_import_success)
             }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
         }
-
-    /**
-     * Exports the save file located in the given folder path by creating a zip file and sharing it via intent.
-     */
-    val exportSaves = registerForActivityResult(
-        ActivityResultContracts.CreateDocument("application/zip")
-    ) { result ->
-        if (result == null) {
-            return@registerForActivityResult
-        }
-
-        IndeterminateProgressDialogFragment.newInstance(
-            this,
-            R.string.save_files_exporting,
-            false
-        ) {
-            val zipResult = FileUtil.zipFromInternalStorage(
-                File(savesFolderRoot),
-                savesFolderRoot,
-                BufferedOutputStream(contentResolver.openOutputStream(result))
-            )
-            return@newInstance when (zipResult) {
-                TaskState.Completed -> getString(R.string.export_success)
-                TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
-            }
-        }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
-    }
-
-    private val startForResultExportSave =
-        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ ->
-            File(getPublicFilesDir().canonicalPath, "temp").deleteRecursively()
-        }
-
-    val importSaves =
-        registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
-            if (result == null) {
-                return@registerForActivityResult
-            }
-
-            NativeLibrary.initializeEmptyUserDirectory()
-
-            val inputZip = contentResolver.openInputStream(result)
-            // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid.
-            var validZip = false
-            val savesFolder = File(savesFolderRoot)
-            val cacheSaveDir = File("${applicationContext.cacheDir.path}/saves/")
-            cacheSaveDir.mkdir()
-
-            if (inputZip == null) {
-                Toast.makeText(
-                    applicationContext,
-                    getString(R.string.fatal_error),
-                    Toast.LENGTH_LONG
-                ).show()
-                return@registerForActivityResult
-            }
-
-            val filterTitleId =
-                FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) }
-
-            try {
-                CoroutineScope(Dispatchers.IO).launch {
-                    FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
-                    cacheSaveDir.list(filterTitleId)?.forEach { savePath ->
-                        File(savesFolder, savePath).deleteRecursively()
-                        File(cacheSaveDir, savePath).copyRecursively(
-                            File(savesFolder, savePath),
-                            true
-                        )
-                        validZip = true
-                    }
-
-                    withContext(Dispatchers.Main) {
-                        if (!validZip) {
-                            MessageDialogFragment.newInstance(
-                                this@MainActivity,
-                                titleId = R.string.save_file_invalid_zip_structure,
-                                descriptionId = R.string.save_file_invalid_zip_structure_description
-                            ).show(supportFragmentManager, MessageDialogFragment.TAG)
-                            return@withContext
-                        }
-                        Toast.makeText(
-                            applicationContext,
-                            getString(R.string.save_file_imported_success),
-                            Toast.LENGTH_LONG
-                        ).show()
-                    }
-
-                    cacheSaveDir.deleteRecursively()
-                }
-            } catch (e: Exception) {
-                Toast.makeText(
-                    applicationContext,
-                    getString(R.string.fatal_error),
-                    Toast.LENGTH_LONG
-                ).show()
-            }
-        }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddonUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddonUtil.kt
new file mode 100644
index 0000000000..8cc5ea71f2
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddonUtil.kt
@@ -0,0 +1,8 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.utils
+
+object AddonUtil {
+    val validAddonDirectories = listOf("cheats", "exefs", "romfs")
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
index bbe7bfa922..00c6bf90e7 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
@@ -22,6 +22,7 @@ import java.io.BufferedOutputStream
 import java.lang.NullPointerException
 import java.nio.charset.StandardCharsets
 import java.util.zip.ZipOutputStream
+import kotlin.IllegalStateException
 
 object FileUtil {
     const val PATH_TREE = "tree"
@@ -342,6 +343,37 @@ object FileUtil {
         return TaskState.Completed
     }
 
+    /**
+     * Helper function that copies the contents of a DocumentFile folder into a [File]
+     * @param file [File] representation of the folder to copy into
+     * @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa
+     */
+    fun DocumentFile.copyFilesTo(file: File) {
+        file.mkdirs()
+        if (!this.isDirectory || !file.isDirectory) {
+            throw IllegalStateException(
+                "[FileUtil] Tried to copy a folder into a file or vice versa"
+            )
+        }
+
+        this.listFiles().forEach {
+            val newFile = File(file, it.name!!)
+            if (it.isDirectory) {
+                newFile.mkdirs()
+                DocumentFile.fromTreeUri(YuzuApplication.appContext, it.uri)?.copyFilesTo(newFile)
+            } else {
+                val inputStream =
+                    YuzuApplication.appContext.contentResolver.openInputStream(it.uri)
+                BufferedInputStream(inputStream).use { bos ->
+                    if (!newFile.exists()) {
+                        newFile.createNewFile()
+                    }
+                    newFile.outputStream().use { os -> bos.copyTo(os) }
+                }
+            }
+        }
+    }
+
     fun isRootTreeUri(uri: Uri): Boolean {
         val paths = uri.pathSegments
         return paths.size == 2 && PATH_TREE == paths[0]
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
index 4c7316ba39..7d629b7d54 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
@@ -105,4 +105,23 @@ object NativeConfig {
      */
     @Synchronized
     external fun addGameDir(dir: GameDir)
+
+    /**
+     * Gets an array of the addons that are disabled for a given game
+     *
+     * @param programId String representation of a game's program ID
+     * @return An array of disabled addons
+     */
+    @Synchronized
+    external fun getDisabledAddons(programId: String): Array<String>
+
+    /**
+     * Clears the disabled addons array corresponding to [programId] and replaces them
+     * with [disabledAddons]
+     *
+     * @param programId String representation of a game's program ID
+     * @param disabledAddons Replacement array of disabled addons
+     */
+    @Synchronized
+    external fun setDisabledAddons(programId: String, disabledAddons: Array<String>)
 }
diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp
index a56ed56629..df89351783 100644
--- a/src/android/app/src/main/jni/id_cache.cpp
+++ b/src/android/app/src/main/jni/id_cache.cpp
@@ -20,6 +20,12 @@ static jmethodID s_disk_cache_load_progress;
 static jmethodID s_on_emulation_started;
 static jmethodID s_on_emulation_stopped;
 
+static jclass s_string_class;
+static jclass s_pair_class;
+static jmethodID s_pair_constructor;
+static jfieldID s_pair_first_field;
+static jfieldID s_pair_second_field;
+
 static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
 
 namespace IDCache {
@@ -79,6 +85,26 @@ jmethodID GetOnEmulationStopped() {
     return s_on_emulation_stopped;
 }
 
+jclass GetStringClass() {
+    return s_string_class;
+}
+
+jclass GetPairClass() {
+    return s_pair_class;
+}
+
+jmethodID GetPairConstructor() {
+    return s_pair_constructor;
+}
+
+jfieldID GetPairFirstField() {
+    return s_pair_first_field;
+}
+
+jfieldID GetPairSecondField() {
+    return s_pair_second_field;
+}
+
 } // namespace IDCache
 
 #ifdef __cplusplus
@@ -115,6 +141,18 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
     s_on_emulation_stopped =
         env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V");
 
+    const jclass string_class = env->FindClass("java/lang/String");
+    s_string_class = reinterpret_cast<jclass>(env->NewGlobalRef(string_class));
+    env->DeleteLocalRef(string_class);
+
+    const jclass pair_class = env->FindClass("kotlin/Pair");
+    s_pair_class = reinterpret_cast<jclass>(env->NewGlobalRef(pair_class));
+    s_pair_constructor =
+        env->GetMethodID(pair_class, "<init>", "(Ljava/lang/Object;Ljava/lang/Object;)V");
+    s_pair_first_field = env->GetFieldID(pair_class, "first", "Ljava/lang/Object;");
+    s_pair_second_field = env->GetFieldID(pair_class, "second", "Ljava/lang/Object;");
+    env->DeleteLocalRef(pair_class);
+
     // Initialize Android Storage
     Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
 
@@ -136,6 +174,8 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
     env->DeleteGlobalRef(s_disk_cache_progress_class);
     env->DeleteGlobalRef(s_load_callback_stage_class);
     env->DeleteGlobalRef(s_game_dir_class);
+    env->DeleteGlobalRef(s_string_class);
+    env->DeleteGlobalRef(s_pair_class);
 
     // UnInitialize applets
     SoftwareKeyboard::CleanupJNI(env);
diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h
index 855649efaf..36233423eb 100644
--- a/src/android/app/src/main/jni/id_cache.h
+++ b/src/android/app/src/main/jni/id_cache.h
@@ -20,4 +20,10 @@ jmethodID GetDiskCacheLoadProgress();
 jmethodID GetOnEmulationStarted();
 jmethodID GetOnEmulationStopped();
 
+jclass GetStringClass();
+jclass GetPairClass();
+jmethodID GetPairConstructor();
+jfieldID GetPairFirstField();
+jfieldID GetPairSecondField();
+
 } // namespace IDCache
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index e5d3158c83..ce570b811a 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -14,6 +14,7 @@
 #include <android/api-level.h>
 #include <android/native_window_jni.h>
 #include <common/fs/fs.h>
+#include <core/file_sys/patch_manager.h>
 #include <core/file_sys/savedata_factory.h>
 #include <core/loader/nro.h>
 #include <jni.h>
@@ -79,6 +80,10 @@ Core::System& EmulationSession::System() {
     return m_system;
 }
 
+FileSys::ManualContentProvider* EmulationSession::ContentProvider() {
+    return m_manual_provider.get();
+}
+
 const EmuWindow_Android& EmulationSession::Window() const {
     return *m_window;
 }
@@ -455,6 +460,15 @@ void EmulationSession::OnEmulationStopped(Core::SystemResultStatus result) {
                               static_cast<jint>(result));
 }
 
+u64 EmulationSession::GetProgramId(JNIEnv* env, jstring jprogramId) {
+    auto program_id_string = GetJString(env, jprogramId);
+    try {
+        return std::stoull(program_id_string);
+    } catch (...) {
+        return 0;
+    }
+}
+
 static Core::SystemResultStatus RunEmulation(const std::string& filepath) {
     MicroProfileOnThreadCreate("EmuThread");
     SCOPE_EXIT({ MicroProfileShutdown(); });
@@ -504,6 +518,27 @@ int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject
                                                              GetJString(env, j_file_extension));
 }
 
+jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj,
+                                                                      jstring jprogramId,
+                                                                      jstring jupdatePath) {
+    u64 program_id = EmulationSession::GetProgramId(env, jprogramId);
+    std::string updatePath = GetJString(env, jupdatePath);
+    std::shared_ptr<FileSys::NSP> nsp = std::make_shared<FileSys::NSP>(
+        EmulationSession::GetInstance().System().GetFilesystem()->OpenFile(updatePath,
+                                                                           FileSys::Mode::Read));
+    for (const auto& item : nsp->GetNCAs()) {
+        for (const auto& nca_details : item.second) {
+            if (nca_details.second->GetName().ends_with(".cnmt.nca")) {
+                auto update_id = nca_details.second->GetTitleId() & ~0xFFFULL;
+                if (update_id == program_id) {
+                    return true;
+                }
+            }
+        }
+    }
+    return false;
+}
+
 void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, jclass clazz,
                                                                        jstring hook_lib_dir,
                                                                        jstring custom_driver_dir,
@@ -665,13 +700,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass
     EmulationSession::GetInstance().InitializeSystem(reload);
 }
 
-jint Java_org_yuzu_yuzu_1emu_NativeLibrary_defaultCPUCore(JNIEnv* env, jclass clazz) {
-    return {};
-}
-
-void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2Ljava_lang_String_2Z(
-    JNIEnv* env, jclass clazz, jstring j_file, jstring j_savestate, jboolean j_delete_savestate) {}
-
 jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats(JNIEnv* env, jclass clazz) {
     jdoubleArray j_stats = env->NewDoubleArray(4);
 
@@ -696,9 +724,13 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuBackend(JNIEnv* env, jclass
     return ToJString(env, "JIT");
 }
 
-void Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_setSysDirectory(JNIEnv* env,
-                                                                           jclass clazz,
-                                                                           jstring j_path) {}
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) {
+    EmulationSession::GetInstance().System().ApplySettings();
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj) {
+    Settings::LogSettings();
+}
 
 void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2(JNIEnv* env, jclass clazz,
                                                                     jstring j_path) {
@@ -792,4 +824,60 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env,
     return true;
 }
 
+jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, jobject jobj,
+                                                                    jstring jpath,
+                                                                    jstring jprogramId) {
+    const auto path = GetJString(env, jpath);
+    const auto vFile =
+        Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path);
+    if (vFile == nullptr) {
+        return nullptr;
+    }
+
+    auto& system = EmulationSession::GetInstance().System();
+    auto program_id = EmulationSession::GetProgramId(env, jprogramId);
+    const FileSys::PatchManager pm{program_id, system.GetFileSystemController(),
+                                   system.GetContentProvider()};
+    const auto loader = Loader::GetLoader(system, vFile);
+
+    FileSys::VirtualFile update_raw;
+    loader->ReadUpdateRaw(update_raw);
+
+    auto addons = pm.GetPatchVersionNames(update_raw);
+    auto jemptyString = ToJString(env, "");
+    auto jemptyStringPair = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(),
+                                           jemptyString, jemptyString);
+    jobjectArray jaddonsArray =
+        env->NewObjectArray(addons.size(), IDCache::GetPairClass(), jemptyStringPair);
+    int i = 0;
+    for (const auto& addon : addons) {
+        jobject jaddon = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(),
+                                        ToJString(env, addon.first), ToJString(env, addon.second));
+        env->SetObjectArrayElement(jaddonsArray, i, jaddon);
+        ++i;
+    }
+    return jaddonsArray;
+}
+
+jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj,
+                                                          jstring jprogramId) {
+    auto program_id = EmulationSession::GetProgramId(env, jprogramId);
+
+    auto& system = EmulationSession::GetInstance().System();
+
+    Service::Account::ProfileManager manager;
+    // TODO: Pass in a selected user once we get the relevant UI working
+    const auto user_id = manager.GetUser(static_cast<std::size_t>(0));
+    ASSERT(user_id);
+
+    const auto nandDir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir);
+    auto vfsNandDir = system.GetFilesystem()->OpenDirectory(Common::FS::PathToUTF8String(nandDir),
+                                                            FileSys::Mode::Read);
+
+    const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath(
+        system, vfsNandDir, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData,
+        program_id, user_id->AsU128(), 0);
+    return ToJString(env, user_save_data_path);
+}
+
 } // extern "C"
diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h
index f1457bd1f6..96c22d52b3 100644
--- a/src/android/app/src/main/jni/native.h
+++ b/src/android/app/src/main/jni/native.h
@@ -54,6 +54,8 @@ public:
 
     static void OnEmulationStarted();
 
+    static u64 GetProgramId(JNIEnv* env, jstring jprogramId);
+
 private:
     static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max);
     static void OnEmulationStopped(Core::SystemResultStatus result);
diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp
index 9439d11e14..7f2485720a 100644
--- a/src/android/app/src/main/jni/native_config.cpp
+++ b/src/android/app/src/main/jni/native_config.cpp
@@ -283,4 +283,30 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_addGameDir(JNIEnv* env, jobject
         AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)});
 }
 
+jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getDisabledAddons(JNIEnv* env, jobject obj,
+                                                                          jstring jprogramId) {
+    auto program_id = EmulationSession::GetProgramId(env, jprogramId);
+    auto& disabledAddons = Settings::values.disabled_addons[program_id];
+    jobjectArray jdisabledAddonsArray =
+        env->NewObjectArray(disabledAddons.size(), IDCache::GetStringClass(), ToJString(env, ""));
+    for (size_t i = 0; i < disabledAddons.size(); ++i) {
+        env->SetObjectArrayElement(jdisabledAddonsArray, i, ToJString(env, disabledAddons[i]));
+    }
+    return jdisabledAddonsArray;
+}
+
+void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setDisabledAddons(JNIEnv* env, jobject obj,
+                                                                  jstring jprogramId,
+                                                                  jobjectArray jdisabledAddons) {
+    auto program_id = EmulationSession::GetProgramId(env, jprogramId);
+    Settings::values.disabled_addons[program_id].clear();
+    std::vector<std::string> disabled_addons;
+    const int size = env->GetArrayLength(jdisabledAddons);
+    for (int i = 0; i < size; ++i) {
+        auto jaddon = static_cast<jstring>(env->GetObjectArrayElement(jdisabledAddons, i));
+        disabled_addons.push_back(GetJString(env, jaddon));
+    }
+    Settings::values.disabled_addons[program_id] = disabled_addons;
+}
+
 } // extern "C"
diff --git a/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml b/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml
new file mode 100644
index 0000000000..0b96338556
--- /dev/null
+++ b/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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">
+
+    <androidx.core.widget.NestedScrollView
+        android:id="@+id/list_all"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:clipToPadding="false"
+        android:fadeScrollbars="false"
+        android:scrollbars="vertical"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@+id/icon_layout"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <LinearLayout
+            android:id="@+id/layout_all"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="center_horizontal"
+            android:orientation="horizontal">
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/list_properties"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                tools:listitem="@layout/card_simple_outlined" />
+
+        </LinearLayout>
+
+    </androidx.core.widget.NestedScrollView>
+
+    <LinearLayout
+        android:id="@+id/icon_layout"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <Button
+            android:id="@+id/button_back"
+            style="?attr/materialIconButtonStyle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="start"
+            android:layout_margin="8dp"
+            app:icon="@drawable/ic_back"
+            app:iconSize="24dp"
+            app:iconTint="?attr/colorOnSurface" />
+
+        <com.google.android.material.card.MaterialCardView
+            style="?attr/materialCardViewElevatedStyle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginHorizontal="16dp"
+            android:layout_marginTop="8dp"
+            app:cardCornerRadius="4dp"
+            app:cardElevation="4dp">
+
+            <ImageView
+                android:id="@+id/image_game_screen"
+                android:layout_width="175dp"
+                android:layout_height="175dp"
+                tools:src="@drawable/default_icon" />
+
+        </com.google.android.material.card.MaterialCardView>
+
+        <com.google.android.material.textview.MaterialTextView
+            android:id="@+id/title"
+            style="@style/TextAppearance.Material3.TitleMedium"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginHorizontal="16dp"
+            android:layout_marginTop="12dp"
+            android:ellipsize="none"
+            android:marqueeRepeatLimit="marquee_forever"
+            android:requiresFadingEdge="horizontal"
+            android:singleLine="true"
+            android:textAlignment="center"
+            tools:text="deko_basic" />
+
+    </LinearLayout>
+
+    <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
+        android:id="@+id/button_start"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/start"
+        app:icon="@drawable/ic_play"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/card_installable.xml b/src/android/app/src/main/res/layout/card_installable.xml
index f5b0e37419..ce2402d7a9 100644
--- a/src/android/app/src/main/res/layout/card_installable.xml
+++ b/src/android/app/src/main/res/layout/card_installable.xml
@@ -11,7 +11,8 @@
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_margin="16dp"
+        android:paddingVertical="16dp"
+        android:paddingHorizontal="24dp"
         android:orientation="horizontal"
         android:layout_gravity="center">
 
diff --git a/src/android/app/src/main/res/layout/card_applet_option.xml b/src/android/app/src/main/res/layout/card_simple_outlined.xml
similarity index 71%
rename from src/android/app/src/main/res/layout/card_applet_option.xml
rename to src/android/app/src/main/res/layout/card_simple_outlined.xml
index 19fbec9f1e..b73930e7e0 100644
--- a/src/android/app/src/main/res/layout/card_applet_option.xml
+++ b/src/android/app/src/main/res/layout/card_simple_outlined.xml
@@ -16,7 +16,8 @@
         android:layout_height="wrap_content"
         android:orientation="horizontal"
         android:layout_gravity="center"
-        android:padding="24dp">
+        android:paddingVertical="16dp"
+        android:paddingHorizontal="24dp">
 
         <ImageView
             android:id="@+id/icon"
@@ -50,6 +51,23 @@
                 android:textAlignment="viewStart"
                 tools:text="@string/applets_description" />
 
+            <com.google.android.material.textview.MaterialTextView
+                style="@style/TextAppearance.Material3.LabelMedium"
+                android:id="@+id/details"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:textAlignment="viewStart"
+                android:textSize="14sp"
+                android:textStyle="bold"
+                android:singleLine="true"
+                android:marqueeRepeatLimit="marquee_forever"
+                android:ellipsize="none"
+                android:requiresFadingEdge="horizontal"
+                android:layout_marginTop="6dp"
+                android:visibility="gone"
+                tools:visibility="visible"
+                tools:text="/tree/primary:Games" />
+
         </LinearLayout>
 
     </LinearLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_addons.xml b/src/android/app/src/main/res/layout/fragment_addons.xml
new file mode 100644
index 0000000000..a25e82766c
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_addons.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/coordinator_about"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="?attr/colorSurface">
+
+    <com.google.android.material.appbar.AppBarLayout
+        android:id="@+id/appbar_addons"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:fitsSystemWindows="true"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <com.google.android.material.appbar.MaterialToolbar
+            android:id="@+id/toolbar_addons"
+            android:layout_width="match_parent"
+            android:layout_height="?attr/actionBarSize"
+            app:navigationIcon="@drawable/ic_back" />
+
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/list_addons"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:clipToPadding="false"
+        app:layout_behavior="@string/appbar_scrolling_view_behavior"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/appbar_addons" />
+
+    <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
+        android:id="@+id/button_install"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom|end"
+        android:text="@string/install"
+        app:icon="@drawable/ic_add"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_game_info.xml b/src/android/app/src/main/res/layout/fragment_game_info.xml
new file mode 100644
index 0000000000..80ede8a8c1
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_game_info.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout 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/coordinator_about"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="?attr/colorSurface">
+
+    <com.google.android.material.appbar.AppBarLayout
+        android:id="@+id/appbar_info"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:fitsSystemWindows="true">
+
+        <com.google.android.material.appbar.MaterialToolbar
+            android:id="@+id/toolbar_info"
+            android:layout_width="match_parent"
+            android:layout_height="?attr/actionBarSize"
+            app:navigationIcon="@drawable/ic_back" />
+
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <androidx.core.widget.NestedScrollView
+        android:id="@+id/scroll_info"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_behavior="@string/appbar_scrolling_view_behavior">
+
+        <LinearLayout
+            android:id="@+id/content_info"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            android:paddingHorizontal="16dp">
+
+            <com.google.android.material.textfield.TextInputLayout
+                android:id="@+id/path"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingTop="16dp">
+
+                <com.google.android.material.textfield.TextInputEditText
+                    android:id="@+id/path_field"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:editable="false"
+                    android:importantForAutofill="no"
+                    android:inputType="none"
+                    android:minHeight="48dp"
+                    android:textAlignment="viewStart"
+                    tools:text="1.0.0" />
+
+            </com.google.android.material.textfield.TextInputLayout>
+
+            <com.google.android.material.textfield.TextInputLayout
+                android:id="@+id/program_id"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingTop="16dp">
+
+                <com.google.android.material.textfield.TextInputEditText
+                    android:id="@+id/program_id_field"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:editable="false"
+                    android:importantForAutofill="no"
+                    android:inputType="none"
+                    android:minHeight="48dp"
+                    android:textAlignment="viewStart"
+                    tools:text="1.0.0" />
+
+            </com.google.android.material.textfield.TextInputLayout>
+
+            <com.google.android.material.textfield.TextInputLayout
+                android:id="@+id/developer"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingTop="16dp">
+
+                <com.google.android.material.textfield.TextInputEditText
+                    android:id="@+id/developer_field"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:editable="false"
+                    android:importantForAutofill="no"
+                    android:inputType="none"
+                    android:minHeight="48dp"
+                    android:textAlignment="viewStart"
+                    tools:text="1.0.0" />
+
+            </com.google.android.material.textfield.TextInputLayout>
+
+            <com.google.android.material.textfield.TextInputLayout
+                android:id="@+id/version"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:paddingTop="16dp">
+
+                <com.google.android.material.textfield.TextInputEditText
+                    android:id="@+id/version_field"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:editable="false"
+                    android:importantForAutofill="no"
+                    android:inputType="none"
+                    android:minHeight="48dp"
+                    android:textAlignment="viewStart"
+                    tools:text="1.0.0" />
+
+            </com.google.android.material.textfield.TextInputLayout>
+
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/button_copy"
+                style="@style/Widget.Material3.Button"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="16dp"
+                android:text="@string/copy_details" />
+
+        </LinearLayout>
+
+    </androidx.core.widget.NestedScrollView>
+
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_game_properties.xml b/src/android/app/src/main/res/layout/fragment_game_properties.xml
new file mode 100644
index 0000000000..72ecbde30a
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_game_properties.xml
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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">
+
+    <androidx.core.widget.NestedScrollView
+        android:id="@+id/list_all"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:scrollbars="vertical"
+        android:fadeScrollbars="false"
+        android:clipToPadding="false">
+
+        <LinearLayout
+            android:id="@+id/layout_all"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            android:gravity="center_horizontal">
+
+            <Button
+                android:id="@+id/button_back"
+                style="?attr/materialIconButtonStyle"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_margin="8dp"
+                android:layout_gravity="start"
+                app:icon="@drawable/ic_back"
+                app:iconSize="24dp"
+                app:iconTint="?attr/colorOnSurface" />
+
+            <com.google.android.material.card.MaterialCardView
+                style="?attr/materialCardViewElevatedStyle"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="8dp"
+                app:cardCornerRadius="4dp"
+                app:cardElevation="4dp">
+
+                <ImageView
+                    android:id="@+id/image_game_screen"
+                    android:layout_width="175dp"
+                    android:layout_height="175dp"
+                    tools:src="@drawable/default_icon"/>
+
+            </com.google.android.material.card.MaterialCardView>
+
+            <com.google.android.material.textview.MaterialTextView
+                android:id="@+id/title"
+                style="@style/TextAppearance.Material3.TitleMedium"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="12dp"
+                android:layout_marginBottom="12dp"
+                android:layout_marginHorizontal="16dp"
+                android:ellipsize="none"
+                android:marqueeRepeatLimit="marquee_forever"
+                android:requiresFadingEdge="horizontal"
+                android:singleLine="true"
+                android:textAlignment="center"
+                tools:text="deko_basic" />
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/list_properties"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                tools:listitem="@layout/card_simple_outlined" />
+
+        </LinearLayout>
+
+    </androidx.core.widget.NestedScrollView>
+
+    <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
+        android:id="@+id/button_start"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/start"
+        app:icon="@drawable/ic_play"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/list_item_addon.xml b/src/android/app/src/main/res/layout/list_item_addon.xml
new file mode 100644
index 0000000000..74ca04ef1b
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_addon.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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/addon_container"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="?attr/selectableItemBackground"
+    android:focusable="true"
+    android:paddingHorizontal="20dp"
+    android:paddingVertical="16dp">
+
+    <LinearLayout
+        android:id="@+id/text_container"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="16dp"
+        android:orientation="vertical"
+        app:layout_constraintBottom_toBottomOf="@+id/addon_switch"
+        app:layout_constraintEnd_toStartOf="@+id/addon_switch"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="@+id/addon_switch">
+
+        <com.google.android.material.textview.MaterialTextView
+            android:id="@+id/title"
+            style="@style/TextAppearance.Material3.HeadlineMedium"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textAlignment="viewStart"
+            android:textSize="17sp"
+            app:lineHeight="28dp"
+            tools:text="1440p Resolution" />
+
+        <com.google.android.material.textview.MaterialTextView
+            android:id="@+id/version"
+            style="@style/TextAppearance.Material3.BodySmall"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="@dimen/spacing_small"
+            android:textAlignment="viewStart"
+            tools:text="1.0.0" />
+
+    </LinearLayout>
+
+    <com.google.android.material.materialswitch.MaterialSwitch
+        android:id="@+id/addon_switch"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:focusable="true"
+        android:gravity="center"
+        android:nextFocusLeft="@id/addon_container"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toEndOf="@id/text_container"
+        app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
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 cf70b4bc42..1c69bf0db5 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -124,5 +124,38 @@
         android:id="@+id/gameFoldersFragment"
         android:name="org.yuzu.yuzu_emu.fragments.GameFoldersFragment"
         android:label="GameFoldersFragment" />
+    <fragment
+        android:id="@+id/perGamePropertiesFragment"
+        android:name="org.yuzu.yuzu_emu.fragments.GamePropertiesFragment"
+        android:label="PerGamePropertiesFragment" >
+        <argument
+            android:name="game"
+            app:argType="org.yuzu.yuzu_emu.model.Game" />
+        <action
+            android:id="@+id/action_perGamePropertiesFragment_to_gameInfoFragment"
+            app:destination="@id/gameInfoFragment" />
+        <action
+            android:id="@+id/action_perGamePropertiesFragment_to_addonsFragment"
+            app:destination="@id/addonsFragment" />
+    </fragment>
+    <action
+        android:id="@+id/action_global_perGamePropertiesFragment"
+        app:destination="@id/perGamePropertiesFragment" />
+    <fragment
+        android:id="@+id/gameInfoFragment"
+        android:name="org.yuzu.yuzu_emu.fragments.GameInfoFragment"
+        android:label="GameInfoFragment" >
+        <argument
+            android:name="game"
+            app:argType="org.yuzu.yuzu_emu.model.Game" />
+    </fragment>
+    <fragment
+        android:id="@+id/addonsFragment"
+        android:name="org.yuzu.yuzu_emu.fragments.AddonsFragment"
+        android:label="AddonsFragment" >
+        <argument
+            android:name="game"
+            app:argType="org.yuzu.yuzu_emu.model.Game" />
+    </fragment>
 
 </navigation>
diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml
index 380d142138..992b5ae443 100644
--- a/src/android/app/src/main/res/values/dimens.xml
+++ b/src/android/app/src/main/res/values/dimens.xml
@@ -13,7 +13,7 @@
     <dimen name="menu_width">256dp</dimen>
     <dimen name="card_width">165dp</dimen>
     <dimen name="icon_inset">24dp</dimen>
-    <dimen name="spacing_bottom_list_fab">76dp</dimen>
+    <dimen name="spacing_bottom_list_fab">96dp</dimen>
     <dimen name="spacing_fab">24dp</dimen>
 
     <dimen name="dialog_margin">20dp</dimen>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index a6ccef8a18..cd5571aa9c 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -91,7 +91,10 @@
     <string name="notification_no_directory_link_description">Please locate the user folder with the file manager\'s side panel manually.</string>
     <string name="manage_save_data">Manage save data</string>
     <string name="manage_save_data_description">Save data found. Please select an option below.</string>
+    <string name="import_save_warning">Import save data</string>
+    <string name="import_save_warning_description">This will overwrite all existing save data with the provided file. Are you sure that you want to continue?</string>
     <string name="import_export_saves_description">Import or export save files</string>
+    <string name="save_files_importing">Importing save files…</string>
     <string name="save_files_exporting">Exporting save files…</string>
     <string name="save_file_imported_success">Imported successfully</string>
     <string name="save_file_invalid_zip_structure">Invalid save directory structure</string>
@@ -266,6 +269,11 @@
     <string name="delete">Delete</string>
     <string name="edit">Edit</string>
     <string name="export_success">Exported successfully</string>
+    <string name="start">Start</string>
+    <string name="clear">Clear</string>
+    <string name="global">Global</string>
+    <string name="custom">Custom</string>
+    <string name="notice">Notice</string>
 
     <!-- GPU driver installation -->
     <string name="select_gpu_driver">Select GPU driver</string>
@@ -291,6 +299,43 @@
     <string name="preferences_debug">Debug</string>
     <string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string>
 
+    <!-- Game properties -->
+    <string name="info">Info</string>
+    <string name="info_description">Program ID, developer, version</string>
+    <string name="per_game_settings">Per-game settings</string>
+    <string name="per_game_settings_description">Edit settings specific to this game</string>
+    <string name="launch_options">Launch config</string>
+    <string name="path">Path</string>
+    <string name="program_id">Program ID</string>
+    <string name="developer">Developer</string>
+    <string name="version">Version</string>
+    <string name="copy_details">Copy details</string>
+    <string name="add_ons">Add-ons</string>
+    <string name="add_ons_description">Toggle mods, updates and DLC</string>
+    <string name="clear_shader_cache">Clear shader cache</string>
+    <string name="clear_shader_cache_description">Removes all shaders built while playing this game</string>
+    <string name="cleared_shaders_successfully">Cleared shaders successfully</string>
+    <string name="addons_game">Addons: %1$s</string>
+    <string name="save_data">Save data</string>
+    <string name="save_data_description">Manage save data specific to this game</string>
+    <string name="delete_save_data">Delete save data</string>
+    <string name="delete_save_data_description">Removes all save data specific to this game</string>
+    <string name="delete_save_data_warning_description">This irrecoverably removes all of this game\'s save data. Are you sure you want to continue?</string>
+    <string name="save_data_deleted_successfully">Save data deleted successfully</string>
+    <string name="select_content_type">Content type</string>
+    <string name="updates_and_dlc">Updates and DLC</string>
+    <string name="mods_and_cheats">Mods and cheats</string>
+    <string name="addon_notice">Important addon notice</string>
+    <!-- "cheats/" "romfs/" and "exefs/ should not be translated -->
+    <string name="addon_notice_description">In order to install mods and cheats, you must select a folder that contains a cheats/, romfs/, or exefs/ directory. We can\'t verify if these will be compatible with your game so be careful!</string>
+    <string name="invalid_directory">Invalid directory</string>
+    <!-- "cheats/" "romfs/" and "exefs/ should not be translated -->
+    <string name="invalid_directory_description">Please make sure that the directory you selected contains a cheats/, romfs/, or exefs/ folder and try again.</string>
+    <string name="addon_installed_successfully">Addon installed successfully</string>
+    <string name="verifying_content">Verifying content…</string>
+    <string name="content_install_notice">Content install notice</string>
+    <string name="content_install_notice_description">The content that you selected does not match this game.\nInstall anyway?</string>
+
     <!-- ROM loading errors -->
     <string name="loader_error_encrypted">Your ROM is encrypted</string>
     <string name="loader_error_encrypted_roms_description"><![CDATA[Please follow the guides to redump your <a href="https://yuzu-emu.org/help/quickstart/#dumping-physical-titles-game-cards">game cartidges</a> or <a href="https://yuzu-emu.org/help/quickstart/#dumping-digital-titles-eshop">installed titles</a>.]]></string>

From 2fce81202608cbf4b75fd46e19180fee60e47a53 Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Sun, 10 Dec 2023 20:38:58 -0500
Subject: [PATCH 04/20] android: Add per-game settings

---
 .../settings/model/view/SettingsItem.kt       | 11 +++
 .../features/settings/ui/SettingsActivity.kt  | 42 +++++++----
 .../features/settings/ui/SettingsAdapter.kt   |  6 ++
 .../features/settings/ui/SettingsFragment.kt  |  8 +-
 .../settings/ui/SettingsFragmentPresenter.kt  | 11 ++-
 .../ui/viewholder/DateTimeViewHolder.kt       | 12 +++
 .../ui/viewholder/RunnableViewHolder.kt       |  1 +
 .../ui/viewholder/SettingViewHolder.kt        |  2 +
 .../ui/viewholder/SingleChoiceViewHolder.kt   | 12 +++
 .../ui/viewholder/SliderViewHolder.kt         | 12 +++
 .../ui/viewholder/SubmenuViewHolder.kt        |  1 +
 .../ui/viewholder/SwitchSettingViewHolder.kt  | 14 +++-
 .../features/settings/utils/SettingsFile.kt   | 16 +++-
 .../yuzu_emu/fragments/EmulationFragment.kt   | 20 +++++
 .../GameFolderPropertiesDialogFragment.kt     |  6 ++
 .../yuzu/yuzu_emu/fragments/SetupFragment.kt  |  5 ++
 .../org/yuzu/yuzu_emu/model/GamesViewModel.kt |  1 +
 .../yuzu/yuzu_emu/model/SettingsViewModel.kt  |  4 -
 .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 14 +---
 .../yuzu_emu/utils/DirectoryInitialization.kt |  2 +-
 .../org/yuzu/yuzu_emu/utils/NativeConfig.kt   | 42 ++++++++---
 .../app/src/main/jni/native_config.cpp        | 46 +++++++++---
 .../src/main/res/layout/list_item_setting.xml | 10 +++
 .../res/layout/list_item_setting_switch.xml   | 73 ++++++++++++-------
 .../app/src/main/res/menu/menu_in_game.xml    |  5 ++
 .../res/navigation/emulation_navigation.xml   |  4 +
 .../main/res/navigation/home_navigation.xml   |  4 +
 27 files changed, 302 insertions(+), 82 deletions(-)

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
index 3845272946..28d8dea603 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
@@ -12,6 +12,7 @@ import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
 import org.yuzu.yuzu_emu.features.settings.model.IntSetting
 import org.yuzu.yuzu_emu.features.settings.model.LongSetting
 import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
+import org.yuzu.yuzu_emu.utils.NativeConfig
 
 /**
  * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
@@ -30,9 +31,19 @@ abstract class SettingsItem(
     val isEditable: Boolean
         get() {
             if (!NativeLibrary.isRunning()) return true
+
+            // Prevent editing settings that were modified in per-game config while editing global
+            // config
+            if (!NativeConfig.isPerGameConfigLoaded() && !setting.global) {
+                return false
+            }
             return setting.isRuntimeModifiable
         }
 
+    val needsRuntimeGlobal: Boolean
+        get() = NativeLibrary.isRunning() && !setting.global &&
+            !NativeConfig.isPerGameConfigLoaded()
+
     companion object {
         const val TYPE_HEADER = 0
         const val TYPE_SWITCH = 1
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
index 64bfc6dd04..6f072241a4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
@@ -19,10 +19,9 @@ import androidx.lifecycle.repeatOnLifecycle
 import androidx.navigation.fragment.NavHostFragment
 import androidx.navigation.navArgs
 import com.google.android.material.color.MaterialColors
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.launch
+import org.yuzu.yuzu_emu.NativeLibrary
 import java.io.IOException
 import org.yuzu.yuzu_emu.R
 import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
@@ -46,6 +45,9 @@ class SettingsActivity : AppCompatActivity() {
         binding = ActivitySettingsBinding.inflate(layoutInflater)
         setContentView(binding.root)
 
+        if (!NativeConfig.isPerGameConfigLoaded() && args.game != null) {
+            SettingsFile.loadCustomConfig(args.game!!)
+        }
         settingsViewModel.game = args.game
 
         val navHostFragment =
@@ -126,7 +128,6 @@ class SettingsActivity : AppCompatActivity() {
 
     override fun onStart() {
         super.onStart()
-        // TODO: Load custom settings contextually
         if (!DirectoryInitialization.areDirectoriesReady) {
             DirectoryInitialization.start()
         }
@@ -134,24 +135,35 @@ class SettingsActivity : AppCompatActivity() {
 
     override fun onStop() {
         super.onStop()
-        CoroutineScope(Dispatchers.IO).launch {
-            NativeConfig.saveSettings()
+        Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
+        if (isFinishing) {
+            NativeLibrary.applySettings()
+            if (args.game == null) {
+                NativeConfig.saveGlobalConfig()
+            } else if (NativeConfig.isPerGameConfigLoaded()) {
+                NativeLibrary.logSettings()
+                NativeConfig.savePerGameConfig()
+                NativeConfig.unloadPerGameConfig()
+            }
         }
     }
 
-    override fun onDestroy() {
-        settingsViewModel.clear()
-        super.onDestroy()
-    }
-
     fun onSettingsReset() {
         // Delete settings file because the user may have changed values that do not exist in the UI
-        NativeConfig.unloadConfig()
-        val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
-        if (!settingsFile.delete()) {
-            throw IOException("Failed to delete $settingsFile")
+        if (args.game == null) {
+            NativeConfig.unloadGlobalConfig()
+            val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
+            if (!settingsFile.delete()) {
+                throw IOException("Failed to delete $settingsFile")
+            }
+            NativeConfig.initializeGlobalConfig()
+        } else {
+            NativeConfig.unloadPerGameConfig()
+            val settingsFile = SettingsFile.getCustomSettingsFile(args.game!!)
+            if (!settingsFile.delete()) {
+                throw IOException("Failed to delete $settingsFile")
+            }
         }
-        NativeConfig.initializeConfig()
 
         Toast.makeText(
             applicationContext,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
index 3f23c064e2..be9b3031b7 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
@@ -196,6 +196,12 @@ class SettingsAdapter(
         return true
     }
 
+    fun onClearClick(item: SettingsItem, position: Int) {
+        item.setting.global = true
+        notifyItemChanged(position)
+        settingsViewModel.setShouldReloadSettingsList(true)
+    }
+
     private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() {
         override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
             return oldItem.setting.key == newItem.setting.key
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
index 769baf7444..d7ab0b5d9c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
@@ -66,7 +66,13 @@ class SettingsFragment : Fragment() {
             args.menuTag
         )
 
-        binding.toolbarSettingsLayout.title = getString(args.menuTag.titleId)
+        binding.toolbarSettingsLayout.title = if (args.menuTag == Settings.MenuTag.SECTION_ROOT &&
+            args.game != null
+        ) {
+            args.game!!.title
+        } else {
+            getString(args.menuTag.titleId)
+        }
         binding.listSettings.apply {
             adapter = settingsAdapter
             layoutManager = LinearLayoutManager(requireContext())
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
index 12a389b37a..a7e965589f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -7,6 +7,7 @@ import android.content.SharedPreferences
 import android.os.Build
 import android.widget.Toast
 import androidx.preference.PreferenceManager
+import org.yuzu.yuzu_emu.NativeLibrary
 import org.yuzu.yuzu_emu.R
 import org.yuzu.yuzu_emu.YuzuApplication
 import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
@@ -31,9 +32,17 @@ class SettingsFragmentPresenter(
     private val preferences: SharedPreferences
         get() = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
 
-    // Extension for populating settings list based on paired settings
+    // Extension for altering settings list based on each setting's properties
     fun ArrayList<SettingsItem>.add(key: String) {
         val item = SettingsItem.settingsItems[key]!!
+        if (settingsViewModel.game != null && !item.setting.isSwitchable) {
+            return
+        }
+
+        if (!NativeConfig.isPerGameConfigLoaded() && !NativeLibrary.isRunning()) {
+            item.setting.global = true
+        }
+
         val pairedSettingKey = item.setting.pairedSettingKey
         if (pairedSettingKey.isNotEmpty()) {
             val pairedSettingValue = NativeConfig.getBoolean(
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
index 4e159a7997..5ad0899ddf 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
@@ -13,6 +13,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
 import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting
 import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
 import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
+import org.yuzu.yuzu_emu.utils.NativeConfig
 
 class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
     SettingViewHolder(binding.root, adapter) {
@@ -35,6 +36,17 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
         val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
         binding.textSettingValue.text = dateFormatter.format(zonedTime)
 
+        binding.buttonClear.visibility = if (setting.setting.global ||
+            !NativeConfig.isPerGameConfigLoaded()
+        ) {
+            View.GONE
+        } else {
+            View.VISIBLE
+        }
+        binding.buttonClear.setOnClickListener {
+            adapter.onClearClick(setting, bindingAdapterPosition)
+        }
+
         setStyle(setting.isEditable, binding)
     }
 
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt
index 0361956244..5071842389 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt
@@ -38,6 +38,7 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
             binding.textSettingDescription.visibility = View.GONE
         }
         binding.textSettingValue.visibility = View.GONE
+        binding.buttonClear.visibility = View.GONE
 
         setStyle(setting.isEditable, binding)
     }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt
index 0fd1d2eaaa..d26887df81 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt
@@ -41,6 +41,7 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
         binding.textSettingName.alpha = opacity
         binding.textSettingDescription.alpha = opacity
         binding.textSettingValue.alpha = opacity
+        binding.buttonClear.isEnabled = isEditable
     }
 
     fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) {
@@ -48,5 +49,6 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
         val opacity = if (isEditable) 1.0f else 0.5f
         binding.textSettingName.alpha = opacity
         binding.textSettingDescription.alpha = opacity
+        binding.buttonClear.isEnabled = isEditable
     }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
index 28c4d17775..02dab37859 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
@@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
 import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
 import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
 import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
+import org.yuzu.yuzu_emu.utils.NativeConfig
 
 class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
     SettingViewHolder(binding.root, adapter) {
@@ -43,6 +44,17 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
             }
         }
 
+        binding.buttonClear.visibility = if (setting.setting.global ||
+            !NativeConfig.isPerGameConfigLoaded()
+        ) {
+            View.GONE
+        } else {
+            View.VISIBLE
+        }
+        binding.buttonClear.setOnClickListener {
+            adapter.onClearClick(setting, bindingAdapterPosition)
+        }
+
         setStyle(setting.isEditable, binding)
     }
 
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
index 67432f88ef..596c18012a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
@@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
 import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
 import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
 import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
+import org.yuzu.yuzu_emu.utils.NativeConfig
 
 class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
     SettingViewHolder(binding.root, adapter) {
@@ -30,6 +31,17 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
             setting.units
         )
 
+        binding.buttonClear.visibility = if (setting.setting.global ||
+            !NativeConfig.isPerGameConfigLoaded()
+        ) {
+            View.GONE
+        } else {
+            View.VISIBLE
+        }
+        binding.buttonClear.setOnClickListener {
+            adapter.onClearClick(setting, bindingAdapterPosition)
+        }
+
         setStyle(setting.isEditable, binding)
     }
 
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt
index 8100c65dd5..20d35a17da 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt
@@ -37,6 +37,7 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd
             binding.textSettingDescription.visibility = View.GONE
         }
         binding.textSettingValue.visibility = View.GONE
+        binding.buttonClear.visibility = View.GONE
     }
 
     override fun onClick(clicked: View) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
index 98ed888cb5..d26bf9374e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
@@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
 import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
 import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting
 import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
+import org.yuzu.yuzu_emu.utils.NativeConfig
 
 class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) :
     SettingViewHolder(binding.root, adapter) {
@@ -29,7 +30,18 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
         binding.switchWidget.setOnCheckedChangeListener(null)
         binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal)
         binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
-            adapter.onBooleanClick(item, binding.switchWidget.isChecked)
+            adapter.onBooleanClick(item, binding.switchWidget.isChecked, bindingAdapterPosition)
+        }
+
+        binding.buttonClear.visibility = if (setting.setting.global ||
+            !NativeConfig.isPerGameConfigLoaded()
+        ) {
+            View.GONE
+        } else {
+            View.VISIBLE
+        }
+        binding.buttonClear.setOnClickListener {
+            adapter.onClearClick(setting, bindingAdapterPosition)
         }
 
         setStyle(setting.isEditable, binding)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt
index 3ae5b4653f..5d523be67c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt
@@ -3,15 +3,27 @@
 
 package org.yuzu.yuzu_emu.features.settings.utils
 
+import android.net.Uri
+import org.yuzu.yuzu_emu.model.Game
 import java.io.*
 import org.yuzu.yuzu_emu.utils.DirectoryInitialization
+import org.yuzu.yuzu_emu.utils.FileUtil
+import org.yuzu.yuzu_emu.utils.NativeConfig
 
 /**
  * Contains static methods for interacting with .ini files in which settings are stored.
  */
 object SettingsFile {
-    const val FILE_NAME_CONFIG = "config"
+    const val FILE_NAME_CONFIG = "config.ini"
 
     fun getSettingsFile(fileName: String): File =
-        File(DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini")
+        File(DirectoryInitialization.userDirectory + "/config/" + fileName)
+
+    fun getCustomSettingsFile(game: Game): File =
+        File(DirectoryInitialization.userDirectory + "/config/custom/" + game.settingsName + ".ini")
+
+    fun loadCustomConfig(game: Game) {
+        val fileName = FileUtil.getFilename(Uri.parse(game.path))
+        NativeConfig.initializePerGameConfig(game.programId, fileName)
+    }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
index b09df7db34..6466442d50 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
@@ -52,6 +52,7 @@ import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
 import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
 import org.yuzu.yuzu_emu.features.settings.model.IntSetting
 import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
 import org.yuzu.yuzu_emu.model.DriverViewModel
 import org.yuzu.yuzu_emu.model.Game
 import org.yuzu.yuzu_emu.model.EmulationViewModel
@@ -127,6 +128,16 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
             return
         }
 
+        if (args.custom) {
+            SettingsFile.loadCustomConfig(args.game!!)
+            NativeConfig.unloadPerGameConfig()
+        } else {
+            NativeConfig.reloadGlobalConfig()
+        }
+
+        // Install the selected driver asynchronously as the game starts
+        driverViewModel.onLaunchGame()
+
         // So this fragment doesn't restart on configuration changes; i.e. rotation.
         retainInstance = true
         preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
@@ -217,6 +228,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
                     true
                 }
 
+                R.id.menu_settings_per_game -> {
+                    val action = HomeNavigationDirections.actionGlobalSettingsActivity(
+                        args.game,
+                        Settings.MenuTag.SECTION_ROOT
+                    )
+                    binding.root.findNavController().navigate(action)
+                    true
+                }
+
                 R.id.menu_overlay_controls -> {
                     showOverlayOptions()
                     true
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt
index b6c2e4635b..1ea1e036e6 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt
@@ -13,6 +13,7 @@ import org.yuzu.yuzu_emu.R
 import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
 import org.yuzu.yuzu_emu.model.GameDir
 import org.yuzu.yuzu_emu.model.GamesViewModel
+import org.yuzu.yuzu_emu.utils.NativeConfig
 import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
 
 class GameFolderPropertiesDialogFragment : DialogFragment() {
@@ -49,6 +50,11 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
             .show()
     }
 
+    override fun onStop() {
+        super.onStop()
+        NativeConfig.saveGlobalConfig()
+    }
+
     override fun onSaveInstanceState(outState: Bundle) {
         super.onSaveInstanceState(outState)
         outState.putBoolean(DEEP_SCAN, deepScan)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
index eb5edaa104..064342cdda 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
@@ -304,6 +304,11 @@ class SetupFragment : Fragment() {
         setInsets()
     }
 
+    override fun onStop() {
+        super.onStop()
+        NativeConfig.saveGlobalConfig()
+    }
+
     override fun onSaveInstanceState(outState: Bundle) {
         super.onSaveInstanceState(outState)
         if (_binding != null) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
index fd925235b9..eaec09b246 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
@@ -168,6 +168,7 @@ class GamesViewModel : ViewModel() {
     fun onCloseGameFoldersFragment() =
         viewModelScope.launch {
             withContext(Dispatchers.IO) {
+                NativeConfig.saveGlobalConfig()
                 getGameDirs(true)
             }
         }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt
index ccc981e950..5cb6a5d57f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt
@@ -68,8 +68,4 @@ class SettingsViewModel : ViewModel() {
     fun setAdapterItemChanged(value: Int) {
         _adapterItemChanged.value = value
     }
-
-    fun clear() {
-        game = null
-    }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
index 09ddd1bbd0..b4117d7610 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
@@ -28,12 +28,9 @@ import androidx.navigation.ui.setupWithNavController
 import androidx.preference.PreferenceManager
 import com.google.android.material.color.MaterialColors
 import com.google.android.material.navigation.NavigationBarView
-import kotlinx.coroutines.CoroutineScope
 import java.io.File
 import java.io.FilenameFilter
-import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
 import org.yuzu.yuzu_emu.HomeNavigationDirections
 import org.yuzu.yuzu_emu.NativeLibrary
 import org.yuzu.yuzu_emu.R
@@ -258,13 +255,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
         super.onResume()
     }
 
-    override fun onStop() {
-        super.onStop()
-        CoroutineScope(Dispatchers.IO).launch {
-            NativeConfig.saveSettings()
-        }
-    }
-
     override fun onDestroy() {
         EmulationActivity.stopForegroundService(this)
         super.onDestroy()
@@ -677,7 +667,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                 }
 
                 // Clear existing user data
-                NativeConfig.unloadConfig()
+                NativeConfig.unloadGlobalConfig()
                 File(DirectoryInitialization.userDirectory!!).deleteRecursively()
 
                 // Copy archive to internal storage
@@ -696,7 +686,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
 
                 // Reinitialize relevant data
                 NativeLibrary.initializeSystem(true)
-                NativeConfig.initializeConfig()
+                NativeConfig.initializeGlobalConfig()
                 gamesViewModel.reloadGames(false)
 
                 return@newInstance getString(R.string.user_data_import_success)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
index 21270fc843..0197fd712f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
@@ -16,7 +16,7 @@ object DirectoryInitialization {
         if (!areDirectoriesReady) {
             initializeInternalStorage()
             NativeLibrary.initializeSystem(false)
-            NativeConfig.initializeConfig()
+            NativeConfig.initializeGlobalConfig()
             areDirectoriesReady = true
         }
     }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
index 7d629b7d54..2d3d8ec795 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
@@ -7,30 +7,54 @@ import org.yuzu.yuzu_emu.model.GameDir
 
 object NativeConfig {
     /**
-     * Creates a Config object and opens the emulation config.
+     * Loads global config.
      */
     @Synchronized
-    external fun initializeConfig()
+    external fun initializeGlobalConfig()
 
     /**
-     * Destroys the stored config object. This automatically saves the existing config.
+     * Destroys the stored global config object. This does not save the existing config.
      */
     @Synchronized
-    external fun unloadConfig()
+    external fun unloadGlobalConfig()
 
     /**
-     * Reads values saved to the config file and saves them.
+     * Reads values in the global config file and saves them.
      */
     @Synchronized
-    external fun reloadSettings()
+    external fun reloadGlobalConfig()
 
     /**
-     * Saves settings values in memory to disk.
+     * Saves global settings values in memory to disk.
      */
     @Synchronized
-    external fun saveSettings()
+    external fun saveGlobalConfig()
 
-    external fun getBoolean(key: String, getDefault: Boolean): Boolean
+    /**
+     * Creates per-game config for the specified parameters. Must be unloaded once per-game config
+     * is closed with [unloadPerGameConfig]. All switchable values that [NativeConfig] gets/sets
+     * will follow the per-game config until the global config is reloaded.
+     *
+     * @param programId String representation of the u64 programId
+     * @param fileName Filename of the game, including its extension
+     */
+    @Synchronized
+    external fun initializePerGameConfig(programId: String, fileName: String)
+
+    @Synchronized
+    external fun isPerGameConfigLoaded(): Boolean
+
+    /**
+     * Saves per-game settings values in memory to disk.
+     */
+    @Synchronized
+    external fun savePerGameConfig()
+
+    /**
+     * Destroys the stored per-game config object. This does not save the config.
+     */
+    @Synchronized
+    external fun unloadPerGameConfig()
 
     @Synchronized
     external fun getBoolean(key: String, needsGlobal: Boolean): Boolean
diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp
index 7f2485720a..56989bde8d 100644
--- a/src/android/app/src/main/jni/native_config.cpp
+++ b/src/android/app/src/main/jni/native_config.cpp
@@ -3,6 +3,7 @@
 
 #include <string>
 
+#include <common/fs/fs_util.h>
 #include <jni.h>
 
 #include "android_config.h"
@@ -12,17 +13,19 @@
 #include "frontend_common/config.h"
 #include "jni/android_common/android_common.h"
 #include "jni/id_cache.h"
+#include "native.h"
 
-std::unique_ptr<AndroidConfig> config;
+std::unique_ptr<AndroidConfig> global_config;
+std::unique_ptr<AndroidConfig> per_game_config;
 
 template <typename T>
 Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) {
     auto key = GetJString(env, jkey);
     auto basicSetting = Settings::values.linkage.by_key[key];
-    auto basicAndroidSetting = AndroidSettings::values.linkage.by_key[key];
     if (basicSetting != 0) {
         return static_cast<Settings::Setting<T>*>(basicSetting);
     }
+    auto basicAndroidSetting = AndroidSettings::values.linkage.by_key[key];
     if (basicAndroidSetting != 0) {
         return static_cast<Settings::Setting<T>*>(basicAndroidSetting);
     }
@@ -32,20 +35,43 @@ Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) {
 
 extern "C" {
 
-void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeConfig(JNIEnv* env, jobject obj) {
-    config = std::make_unique<AndroidConfig>();
+void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeGlobalConfig(JNIEnv* env, jobject obj) {
+    global_config = std::make_unique<AndroidConfig>();
 }
 
-void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadConfig(JNIEnv* env, jobject obj) {
-    config.reset();
+void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadGlobalConfig(JNIEnv* env, jobject obj) {
+    global_config.reset();
 }
 
-void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_reloadSettings(JNIEnv* env, jobject obj) {
-    config->AndroidConfig::ReloadAllValues();
+void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_reloadGlobalConfig(JNIEnv* env, jobject obj) {
+    global_config->AndroidConfig::ReloadAllValues();
 }
 
-void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveSettings(JNIEnv* env, jobject obj) {
-    config->AndroidConfig::SaveAllValues();
+void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveGlobalConfig(JNIEnv* env, jobject obj) {
+    global_config->AndroidConfig::SaveAllValues();
+}
+
+void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializePerGameConfig(JNIEnv* env, jobject obj,
+                                                                        jstring jprogramId,
+                                                                        jstring jfileName) {
+    auto program_id = EmulationSession::GetProgramId(env, jprogramId);
+    auto file_name = GetJString(env, jfileName);
+    const auto config_file_name = program_id == 0 ? file_name : fmt::format("{:016X}", program_id);
+    per_game_config =
+        std::make_unique<AndroidConfig>(config_file_name, Config::ConfigType::PerGameConfig);
+}
+
+jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_isPerGameConfigLoaded(JNIEnv* env,
+                                                                          jobject obj) {
+    return per_game_config != nullptr;
+}
+
+void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_savePerGameConfig(JNIEnv* env, jobject obj) {
+    per_game_config->AndroidConfig::SaveAllValues();
+}
+
+void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadPerGameConfig(JNIEnv* env, jobject obj) {
+    per_game_config.reset();
 }
 
 jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getBoolean(JNIEnv* env, jobject obj,
diff --git a/src/android/app/src/main/res/layout/list_item_setting.xml b/src/android/app/src/main/res/layout/list_item_setting.xml
index 544280e753..1f80682f10 100644
--- a/src/android/app/src/main/res/layout/list_item_setting.xml
+++ b/src/android/app/src/main/res/layout/list_item_setting.xml
@@ -62,6 +62,16 @@
                 android:textSize="13sp"
                 tools:text="1x" />
 
+            <com.google.android.material.button.MaterialButton
+                android:id="@+id/button_clear"
+                style="@style/Widget.Material3.Button.TonalButton"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="16dp"
+                android:visibility="gone"
+                android:text="@string/clear"
+                tools:visibility="visible" />
+
         </LinearLayout>
 
     </LinearLayout>
diff --git a/src/android/app/src/main/res/layout/list_item_setting_switch.xml b/src/android/app/src/main/res/layout/list_item_setting_switch.xml
index a8f5aff78b..5cb84182e0 100644
--- a/src/android/app/src/main/res/layout/list_item_setting_switch.xml
+++ b/src/android/app/src/main/res/layout/list_item_setting_switch.xml
@@ -10,41 +10,62 @@
     android:minHeight="72dp"
     android:padding="16dp">
 
-    <com.google.android.material.materialswitch.MaterialSwitch
-        android:id="@+id/switch_widget"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:layout_alignParentEnd="true"
-        android:layout_centerVertical="true" />
-
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_alignParentTop="true"
-        android:layout_centerVertical="true"
-        android:layout_marginEnd="24dp"
-        android:layout_toStartOf="@+id/switch_widget"
-        android:gravity="center_vertical"
         android:orientation="vertical">
 
-        <com.google.android.material.textview.MaterialTextView
-            android:id="@+id/text_setting_name"
-            style="@style/TextAppearance.Material3.HeadlineMedium"
-            android:layout_width="wrap_content"
+        <LinearLayout
+            android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:textAlignment="viewStart"
-            android:textSize="17sp"
-            app:lineHeight="28dp"
-            tools:text="@string/frame_limit_enable" />
+            android:orientation="horizontal">
 
-        <com.google.android.material.textview.MaterialTextView
-            android:id="@+id/text_setting_description"
-            style="@style/TextAppearance.Material3.BodySmall"
+            <LinearLayout
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginEnd="24dp"
+                android:gravity="center_vertical"
+                android:orientation="vertical"
+                android:layout_weight="1">
+
+                <com.google.android.material.textview.MaterialTextView
+                    android:id="@+id/text_setting_name"
+                    style="@style/TextAppearance.Material3.HeadlineMedium"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:textAlignment="viewStart"
+                    android:textSize="17sp"
+                    app:lineHeight="28dp"
+                    tools:text="@string/frame_limit_enable" />
+
+                <com.google.android.material.textview.MaterialTextView
+                    android:id="@+id/text_setting_description"
+                    style="@style/TextAppearance.Material3.BodySmall"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="@dimen/spacing_small"
+                    android:textAlignment="viewStart"
+                    tools:text="@string/frame_limit_enable_description" />
+
+            </LinearLayout>
+
+            <com.google.android.material.materialswitch.MaterialSwitch
+                android:id="@+id/switch_widget"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_vertical"/>
+
+        </LinearLayout>
+
+        <com.google.android.material.button.MaterialButton
+            android:id="@+id/button_clear"
+            style="@style/Widget.Material3.Button.TonalButton"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_marginTop="@dimen/spacing_small"
-            android:textAlignment="viewStart"
-            tools:text="@string/frame_limit_enable_description" />
+            android:layout_marginTop="16dp"
+            android:text="@string/clear"
+            android:visibility="gone"
+            tools:visibility="visible" />
 
     </LinearLayout>
 
diff --git a/src/android/app/src/main/res/menu/menu_in_game.xml b/src/android/app/src/main/res/menu/menu_in_game.xml
index f98f727b66..ac6ab06ffc 100644
--- a/src/android/app/src/main/res/menu/menu_in_game.xml
+++ b/src/android/app/src/main/res/menu/menu_in_game.xml
@@ -11,6 +11,11 @@
         android:icon="@drawable/ic_settings"
         android:title="@string/preferences_settings" />
 
+    <item
+        android:id="@+id/menu_settings_per_game"
+        android:icon="@drawable/ic_settings_outline"
+        android:title="@string/per_game_settings" />
+
     <item
         android:id="@+id/menu_overlay_controls"
         android:icon="@drawable/ic_controller"
diff --git a/src/android/app/src/main/res/navigation/emulation_navigation.xml b/src/android/app/src/main/res/navigation/emulation_navigation.xml
index cfc494b3f1..2f8c3fa0dd 100644
--- a/src/android/app/src/main/res/navigation/emulation_navigation.xml
+++ b/src/android/app/src/main/res/navigation/emulation_navigation.xml
@@ -15,6 +15,10 @@
             app:argType="org.yuzu.yuzu_emu.model.Game"
             app:nullable="true"
             android:defaultValue="@null" />
+        <argument
+            android:name="custom"
+            app:argType="boolean"
+            android:defaultValue="false" />
     </fragment>
 
     <activity
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 1c69bf0db5..226cf56001 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -77,6 +77,10 @@
             app:argType="org.yuzu.yuzu_emu.model.Game"
             app:nullable="true"
             android:defaultValue="@null" />
+        <argument
+            android:name="custom"
+            app:argType="boolean"
+            android:defaultValue="false" />
     </activity>
 
     <action

From f2eb3c579f2de15410681014b47779d44d77fd48 Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Sun, 10 Dec 2023 20:45:02 -0500
Subject: [PATCH 05/20] android: Add per-game drivers

---
 .../yuzu/yuzu_emu/adapters/DriverAdapter.kt   |   2 +-
 .../fragments/DriverManagerFragment.kt        |  14 +-
 .../fragments/DriversLoadingDialogFragment.kt |  18 +-
 .../yuzu_emu/fragments/EmulationFragment.kt   |  24 ++-
 .../fragments/GamePropertiesFragment.kt       |  15 ++
 .../fragments/HomeSettingsFragment.kt         |  17 +-
 .../yuzu/yuzu_emu/model/DriverViewModel.kt    | 175 ++++++++++++------
 .../yuzu/yuzu_emu/utils/GpuDriverHelper.kt    |  10 +-
 .../app/src/main/jni/android_config.cpp       |  19 ++
 src/android/app/src/main/jni/android_config.h |   2 +
 .../app/src/main/jni/android_settings.h       |   3 +
 .../main/res/navigation/home_navigation.xml   |  11 +-
 src/common/settings.cpp                       |   2 +
 src/common/settings_common.h                  |   1 +
 14 files changed, 218 insertions(+), 95 deletions(-)

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
index 0e818cab99..d290a656c0 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
@@ -42,7 +42,7 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) :
         if (driverViewModel.selectedDriver > position) {
             driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1)
         }
-        if (GpuDriverHelper.customDriverData == driverData.second) {
+        if (GpuDriverHelper.customDriverSettingData == driverData.second) {
             driverViewModel.setSelectedDriverIndex(0)
         }
         driverViewModel.driversToDelete.add(driverData.first)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
index df21d74b2c..cc71254dc5 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
@@ -15,6 +15,7 @@ import androidx.fragment.app.Fragment
 import androidx.fragment.app.activityViewModels
 import androidx.lifecycle.lifecycleScope
 import androidx.navigation.findNavController
+import androidx.navigation.fragment.navArgs
 import androidx.recyclerview.widget.GridLayoutManager
 import com.google.android.material.transition.MaterialSharedAxis
 import kotlinx.coroutines.flow.collectLatest
@@ -36,6 +37,8 @@ class DriverManagerFragment : Fragment() {
     private val homeViewModel: HomeViewModel by activityViewModels()
     private val driverViewModel: DriverViewModel by activityViewModels()
 
+    private val args by navArgs<DriverManagerFragmentArgs>()
+
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
@@ -57,7 +60,9 @@ class DriverManagerFragment : Fragment() {
         homeViewModel.setNavigationVisibility(visible = false, animated = true)
         homeViewModel.setStatusBarShadeVisibility(visible = false)
 
-        if (!driverViewModel.isInteractionAllowed) {
+        driverViewModel.onOpenDriverManager(args.game)
+
+        if (!driverViewModel.isInteractionAllowed.value) {
             DriversLoadingDialogFragment().show(
                 childFragmentManager,
                 DriversLoadingDialogFragment.TAG
@@ -102,10 +107,9 @@ class DriverManagerFragment : Fragment() {
         setInsets()
     }
 
-    // Start installing requested driver
-    override fun onStop() {
-        super.onStop()
-        driverViewModel.onCloseDriverManager()
+    override fun onDestroy() {
+        super.onDestroy()
+        driverViewModel.onCloseDriverManager(args.game)
     }
 
     private fun setInsets() =
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt
index f8c34346a6..6a47b29f0d 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt
@@ -47,25 +47,9 @@ class DriversLoadingDialogFragment : DialogFragment() {
         viewLifecycleOwner.lifecycleScope.apply {
             launch {
                 repeatOnLifecycle(Lifecycle.State.RESUMED) {
-                    driverViewModel.areDriversLoading.collect { checkForDismiss() }
+                    driverViewModel.isInteractionAllowed.collect { if (it) dismiss() }
                 }
             }
-            launch {
-                repeatOnLifecycle(Lifecycle.State.RESUMED) {
-                    driverViewModel.isDriverReady.collect { checkForDismiss() }
-                }
-            }
-            launch {
-                repeatOnLifecycle(Lifecycle.State.RESUMED) {
-                    driverViewModel.isDeletingDrivers.collect { checkForDismiss() }
-                }
-            }
-        }
-    }
-
-    private fun checkForDismiss() {
-        if (driverViewModel.isInteractionAllowed) {
-            dismiss()
         }
     }
 
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
index 6466442d50..c40f5f41a1 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
@@ -352,15 +352,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
             }
             launch {
                 repeatOnLifecycle(Lifecycle.State.RESUMED) {
-                    driverViewModel.isDriverReady.collect {
-                        if (it && !emulationState.isRunning) {
-                            if (!DirectoryInitialization.areDirectoriesReady) {
-                                DirectoryInitialization.start()
-                            }
-
-                            updateScreenLayout()
-
-                            emulationState.run(emulationActivity!!.isActivityRecreated)
+                    driverViewModel.isInteractionAllowed.collect {
+                        if (it) {
+                            onEmulationStart()
                         }
                     }
                 }
@@ -368,6 +362,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
         }
     }
 
+    private fun onEmulationStart() {
+        if (!NativeLibrary.isRunning() && !NativeLibrary.isPaused()) {
+            if (!DirectoryInitialization.areDirectoriesReady) {
+                DirectoryInitialization.start()
+            }
+
+            updateScreenLayout()
+
+            emulationState.run(emulationActivity!!.isActivityRecreated)
+        }
+    }
+
     override fun onConfigurationChanged(newConfig: Configuration) {
         super.onConfigurationChanged(newConfig)
         if (_binding == null) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
index 485989e2e3..e062425a15 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
@@ -148,6 +148,21 @@ class GamePropertiesFragment : Fragment() {
                 }
             )
 
+            if (GpuDriverHelper.supportsCustomDriverLoading()) {
+                add(
+                    SubmenuProperty(
+                        R.string.gpu_driver_manager,
+                        R.string.install_gpu_driver_description,
+                        R.drawable.ic_build,
+                        detailsFlow = driverViewModel.selectedDriverTitle
+                    ) {
+                        val action = GamePropertiesFragmentDirections
+                            .actionPerGamePropertiesFragmentToDriverManagerFragment(args.game)
+                        binding.root.findNavController().navigate(action)
+                    }
+                )
+            }
+
             if (!args.game.isHomebrew) {
                 add(
                     SubmenuProperty(
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
index 3addc2e63b..6ddd758e67 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
@@ -68,6 +68,9 @@ class HomeSettingsFragment : Fragment() {
     }
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        homeViewModel.setNavigationVisibility(visible = true, animated = true)
+        homeViewModel.setStatusBarShadeVisibility(visible = true)
         mainActivity = requireActivity() as MainActivity
 
         val optionsList: MutableList<HomeSetting> = mutableListOf<HomeSetting>().apply {
@@ -91,13 +94,14 @@ class HomeSettingsFragment : Fragment() {
                     R.string.install_gpu_driver_description,
                     R.drawable.ic_build,
                     {
-                        binding.root.findNavController()
-                            .navigate(R.id.action_homeSettingsFragment_to_driverManagerFragment)
+                        val action = HomeSettingsFragmentDirections
+                            .actionHomeSettingsFragmentToDriverManagerFragment(null)
+                        binding.root.findNavController().navigate(action)
                     },
                     { GpuDriverHelper.supportsCustomDriverLoading() },
                     R.string.custom_driver_not_supported,
                     R.string.custom_driver_not_supported_description,
-                    driverViewModel.selectedDriverMetadata
+                    driverViewModel.selectedDriverTitle
                 )
             )
             add(
@@ -212,8 +216,11 @@ class HomeSettingsFragment : Fragment() {
     override fun onStart() {
         super.onStart()
         exitTransition = null
-        homeViewModel.setNavigationVisibility(visible = true, animated = true)
-        homeViewModel.setStatusBarShadeVisibility(visible = true)
+    }
+
+    override fun onResume() {
+        super.onResume()
+        driverViewModel.updateDriverNameForGame(null)
     }
 
     override fun onDestroyView() {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt
index 62945ad650..76accf8f31 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt
@@ -7,81 +7,83 @@ import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import org.yuzu.yuzu_emu.R
 import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.features.settings.model.StringSetting
+import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
 import org.yuzu.yuzu_emu.utils.FileUtil
 import org.yuzu.yuzu_emu.utils.GpuDriverHelper
 import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
+import org.yuzu.yuzu_emu.utils.NativeConfig
 import java.io.BufferedOutputStream
 import java.io.File
 
 class DriverViewModel : ViewModel() {
     private val _areDriversLoading = MutableStateFlow(false)
-    val areDriversLoading: StateFlow<Boolean> get() = _areDriversLoading
-
     private val _isDriverReady = MutableStateFlow(true)
-    val isDriverReady: StateFlow<Boolean> get() = _isDriverReady
-
     private val _isDeletingDrivers = MutableStateFlow(false)
-    val isDeletingDrivers: StateFlow<Boolean> get() = _isDeletingDrivers
 
-    private val _driverList = MutableStateFlow(mutableListOf<Pair<String, GpuDriverMetadata>>())
+    val isInteractionAllowed: StateFlow<Boolean> =
+        combine(
+            _areDriversLoading,
+            _isDriverReady,
+            _isDeletingDrivers
+        ) { loading, ready, deleting ->
+            !loading && ready && !deleting
+        }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = false)
+
+    private val _driverList = MutableStateFlow(GpuDriverHelper.getDrivers())
     val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList
 
     var previouslySelectedDriver = 0
     var selectedDriver = -1
 
-    private val _selectedDriverMetadata =
-        MutableStateFlow(
-            GpuDriverHelper.customDriverData.name
-                ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
-        )
-    val selectedDriverMetadata: StateFlow<String> get() = _selectedDriverMetadata
+    // Used for showing which driver is currently installed within the driver manager card
+    private val _selectedDriverTitle = MutableStateFlow("")
+    val selectedDriverTitle: StateFlow<String> get() = _selectedDriverTitle
 
     private val _newDriverInstalled = MutableStateFlow(false)
     val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled
 
     val driversToDelete = mutableListOf<String>()
 
-    val isInteractionAllowed
-        get() = !areDriversLoading.value && isDriverReady.value && !isDeletingDrivers.value
-
     init {
-        _areDriversLoading.value = true
-        viewModelScope.launch {
-            withContext(Dispatchers.IO) {
-                val drivers = GpuDriverHelper.getDrivers()
-                val currentDriverMetadata = GpuDriverHelper.customDriverData
-                for (i in drivers.indices) {
-                    if (drivers[i].second == currentDriverMetadata) {
-                        setSelectedDriverIndex(i)
-                        break
-                    }
-                }
+        val currentDriverMetadata = GpuDriverHelper.installedCustomDriverData
+        findSelectedDriver(currentDriverMetadata)
 
-                // If a user had installed a driver before the manager was implemented, this zips
-                // the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can
-                // be indexed and exported as expected.
-                if (selectedDriver == -1) {
-                    val driverToSave =
-                        File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip")
-                    driverToSave.createNewFile()
-                    FileUtil.zipFromInternalStorage(
-                        File(GpuDriverHelper.driverInstallationPath!!),
-                        GpuDriverHelper.driverInstallationPath!!,
-                        BufferedOutputStream(driverToSave.outputStream())
-                    )
-                    drivers.add(Pair(driverToSave.path, currentDriverMetadata))
-                    setSelectedDriverIndex(drivers.size - 1)
-                }
-
-                _driverList.value = drivers
-                _areDriversLoading.value = false
-            }
+        // If a user had installed a driver before the manager was implemented, this zips
+        // the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can
+        // be indexed and exported as expected.
+        if (selectedDriver == -1) {
+            val driverToSave =
+                File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip")
+            driverToSave.createNewFile()
+            FileUtil.zipFromInternalStorage(
+                File(GpuDriverHelper.driverInstallationPath!!),
+                GpuDriverHelper.driverInstallationPath!!,
+                BufferedOutputStream(driverToSave.outputStream())
+            )
+            _driverList.value.add(Pair(driverToSave.path, currentDriverMetadata))
+            setSelectedDriverIndex(_driverList.value.size - 1)
         }
+
+        // If a user had installed a driver before the config was reworked to be multiplatform,
+        // we have save the path of the previously selected driver to the new setting.
+        if (StringSetting.DRIVER_PATH.getString(true).isEmpty() && selectedDriver > 0 &&
+            StringSetting.DRIVER_PATH.global
+        ) {
+            StringSetting.DRIVER_PATH.setString(_driverList.value[selectedDriver].first)
+            NativeConfig.saveGlobalConfig()
+        } else {
+            findSelectedDriver(GpuDriverHelper.customDriverSettingData)
+        }
+        updateDriverNameForGame(null)
     }
 
     fun setSelectedDriverIndex(value: Int) {
@@ -98,9 +100,9 @@ class DriverViewModel : ViewModel() {
     fun addDriver(driverData: Pair<String, GpuDriverMetadata>) {
         val driverIndex = _driverList.value.indexOfFirst { it == driverData }
         if (driverIndex == -1) {
-            setSelectedDriverIndex(_driverList.value.size)
             _driverList.value.add(driverData)
-            _selectedDriverMetadata.value = driverData.second.name
+            setSelectedDriverIndex(_driverList.value.size - 1)
+            _selectedDriverTitle.value = driverData.second.name
                 ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
         } else {
             setSelectedDriverIndex(driverIndex)
@@ -111,8 +113,31 @@ class DriverViewModel : ViewModel() {
         _driverList.value.remove(driverData)
     }
 
-    fun onCloseDriverManager() {
+    fun onOpenDriverManager(game: Game?) {
+        if (game != null) {
+            SettingsFile.loadCustomConfig(game)
+        }
+
+        val driverPath = StringSetting.DRIVER_PATH.getString()
+        if (driverPath.isEmpty()) {
+            setSelectedDriverIndex(0)
+        } else {
+            findSelectedDriver(GpuDriverHelper.getMetadataFromZip(File(driverPath)))
+        }
+    }
+
+    fun onCloseDriverManager(game: Game?) {
         _isDeletingDrivers.value = true
+        StringSetting.DRIVER_PATH.setString(driverList.value[selectedDriver].first)
+        updateDriverNameForGame(game)
+        if (game == null) {
+            NativeConfig.saveGlobalConfig()
+        } else {
+            NativeConfig.savePerGameConfig()
+            NativeConfig.unloadPerGameConfig()
+            NativeConfig.reloadGlobalConfig()
+        }
+
         viewModelScope.launch {
             withContext(Dispatchers.IO) {
                 driversToDelete.forEach {
@@ -125,23 +150,29 @@ class DriverViewModel : ViewModel() {
                 _isDeletingDrivers.value = false
             }
         }
+    }
 
-        if (GpuDriverHelper.customDriverData == driverList.value[selectedDriver].second) {
+    // It is the Emulation Fragment's responsibility to load per-game settings so that this function
+    // knows what driver to load.
+    fun onLaunchGame() {
+        _isDriverReady.value = false
+
+        val selectedDriverFile = File(StringSetting.DRIVER_PATH.getString())
+        val selectedDriverMetadata = GpuDriverHelper.customDriverSettingData
+        if (GpuDriverHelper.installedCustomDriverData == selectedDriverMetadata) {
             return
         }
 
-        _isDriverReady.value = false
         viewModelScope.launch {
             withContext(Dispatchers.IO) {
-                if (selectedDriver == 0) {
+                if (selectedDriverMetadata.name == null) {
                     GpuDriverHelper.installDefaultDriver()
                     setDriverReady()
                     return@withContext
                 }
 
-                val driverToInstall = File(driverList.value[selectedDriver].first)
-                if (driverToInstall.exists()) {
-                    GpuDriverHelper.installCustomDriver(driverToInstall)
+                if (selectedDriverFile.exists()) {
+                    GpuDriverHelper.installCustomDriver(selectedDriverFile)
                 } else {
                     GpuDriverHelper.installDefaultDriver()
                 }
@@ -150,9 +181,43 @@ class DriverViewModel : ViewModel() {
         }
     }
 
+    private fun findSelectedDriver(currentDriverMetadata: GpuDriverMetadata) {
+        if (driverList.value.size == 1) {
+            setSelectedDriverIndex(0)
+            return
+        }
+
+        driverList.value.forEachIndexed { i: Int, driver: Pair<String, GpuDriverMetadata> ->
+            if (driver.second == currentDriverMetadata) {
+                setSelectedDriverIndex(i)
+                return
+            }
+        }
+    }
+
+    fun updateDriverNameForGame(game: Game?) {
+        if (!GpuDriverHelper.supportsCustomDriverLoading()) {
+            return
+        }
+
+        if (game == null || NativeConfig.isPerGameConfigLoaded()) {
+            updateName()
+        } else {
+            SettingsFile.loadCustomConfig(game)
+            updateName()
+            NativeConfig.unloadPerGameConfig()
+            NativeConfig.reloadGlobalConfig()
+        }
+    }
+
+    private fun updateName() {
+        _selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name
+            ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
+    }
+
     private fun setDriverReady() {
         _isDriverReady.value = true
-        _selectedDriverMetadata.value = GpuDriverHelper.customDriverData.name
+        _selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name
             ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
     }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
index f6882ce6ce..685272288d 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
@@ -10,6 +10,8 @@ import java.io.File
 import java.io.IOException
 import org.yuzu.yuzu_emu.NativeLibrary
 import org.yuzu.yuzu_emu.YuzuApplication
+import org.yuzu.yuzu_emu.features.settings.model.StringSetting
+import java.io.FileNotFoundException
 import java.util.zip.ZipException
 import java.util.zip.ZipFile
 
@@ -44,7 +46,7 @@ object GpuDriverHelper {
         NativeLibrary.initializeGpuDriver(
             hookLibPath,
             driverInstallationPath,
-            customDriverData.libraryName,
+            installedCustomDriverData.libraryName,
             fileRedirectionPath
         )
     }
@@ -190,6 +192,7 @@ object GpuDriverHelper {
                 }
             }
         } catch (_: ZipException) {
+        } catch (_: FileNotFoundException) {
         }
         return GpuDriverMetadata()
     }
@@ -197,9 +200,12 @@ object GpuDriverHelper {
     external fun supportsCustomDriverLoading(): Boolean
 
     // Parse the custom driver metadata to retrieve the name.
-    val customDriverData: GpuDriverMetadata
+    val installedCustomDriverData: GpuDriverMetadata
         get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME))
 
+    val customDriverSettingData: GpuDriverMetadata
+        get() = getMetadataFromZip(File(StringSetting.DRIVER_PATH.getString()))
+
     fun initializeDirectories() {
         // Ensure the file redirection directory exists.
         val fileRedirectionDir = File(fileRedirectionPath!!)
diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp
index 767d8ea839..9c3a5a9b29 100644
--- a/src/android/app/src/main/jni/android_config.cpp
+++ b/src/android/app/src/main/jni/android_config.cpp
@@ -36,6 +36,7 @@ void AndroidConfig::ReadAndroidValues() {
         ReadAndroidUIValues();
         ReadUIValues();
     }
+    ReadDriverValues();
 }
 
 void AndroidConfig::ReadAndroidUIValues() {
@@ -57,6 +58,7 @@ void AndroidConfig::ReadUIValues() {
 void AndroidConfig::ReadPathValues() {
     BeginGroup(Settings::TranslateCategory(Settings::Category::Paths));
 
+    AndroidSettings::values.game_dirs.clear();
     const int gamedirs_size = BeginArray(std::string("gamedirs"));
     for (int i = 0; i < gamedirs_size; ++i) {
         SetArrayIndex(i);
@@ -71,11 +73,20 @@ void AndroidConfig::ReadPathValues() {
     EndGroup();
 }
 
+void AndroidConfig::ReadDriverValues() {
+    BeginGroup(Settings::TranslateCategory(Settings::Category::GpuDriver));
+
+    ReadCategory(Settings::Category::GpuDriver);
+
+    EndGroup();
+}
+
 void AndroidConfig::SaveAndroidValues() {
     if (global) {
         SaveAndroidUIValues();
         SaveUIValues();
     }
+    SaveDriverValues();
 
     WriteToIni();
 }
@@ -111,6 +122,14 @@ void AndroidConfig::SavePathValues() {
     EndGroup();
 }
 
+void AndroidConfig::SaveDriverValues() {
+    BeginGroup(Settings::TranslateCategory(Settings::Category::GpuDriver));
+
+    WriteCategory(Settings::Category::GpuDriver);
+
+    EndGroup();
+}
+
 std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) {
     auto& map = Settings::values.linkage.by_category;
     if (map.contains(category)) {
diff --git a/src/android/app/src/main/jni/android_config.h b/src/android/app/src/main/jni/android_config.h
index f490be016a..2c12874e15 100644
--- a/src/android/app/src/main/jni/android_config.h
+++ b/src/android/app/src/main/jni/android_config.h
@@ -17,6 +17,7 @@ public:
 protected:
     void ReadAndroidValues();
     void ReadAndroidUIValues();
+    void ReadDriverValues();
     void ReadHidbusValues() override {}
     void ReadDebugControlValues() override {}
     void ReadPathValues() override;
@@ -28,6 +29,7 @@ protected:
 
     void SaveAndroidValues();
     void SaveAndroidUIValues();
+    void SaveDriverValues();
     void SaveHidbusValues() override {}
     void SaveDebugControlValues() override {}
     void SavePathValues() override;
diff --git a/src/android/app/src/main/jni/android_settings.h b/src/android/app/src/main/jni/android_settings.h
index fc05232064..3733f5a3c5 100644
--- a/src/android/app/src/main/jni/android_settings.h
+++ b/src/android/app/src/main/jni/android_settings.h
@@ -30,6 +30,9 @@ struct Values {
                                          Settings::Specialization::Default,
                                          true,
                                          true};
+
+    Settings::SwitchableSetting<std::string, false> driver_path{linkage, "", "driver_path",
+                                                                Settings::Category::GpuDriver};
 };
 
 extern Values values;
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 226cf56001..37a03a8d1a 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -111,7 +111,13 @@
     <fragment
         android:id="@+id/driverManagerFragment"
         android:name="org.yuzu.yuzu_emu.fragments.DriverManagerFragment"
-        android:label="DriverManagerFragment" />
+        android:label="DriverManagerFragment" >
+        <argument
+            android:name="game"
+            app:argType="org.yuzu.yuzu_emu.model.Game"
+            app:nullable="true"
+            android:defaultValue="@null" />
+    </fragment>
     <fragment
         android:id="@+id/appletLauncherFragment"
         android:name="org.yuzu.yuzu_emu.fragments.AppletLauncherFragment"
@@ -141,6 +147,9 @@
         <action
             android:id="@+id/action_perGamePropertiesFragment_to_addonsFragment"
             app:destination="@id/addonsFragment" />
+        <action
+            android:id="@+id/action_perGamePropertiesFragment_to_driverManagerFragment"
+            app:destination="@id/driverManagerFragment" />
     </fragment>
     <action
         android:id="@+id/action_global_perGamePropertiesFragment"
diff --git a/src/common/settings.cpp b/src/common/settings.cpp
index 88f509ba78..ea52bbfa67 100644
--- a/src/common/settings.cpp
+++ b/src/common/settings.cpp
@@ -211,6 +211,8 @@ const char* TranslateCategory(Category category) {
     case Category::Debugging:
     case Category::DebuggingGraphics:
         return "Debugging";
+    case Category::GpuDriver:
+        return "GpuDriver";
     case Category::Miscellaneous:
         return "Miscellaneous";
     case Category::Network:
diff --git a/src/common/settings_common.h b/src/common/settings_common.h
index 344c044394..c82e174959 100644
--- a/src/common/settings_common.h
+++ b/src/common/settings_common.h
@@ -26,6 +26,7 @@ enum class Category : u32 {
     DataStorage,
     Debugging,
     DebuggingGraphics,
+    GpuDriver,
     Miscellaneous,
     Network,
     WebService,

From 62fc386bb4fa5c07edfe07b8735772f37f188bd7 Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Sun, 10 Dec 2023 20:45:45 -0500
Subject: [PATCH 06/20] settings: Allow CPU Debug and Fastmem to be changed
 per-game

---
 src/common/settings.h | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/common/settings.h b/src/common/settings.h
index 7dc18fffe7..25086d3886 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -197,7 +197,7 @@ struct Values {
     SwitchableSetting<CpuAccuracy, true> cpu_accuracy{linkage,           CpuAccuracy::Auto,
                                                       CpuAccuracy::Auto, CpuAccuracy::Paranoid,
                                                       "cpu_accuracy",    Category::Cpu};
-    Setting<bool> cpu_debug_mode{linkage, false, "cpu_debug_mode", Category::CpuDebug};
+    SwitchableSetting<bool> cpu_debug_mode{linkage, false, "cpu_debug_mode", Category::CpuDebug};
 
     Setting<bool> cpuopt_page_tables{linkage, true, "cpuopt_page_tables", Category::CpuDebug};
     Setting<bool> cpuopt_block_linking{linkage, true, "cpuopt_block_linking", Category::CpuDebug};
@@ -211,9 +211,9 @@ struct Values {
     Setting<bool> cpuopt_misc_ir{linkage, true, "cpuopt_misc_ir", Category::CpuDebug};
     Setting<bool> cpuopt_reduce_misalign_checks{linkage, true, "cpuopt_reduce_misalign_checks",
                                                 Category::CpuDebug};
-    Setting<bool> cpuopt_fastmem{linkage, true, "cpuopt_fastmem", Category::CpuDebug};
-    Setting<bool> cpuopt_fastmem_exclusives{linkage, true, "cpuopt_fastmem_exclusives",
-                                            Category::CpuDebug};
+    SwitchableSetting<bool> cpuopt_fastmem{linkage, true, "cpuopt_fastmem", Category::CpuDebug};
+    SwitchableSetting<bool> cpuopt_fastmem_exclusives{linkage, true, "cpuopt_fastmem_exclusives",
+                                                      Category::CpuDebug};
     Setting<bool> cpuopt_recompile_exclusives{linkage, true, "cpuopt_recompile_exclusives",
                                               Category::CpuDebug};
     Setting<bool> cpuopt_ignore_memory_aborts{linkage, true, "cpuopt_ignore_memory_aborts",

From dbddc627d4167e5bb6cc0c63a998c0a7e7890396 Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Sun, 10 Dec 2023 20:46:49 -0500
Subject: [PATCH 07/20] android: Add JNI initialization information for Game
 class

Unused in this PR, but will be useful later
---
 src/android/app/src/main/jni/id_cache.cpp | 55 +++++++++++++++++++++++
 src/android/app/src/main/jni/id_cache.h   |  9 ++++
 2 files changed, 64 insertions(+)

diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp
index df89351783..e7a86d3fd0 100644
--- a/src/android/app/src/main/jni/id_cache.cpp
+++ b/src/android/app/src/main/jni/id_cache.cpp
@@ -20,6 +20,15 @@ static jmethodID s_disk_cache_load_progress;
 static jmethodID s_on_emulation_started;
 static jmethodID s_on_emulation_stopped;
 
+static jclass s_game_class;
+static jmethodID s_game_constructor;
+static jfieldID s_game_title_field;
+static jfieldID s_game_path_field;
+static jfieldID s_game_program_id_field;
+static jfieldID s_game_developer_field;
+static jfieldID s_game_version_field;
+static jfieldID s_game_is_homebrew_field;
+
 static jclass s_string_class;
 static jclass s_pair_class;
 static jmethodID s_pair_constructor;
@@ -85,6 +94,38 @@ jmethodID GetOnEmulationStopped() {
     return s_on_emulation_stopped;
 }
 
+jclass GetGameClass() {
+    return s_game_class;
+}
+
+jmethodID GetGameConstructor() {
+    return s_game_constructor;
+}
+
+jfieldID GetGameTitleField() {
+    return s_game_title_field;
+}
+
+jfieldID GetGamePathField() {
+    return s_game_path_field;
+}
+
+jfieldID GetGameProgramIdField() {
+    return s_game_program_id_field;
+}
+
+jfieldID GetGameDeveloperField() {
+    return s_game_developer_field;
+}
+
+jfieldID GetGameVersionField() {
+    return s_game_version_field;
+}
+
+jfieldID GetGameIsHomebrewField() {
+    return s_game_is_homebrew_field;
+}
+
 jclass GetStringClass() {
     return s_string_class;
 }
@@ -141,6 +182,19 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
     s_on_emulation_stopped =
         env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V");
 
+    const jclass game_class = env->FindClass("org/yuzu/yuzu_emu/model/Game");
+    s_game_class = reinterpret_cast<jclass>(env->NewGlobalRef(game_class));
+    s_game_constructor = env->GetMethodID(game_class, "<init>",
+                                          "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/"
+                                          "String;Ljava/lang/String;Ljava/lang/String;Z)V");
+    s_game_title_field = env->GetFieldID(game_class, "title", "Ljava/lang/String;");
+    s_game_path_field = env->GetFieldID(game_class, "path", "Ljava/lang/String;");
+    s_game_program_id_field = env->GetFieldID(game_class, "programId", "Ljava/lang/String;");
+    s_game_developer_field = env->GetFieldID(game_class, "developer", "Ljava/lang/String;");
+    s_game_version_field = env->GetFieldID(game_class, "version", "Ljava/lang/String;");
+    s_game_is_homebrew_field = env->GetFieldID(game_class, "isHomebrew", "Z");
+    env->DeleteLocalRef(game_class);
+
     const jclass string_class = env->FindClass("java/lang/String");
     s_string_class = reinterpret_cast<jclass>(env->NewGlobalRef(string_class));
     env->DeleteLocalRef(string_class);
@@ -174,6 +228,7 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
     env->DeleteGlobalRef(s_disk_cache_progress_class);
     env->DeleteGlobalRef(s_load_callback_stage_class);
     env->DeleteGlobalRef(s_game_dir_class);
+    env->DeleteGlobalRef(s_game_class);
     env->DeleteGlobalRef(s_string_class);
     env->DeleteGlobalRef(s_pair_class);
 
diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h
index 36233423eb..24030be421 100644
--- a/src/android/app/src/main/jni/id_cache.h
+++ b/src/android/app/src/main/jni/id_cache.h
@@ -20,6 +20,15 @@ jmethodID GetDiskCacheLoadProgress();
 jmethodID GetOnEmulationStarted();
 jmethodID GetOnEmulationStopped();
 
+jclass GetGameClass();
+jmethodID GetGameConstructor();
+jfieldID GetGameTitleField();
+jfieldID GetGamePathField();
+jfieldID GetGameProgramIdField();
+jfieldID GetGameDeveloperField();
+jfieldID GetGameVersionField();
+jfieldID GetGameIsHomebrewField();
+
 jclass GetStringClass();
 jclass GetPairClass();
 jmethodID GetPairConstructor();

From ca5b135ddfce90da05c149bd0b89befd3a59d256 Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Sun, 10 Dec 2023 20:47:32 -0500
Subject: [PATCH 08/20] android: Expose MemoryUtil size formatting function

---
 .../app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt   | 4 ++--
 src/android/app/src/main/res/values/strings.xml               | 1 +
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt
index 9076a86c41..0b94c73e52 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt
@@ -27,13 +27,13 @@ object MemoryUtil {
     const val Pb = Tb * 1024
     const val Eb = Pb * 1024
 
-    private fun bytesToSizeUnit(size: Float, roundUp: Boolean = false): String =
+    fun bytesToSizeUnit(size: Float, roundUp: Boolean = false): String =
         when {
             size < Kb -> {
                 context.getString(
                     R.string.memory_formatted,
                     size.hundredths,
-                    context.getString(R.string.memory_byte)
+                    context.getString(R.string.memory_byte_shorthand)
                 )
             }
             size < Mb -> {
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index cd5571aa9c..50879b3a3a 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -414,6 +414,7 @@
 
     <!-- Memory Sizes -->
     <string name="memory_byte">Byte</string>
+    <string name="memory_byte_shorthand">B</string>
     <string name="memory_kilobyte">KB</string>
     <string name="memory_megabyte">MB</string>
     <string name="memory_gigabyte">GB</string>

From 698c854d5bcd5c021e94dea9bea44e6b07e1c53d Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Sun, 10 Dec 2023 20:48:05 -0500
Subject: [PATCH 09/20] android: Compare all properties between games in
 DiffCallback

---
 .../app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt | 2 +-
 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt   | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
index 928bfe5a70..a578f0de82 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
@@ -165,7 +165,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
 
     private class DiffCallback : DiffUtil.ItemCallback<Game>() {
         override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
-            return oldItem.programId == newItem.programId
+            return oldItem == newItem
         }
 
         override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
index ac642c16e4..f1ea1e20fe 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
@@ -74,6 +74,7 @@ class Game(
         result = 31 * result + path.hashCode()
         result = 31 * result + programId.hashCode()
         result = 31 * result + developer.hashCode()
+        result = 31 * result + version.hashCode()
         result = 31 * result + isHomebrew.hashCode()
         return result
     }

From 809230f634bcd0fb3ad249578fd7f31e734a0c7d Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Sun, 10 Dec 2023 20:50:48 -0500
Subject: [PATCH 10/20] android: Remove global save import/exporter UI

The original implementation exposed here was fundamentally broken where it would not export or import all of your saves depending on your user profile configuration
---
 .../yuzu_emu/fragments/InstallableFragment.kt | 24 -------------------
 1 file changed, 24 deletions(-)

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
index 6940fc7573..569727b901 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
@@ -21,8 +21,6 @@ import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding
 import org.yuzu.yuzu_emu.model.HomeViewModel
 import org.yuzu.yuzu_emu.model.Installable
 import org.yuzu.yuzu_emu.ui.main.MainActivity
-import java.time.LocalDateTime
-import java.time.format.DateTimeFormatter
 
 class InstallableFragment : Fragment() {
     private var _binding: FragmentInstallablesBinding? = null
@@ -75,28 +73,6 @@ class InstallableFragment : Fragment() {
                 R.string.install_firmware_description,
                 install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) }
             ),
-            if (mainActivity.savesFolderRoot != "") {
-                Installable(
-                    R.string.manage_save_data,
-                    R.string.import_export_saves_description,
-                    install = { mainActivity.importSaves.launch(arrayOf("application/zip")) },
-                    export = {
-                        mainActivity.exportSaves.launch(
-                            "yuzu saves - ${
-                            LocalDateTime.now().format(
-                                DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
-                            )
-                            }.zip"
-                        )
-                    }
-                )
-            } else {
-                Installable(
-                    R.string.manage_save_data,
-                    R.string.import_export_saves_description,
-                    install = { mainActivity.importSaves.launch(arrayOf("application/zip")) }
-                )
-            },
             Installable(
                 R.string.install_prod_keys,
                 R.string.install_prod_keys_description,

From 7ea7c72dde7dd0aa1ed7adf5a622303da2420782 Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Sun, 10 Dec 2023 20:52:28 -0500
Subject: [PATCH 11/20] android: Collect latest information for games list

---
 .../main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt    | 3 ++-
 .../app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt    | 2 +-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
index 3ac054d8fa..64b295fbde 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
@@ -24,6 +24,7 @@ import androidx.lifecycle.repeatOnLifecycle
 import androidx.preference.PreferenceManager
 import info.debatty.java.stringsimilarity.Jaccard
 import info.debatty.java.stringsimilarity.JaroWinkler
+import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.launch
 import java.util.Locale
 import org.yuzu.yuzu_emu.R
@@ -101,7 +102,7 @@ class SearchFragment : Fragment() {
             }
             launch {
                 repeatOnLifecycle(Lifecycle.State.CREATED) {
-                    gamesViewModel.games.collect { filterAndSearch() }
+                    gamesViewModel.games.collectLatest { filterAndSearch() }
                 }
             }
             launch {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
index d5acf84791..fc0eeb9ad3 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
@@ -96,7 +96,7 @@ class GamesFragment : Fragment() {
             }
             launch {
                 repeatOnLifecycle(Lifecycle.State.RESUMED) {
-                    gamesViewModel.games.collect {
+                    gamesViewModel.games.collectLatest {
                         (binding.gridGames.adapter as GameAdapter).submitList(it)
                         if (it.isEmpty()) {
                             binding.noticeText.visibility = View.VISIBLE

From f9d48271029142695d29e065c3449c57ea7c7ded Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Sun, 10 Dec 2023 20:54:00 -0500
Subject: [PATCH 12/20] android: Fix games list loading thread safety

Previously we relied on a stateflow for reloading state. Now we use an atomic boolean.
---
 .../org/yuzu/yuzu_emu/model/GamesViewModel.kt | 74 ++++++++++---------
 1 file changed, 38 insertions(+), 36 deletions(-)

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
index eaec09b246..d19f20dc29 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
@@ -20,8 +20,8 @@ import kotlinx.serialization.json.Json
 import org.yuzu.yuzu_emu.NativeLibrary
 import org.yuzu.yuzu_emu.YuzuApplication
 import org.yuzu.yuzu_emu.utils.GameHelper
-import org.yuzu.yuzu_emu.utils.GameMetadata
 import org.yuzu.yuzu_emu.utils.NativeConfig
+import java.util.concurrent.atomic.AtomicBoolean
 
 class GamesViewModel : ViewModel() {
     val games: StateFlow<List<Game>> get() = _games
@@ -33,6 +33,8 @@ class GamesViewModel : ViewModel() {
     val isReloading: StateFlow<Boolean> get() = _isReloading
     private val _isReloading = MutableStateFlow(false)
 
+    private val reloading = AtomicBoolean(false)
+
     val shouldSwapData: StateFlow<Boolean> get() = _shouldSwapData
     private val _shouldSwapData = MutableStateFlow(false)
 
@@ -49,38 +51,8 @@ class GamesViewModel : ViewModel() {
         // Ensure keys are loaded so that ROM metadata can be decrypted.
         NativeLibrary.reloadKeys()
 
-        // Retrieve list of cached games
-        val storedGames = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
-            .getStringSet(GameHelper.KEY_GAMES, emptySet())
-
-        viewModelScope.launch {
-            withContext(Dispatchers.IO) {
-                getGameDirs()
-                if (storedGames!!.isNotEmpty()) {
-                    val deserializedGames = mutableSetOf<Game>()
-                    storedGames.forEach {
-                        val game: Game
-                        try {
-                            game = Json.decodeFromString(it)
-                        } catch (e: Exception) {
-                            // We don't care about any errors related to parsing the game cache
-                            return@forEach
-                        }
-
-                        val gameExists =
-                            DocumentFile.fromSingleUri(
-                                YuzuApplication.appContext,
-                                Uri.parse(game.path)
-                            )?.exists()
-                        if (gameExists == true) {
-                            deserializedGames.add(game)
-                        }
-                    }
-                    setGames(deserializedGames.toList())
-                }
-                reloadGames(false)
-            }
-        }
+        getGameDirs()
+        reloadGames(directoriesChanged = false, firstStartup = true)
     }
 
     fun setGames(games: List<Game>) {
@@ -110,16 +82,46 @@ class GamesViewModel : ViewModel() {
         _searchFocused.value = searchFocused
     }
 
-    fun reloadGames(directoriesChanged: Boolean) {
-        if (isReloading.value) {
+    fun reloadGames(directoriesChanged: Boolean, firstStartup: Boolean = false) {
+        if (reloading.get()) {
             return
         }
+        reloading.set(true)
         _isReloading.value = true
 
         viewModelScope.launch {
             withContext(Dispatchers.IO) {
-                GameMetadata.resetMetadata()
+                if (firstStartup) {
+                    // Retrieve list of cached games
+                    val storedGames =
+                        PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
+                            .getStringSet(GameHelper.KEY_GAMES, emptySet())
+                    if (storedGames!!.isNotEmpty()) {
+                        val deserializedGames = mutableSetOf<Game>()
+                        storedGames.forEach {
+                            val game: Game
+                            try {
+                                game = Json.decodeFromString(it)
+                            } catch (e: Exception) {
+                                // We don't care about any errors related to parsing the game cache
+                                return@forEach
+                            }
+
+                            val gameExists =
+                                DocumentFile.fromSingleUri(
+                                    YuzuApplication.appContext,
+                                    Uri.parse(game.path)
+                                )?.exists()
+                            if (gameExists == true) {
+                                deserializedGames.add(game)
+                            }
+                        }
+                        setGames(deserializedGames.toList())
+                    }
+                }
+
                 setGames(GameHelper.getGames())
+                reloading.set(false)
                 _isReloading.value = false
 
                 if (directoriesChanged) {

From ac222ceba2b39a4efa4f2f217de93206ee8aa3f6 Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Sun, 10 Dec 2023 20:57:51 -0500
Subject: [PATCH 13/20] android: Add game dir entries to FilesystemProvider

Allows us to correctly parse update metadata
---
 .../java/org/yuzu/yuzu_emu/NativeLibrary.kt   | 12 ++++++
 .../org/yuzu/yuzu_emu/utils/GameHelper.kt     | 22 +++++++++--
 .../org/yuzu/yuzu_emu/utils/GameMetadata.kt   |  4 +-
 .../app/src/main/jni/game_metadata.cpp        | 39 +++++++++++++++++--
 src/android/app/src/main/jni/native.cpp       | 11 +++++-
 src/android/app/src/main/jni/native.h         |  1 +
 6 files changed, 80 insertions(+), 9 deletions(-)

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
index 95b98798db..010c449514 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
@@ -547,6 +547,18 @@ object NativeLibrary {
      */
     external fun getSavePath(programId: String): String
 
+    /**
+     * Adds a file to the manual filesystem provider in our EmulationSession instance
+     * @param path Path to the file we're adding. Can be a string representation of a [Uri] or
+     * a normal path
+     */
+    external fun addFileToFilesystemProvider(path: String)
+
+    /**
+     * Clears all files added to the manual filesystem provider in our EmulationSession instance
+     */
+    external fun clearFilesystemProvider()
+
     /**
      * Button type for use in onTouchEvent
      */
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
index 55010dc595..579b600f1a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
@@ -36,6 +36,12 @@ object GameHelper {
         // Ensure keys are loaded so that ROM metadata can be decrypted.
         NativeLibrary.reloadKeys()
 
+        // Reset metadata so we don't use stale information
+        GameMetadata.resetMetadata()
+
+        // Remove previous filesystem provider information so we can get up to date version info
+        NativeLibrary.clearFilesystemProvider()
+
         val badDirs = mutableListOf<Int>()
         gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
             val gameDirUri = Uri.parse(gameDir.uriString)
@@ -92,14 +98,24 @@ object GameHelper {
                 )
             } else {
                 if (Game.extensions.contains(FileUtil.getExtension(it.uri))) {
-                    games.add(getGame(it.uri, true))
+                    val game = getGame(it.uri, true)
+                    if (game != null) {
+                        games.add(game)
+                    }
                 }
             }
         }
     }
 
-    fun getGame(uri: Uri, addedToLibrary: Boolean): Game {
+    fun getGame(uri: Uri, addedToLibrary: Boolean): Game? {
         val filePath = uri.toString()
+        if (!GameMetadata.getIsValid(filePath)) {
+            return null
+        }
+
+        // Needed to update installed content information
+        NativeLibrary.addFileToFilesystemProvider(filePath)
+
         var name = GameMetadata.getTitle(filePath)
 
         // If the game's title field is empty, use the filename.
@@ -118,7 +134,7 @@ object GameHelper {
             filePath,
             programId,
             GameMetadata.getDeveloper(filePath),
-            GameMetadata.getVersion(filePath),
+            GameMetadata.getVersion(filePath, false),
             GameMetadata.getIsHomebrew(filePath)
         )
 
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameMetadata.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameMetadata.kt
index 0f3542ac62..8e412482a1 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameMetadata.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameMetadata.kt
@@ -4,13 +4,15 @@
 package org.yuzu.yuzu_emu.utils
 
 object GameMetadata {
+    external fun getIsValid(path: String): Boolean
+
     external fun getTitle(path: String): String
 
     external fun getProgramId(path: String): String
 
     external fun getDeveloper(path: String): String
 
-    external fun getVersion(path: String): String
+    external fun getVersion(path: String, reload: Boolean): String
 
     external fun getIcon(path: String): ByteArray
 
diff --git a/src/android/app/src/main/jni/game_metadata.cpp b/src/android/app/src/main/jni/game_metadata.cpp
index 24d9df7025..78f604c704 100644
--- a/src/android/app/src/main/jni/game_metadata.cpp
+++ b/src/android/app/src/main/jni/game_metadata.cpp
@@ -2,6 +2,7 @@
 // SPDX-License-Identifier: GPL-2.0-or-later
 
 #include <core/core.h>
+#include <core/file_sys/mode.h>
 #include <core/file_sys/patch_manager.h>
 #include <core/loader/nro.h>
 #include <jni.h>
@@ -61,7 +62,11 @@ RomMetadata CacheRomMetadata(const std::string& path) {
     return entry;
 }
 
-RomMetadata GetRomMetadata(const std::string& path) {
+RomMetadata GetRomMetadata(const std::string& path, bool reload = false) {
+    if (reload) {
+        return CacheRomMetadata(path);
+    }
+
     if (auto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) {
         return search->second;
     }
@@ -71,6 +76,32 @@ RomMetadata GetRomMetadata(const std::string& path) {
 
 extern "C" {
 
+jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsValid(JNIEnv* env, jobject obj,
+                                                               jstring jpath) {
+    const auto file = EmulationSession::GetInstance().System().GetFilesystem()->OpenFile(
+        GetJString(env, jpath), FileSys::Mode::Read);
+    if (!file) {
+        return false;
+    }
+
+    auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file);
+    if (!loader) {
+        return false;
+    }
+
+    const auto file_type = loader->GetFileType();
+    if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) {
+        return false;
+    }
+
+    u64 program_id = 0;
+    Loader::ResultStatus res = loader->ReadProgramId(program_id);
+    if (res != Loader::ResultStatus::Success) {
+        return false;
+    }
+    return true;
+}
+
 jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getTitle(JNIEnv* env, jobject obj,
                                                             jstring jpath) {
     return ToJString(env, GetRomMetadata(GetJString(env, jpath)).title);
@@ -87,8 +118,8 @@ jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getDeveloper(JNIEnv* env, job
 }
 
 jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getVersion(JNIEnv* env, jobject obj,
-                                                              jstring jpath) {
-    return ToJString(env, GetRomMetadata(GetJString(env, jpath)).version);
+                                                              jstring jpath, jboolean jreload) {
+    return ToJString(env, GetRomMetadata(GetJString(env, jpath), jreload).version);
 }
 
 jbyteArray Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIcon(JNIEnv* env, jobject obj,
@@ -106,7 +137,7 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsHomebrew(JNIEnv* env, j
 }
 
 void Java_org_yuzu_yuzu_1emu_utils_GameMetadata_resetMetadata(JNIEnv* env, jobject obj) {
-    return m_rom_metadata_cache.clear();
+    m_rom_metadata_cache.clear();
 }
 
 } // extern "C"
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index ce570b811a..0c1db7d464 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -80,7 +80,7 @@ Core::System& EmulationSession::System() {
     return m_system;
 }
 
-FileSys::ManualContentProvider* EmulationSession::ContentProvider() {
+FileSys::ManualContentProvider* EmulationSession::GetContentProvider() {
     return m_manual_provider.get();
 }
 
@@ -880,4 +880,13 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject j
     return ToJString(env, user_save_data_path);
 }
 
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_addFileToFilesystemProvider(JNIEnv* env, jobject jobj,
+                                                                       jstring jpath) {
+    EmulationSession::GetInstance().ConfigureFilesystemProvider(GetJString(env, jpath));
+}
+
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_clearFilesystemProvider(JNIEnv* env, jobject jobj) {
+    EmulationSession::GetInstance().GetContentProvider()->ClearAllEntries();
+}
+
 } // extern "C"
diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h
index 96c22d52b3..4a80495784 100644
--- a/src/android/app/src/main/jni/native.h
+++ b/src/android/app/src/main/jni/native.h
@@ -21,6 +21,7 @@ public:
     static EmulationSession& GetInstance();
     const Core::System& System() const;
     Core::System& System();
+    FileSys::ManualContentProvider* GetContentProvider();
 
     const EmuWindow_Android& Window() const;
     EmuWindow_Android& Window();

From 5acffe75dfce8e3965ea4d472fc4c42944c7b874 Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Tue, 12 Dec 2023 11:37:30 -0500
Subject: [PATCH 14/20] android: Adjust variable name format for native config

---
 src/android/app/src/main/jni/native_config.cpp | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp
index 56989bde8d..9e0a33c0f5 100644
--- a/src/android/app/src/main/jni/native_config.cpp
+++ b/src/android/app/src/main/jni/native_config.cpp
@@ -21,13 +21,13 @@ std::unique_ptr<AndroidConfig> per_game_config;
 template <typename T>
 Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) {
     auto key = GetJString(env, jkey);
-    auto basicSetting = Settings::values.linkage.by_key[key];
-    if (basicSetting != 0) {
-        return static_cast<Settings::Setting<T>*>(basicSetting);
+    auto basic_setting = Settings::values.linkage.by_key[key];
+    if (basic_setting != 0) {
+        return static_cast<Settings::Setting<T>*>(basic_setting);
     }
-    auto basicAndroidSetting = AndroidSettings::values.linkage.by_key[key];
-    if (basicAndroidSetting != 0) {
-        return static_cast<Settings::Setting<T>*>(basicAndroidSetting);
+    auto basic_android_setting = AndroidSettings::values.linkage.by_key[key];
+    if (basic_android_setting != 0) {
+        return static_cast<Settings::Setting<T>*>(basic_android_setting);
     }
     LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key);
     return nullptr;

From 6c6e8b8de02cadf86ffbdcbf81e11183344899cd Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Tue, 12 Dec 2023 13:39:16 -0500
Subject: [PATCH 15/20] settings: Allow vsync to be changed per-game

---
 src/common/settings.h | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/common/settings.h b/src/common/settings.h
index 25086d3886..07dba53aba 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -256,7 +256,7 @@ struct Values {
                                                             AstcDecodeMode::CpuAsynchronous,
                                                             "accelerate_astc",
                                                             Category::Renderer};
-    Setting<VSyncMode, true> vsync_mode{
+    SwitchableSetting<VSyncMode, true> vsync_mode{
         linkage,     VSyncMode::Fifo,    VSyncMode::Immediate,        VSyncMode::FifoRelaxed,
         "use_vsync", Category::Renderer, Specialization::RuntimeList, true,
         true};

From 87a9dc9489b72c31af79b6170ccd34fcda9f40f7 Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Tue, 12 Dec 2023 13:40:00 -0500
Subject: [PATCH 16/20] android: Always use custom settings when launched from
 intent

---
 .../java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt    | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
index c40f5f41a1..d7b38f62d0 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
@@ -128,8 +128,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
             return
         }
 
-        if (args.custom) {
-            SettingsFile.loadCustomConfig(args.game!!)
+        // Always load custom settings when launching a game from an intent
+        if (args.custom || intentGame != null) {
+            SettingsFile.loadCustomConfig(game)
             NativeConfig.unloadPerGameConfig()
         } else {
             NativeConfig.reloadGlobalConfig()

From 345fb6b22636d2677f9bcb6113d81a4833aadd5c Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Tue, 12 Dec 2023 13:58:25 -0500
Subject: [PATCH 17/20] android: Use confirmation dialog when deleting shader
 cache

---
 .../fragments/GamePropertiesFragment.kt       | 52 +++++++++++++------
 .../org/yuzu/yuzu_emu/model/HomeViewModel.kt  |  7 +++
 .../app/src/main/res/values/strings.xml       |  1 +
 3 files changed, 45 insertions(+), 15 deletions(-)

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
index e062425a15..6ede3f85c0 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
@@ -3,6 +3,7 @@
 
 package org.yuzu.yuzu_emu.fragments
 
+import android.annotation.SuppressLint
 import android.os.Bundle
 import android.text.TextUtils
 import android.view.LayoutInflater
@@ -73,6 +74,8 @@ class GamePropertiesFragment : Fragment() {
         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)
@@ -99,12 +102,24 @@ class GamePropertiesFragment : Fragment() {
 
         reloadList()
 
-        viewLifecycleOwner.lifecycleScope.launch {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
-                homeViewModel.openImportSaves.collect {
-                    if (it) {
-                        importSaves.launch(arrayOf("application/zip"))
-                        homeViewModel.setOpenImportSaves(false)
+        viewLifecycleOwner.lifecycleScope.apply {
+            launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    homeViewModel.openImportSaves.collect {
+                        if (it) {
+                            importSaves.launch(arrayOf("application/zip"))
+                            homeViewModel.setOpenImportSaves(false)
+                        }
+                    }
+                }
+            }
+            launch {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    homeViewModel.reloadPropertiesList.collect {
+                        if (it) {
+                            reloadList()
+                            homeViewModel.reloadPropertiesList(false)
+                        }
                     }
                 }
             }
@@ -214,7 +229,7 @@ class GamePropertiesFragment : Fragment() {
                                             R.string.save_data_deleted_successfully,
                                             Toast.LENGTH_SHORT
                                         ).show()
-                                        reloadList()
+                                        homeViewModel.reloadPropertiesList(true)
                                     }
                                 ).show(parentFragmentManager, MessageDialogFragment.TAG)
                             }
@@ -242,13 +257,20 @@ class GamePropertiesFragment : Fragment() {
                                 }
                             }
                         ) {
-                            shaderCacheDir.deleteRecursively()
-                            Toast.makeText(
-                                YuzuApplication.appContext,
-                                R.string.cleared_shaders_successfully,
-                                Toast.LENGTH_SHORT
-                            ).show()
-                            reloadList()
+                            MessageDialogFragment.newInstance(
+                                requireActivity(),
+                                titleId = R.string.clear_shader_cache,
+                                descriptionId = R.string.clear_shader_cache_warning_description,
+                                positiveAction = {
+                                    shaderCacheDir.deleteRecursively()
+                                    Toast.makeText(
+                                        YuzuApplication.appContext,
+                                        R.string.cleared_shaders_successfully,
+                                        Toast.LENGTH_SHORT
+                                    ).show()
+                                    homeViewModel.reloadPropertiesList(true)
+                                }
+                            ).show(parentFragmentManager, MessageDialogFragment.TAG)
                         }
                     )
                 }
@@ -388,7 +410,7 @@ class GamePropertiesFragment : Fragment() {
                             getString(R.string.save_file_imported_success),
                             Toast.LENGTH_LONG
                         ).show()
-                        reloadList()
+                        homeViewModel.reloadPropertiesList(true)
                     }
 
                     cacheSaveDir.deleteRecursively()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
index d801db1054..513ac2fc57 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
@@ -28,6 +28,9 @@ class HomeViewModel : ViewModel() {
     private val _contentToInstall = MutableStateFlow<List<Uri>?>(null)
     val contentToInstall get() = _contentToInstall.asStateFlow()
 
+    private val _reloadPropertiesList = MutableStateFlow(false)
+    val reloadPropertiesList get() = _reloadPropertiesList.asStateFlow()
+
     var navigatedToSetup = false
 
     fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
@@ -59,4 +62,8 @@ class HomeViewModel : ViewModel() {
     fun setContentToInstall(documents: List<Uri>?) {
         _contentToInstall.value = documents
     }
+
+    fun reloadPropertiesList(reload: Boolean) {
+        _reloadPropertiesList.value = reload
+    }
 }
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 50879b3a3a..c86c43df24 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -314,6 +314,7 @@
     <string name="add_ons_description">Toggle mods, updates and DLC</string>
     <string name="clear_shader_cache">Clear shader cache</string>
     <string name="clear_shader_cache_description">Removes all shaders built while playing this game</string>
+    <string name="clear_shader_cache_warning_description">You will experience more stuttering as the shader cache regenerates</string>
     <string name="cleared_shaders_successfully">Cleared shaders successfully</string>
     <string name="addons_game">Addons: %1$s</string>
     <string name="save_data">Save data</string>

From f6bf8b3ed3efac180e0f8c87d8353cef4d915304 Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Tue, 12 Dec 2023 14:08:30 -0500
Subject: [PATCH 18/20] android: Pre-select custom config in game launch dialog

---
 .../org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt   | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt
index f653826a61..e1ac46c484 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt
@@ -15,7 +15,7 @@ import org.yuzu.yuzu_emu.model.Game
 import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
 
 class LaunchGameDialogFragment : DialogFragment() {
-    private var selectedItem = 0
+    private var selectedItem = 1
 
     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
         val game = requireArguments().parcelable<Game>(GAME)
@@ -32,7 +32,7 @@ class LaunchGameDialogFragment : DialogFragment() {
                     .actionGlobalEmulationActivity(game, selectedItem != 0)
                 requireParentFragment().findNavController().navigate(action)
             }
-            .setSingleChoiceItems(launchOptions, 0) { _: DialogInterface, i: Int ->
+            .setSingleChoiceItems(launchOptions, 1) { _: DialogInterface, i: Int ->
                 selectedItem = i
             }
             .setNegativeButton(android.R.string.cancel, null)

From 6ae4177b2520e0b48399615de5e964495fc2115d Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Tue, 12 Dec 2023 14:53:37 -0500
Subject: [PATCH 19/20] android: Prevent editing non-savable settings in
 per-game settings

---
 .../yuzu_emu/features/settings/model/AbstractSetting.kt  | 3 +++
 .../features/settings/model/view/SettingsItem.kt         | 9 +++++++++
 .../main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt    | 2 ++
 src/android/app/src/main/jni/native_config.cpp           | 9 +++++++++
 4 files changed, 23 insertions(+)

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt
index e384c78c25..3b78c7cf0c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt
@@ -22,6 +22,9 @@ interface AbstractSetting {
         get() = NativeConfig.usingGlobal(key)
         set(value) = NativeConfig.setGlobal(key, value)
 
+    val isSaveable: Boolean
+        get() = NativeConfig.getIsSaveable(key)
+
     fun getValueAsString(needsGlobal: Boolean = false): String
 
     fun reset()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
index 28d8dea603..2e97aee2cc 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
@@ -30,6 +30,11 @@ abstract class SettingsItem(
 
     val isEditable: Boolean
         get() {
+            // Can't edit settings that aren't saveable in per-game config even if they are switchable
+            if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {
+                return false
+            }
+
             if (!NativeLibrary.isRunning()) return true
 
             // Prevent editing settings that were modified in per-game config while editing global
@@ -37,6 +42,7 @@ abstract class SettingsItem(
             if (!NativeConfig.isPerGameConfigLoaded() && !setting.global) {
                 return false
             }
+
             return setting.isRuntimeModifiable
         }
 
@@ -59,6 +65,7 @@ abstract class SettingsItem(
         val emptySetting = object : AbstractSetting {
             override val key: String = ""
             override val defaultValue: Any = false
+            override val isSaveable = true
             override fun getValueAsString(needsGlobal: Boolean): String = ""
             override fun reset() {}
         }
@@ -303,6 +310,8 @@ abstract class SettingsItem(
                         BooleanSetting.FASTMEM_EXCLUSIVES.global = value
                     }
 
+                override val isSaveable = true
+
                 override fun getValueAsString(needsGlobal: Boolean): String =
                     getBoolean().toString()
 
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
index 2d3d8ec795..7512d5eed9 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
@@ -110,6 +110,8 @@ object NativeConfig {
     @Synchronized
     external fun setGlobal(key: String, global: Boolean)
 
+    external fun getIsSaveable(key: String): Boolean
+
     external fun getDefaultToString(key: String): String
 
     /**
diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp
index 9e0a33c0f5..324d9e9cdc 100644
--- a/src/android/app/src/main/jni/native_config.cpp
+++ b/src/android/app/src/main/jni/native_config.cpp
@@ -249,6 +249,15 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setGlobal(JNIEnv* env, jobject o
     }
 }
 
+jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsSaveable(JNIEnv* env, jobject obj,
+                                                                  jstring jkey) {
+    auto setting = getSetting<std::string>(env, jkey);
+    if (setting != nullptr) {
+        return setting->Save();
+    }
+    return false;
+}
+
 jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getDefaultToString(JNIEnv* env, jobject obj,
                                                                       jstring jkey) {
     auto setting = getSetting<std::string>(env, jkey);

From 86d26914a2e7b250d346a8aad84783aef1a6850b Mon Sep 17 00:00:00 2001
From: t895 <clombardo169@gmail.com>
Date: Tue, 12 Dec 2023 17:06:47 -0500
Subject: [PATCH 20/20] android: Rework InstallableProperty view with icon

---
 .../adapters/GamePropertiesAdapter.kt         | 13 ++-
 .../fragments/GamePropertiesFragment.kt       |  1 +
 .../org/yuzu/yuzu_emu/model/GameProperties.kt |  8 +-
 .../app/src/main/res/drawable/ic_save.xml     |  9 +-
 .../layout-w1000dp/card_installable_icon.xml  | 82 +++++++++++++++++
 .../main/res/layout/card_installable_icon.xml | 89 +++++++++++++++++++
 6 files changed, 191 insertions(+), 11 deletions(-)
 create mode 100644 src/android/app/src/main/res/layout-w1000dp/card_installable_icon.xml
 create mode 100644 src/android/app/src/main/res/layout/card_installable_icon.xml

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt
index ff6270fa87..95841d7863 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt
@@ -14,7 +14,7 @@ import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
 import androidx.recyclerview.widget.RecyclerView
 import kotlinx.coroutines.launch
-import org.yuzu.yuzu_emu.databinding.CardInstallableBinding
+import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding
 import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
 import org.yuzu.yuzu_emu.model.GameProperty
 import org.yuzu.yuzu_emu.model.InstallableProperty
@@ -42,7 +42,7 @@ class GamePropertiesAdapter(
             }
 
             else -> InstallablePropertyViewHolder(
-                CardInstallableBinding.inflate(
+                CardInstallableIconBinding.inflate(
                     inflater,
                     parent,
                     false
@@ -107,13 +107,20 @@ class GamePropertiesAdapter(
         }
     }
 
-    inner class InstallablePropertyViewHolder(val binding: CardInstallableBinding) :
+    inner class InstallablePropertyViewHolder(val binding: CardInstallableIconBinding) :
         GamePropertyViewHolder(binding.root) {
         override fun bind(property: GameProperty) {
             val installableProperty = property as InstallableProperty
 
             binding.title.setText(installableProperty.titleId)
             binding.description.setText(installableProperty.descriptionId)
+            binding.icon.setImageDrawable(
+                ResourcesCompat.getDrawable(
+                    binding.icon.context.resources,
+                    installableProperty.iconId,
+                    binding.icon.context.theme
+                )
+            )
 
             if (installableProperty.install != null) {
                 binding.buttonInstall.visibility = View.VISIBLE
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
index 6ede3f85c0..b1d3c0040a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
@@ -194,6 +194,7 @@ class GamePropertiesFragment : Fragment() {
                     InstallableProperty(
                         R.string.save_data,
                         R.string.save_data_description,
+                        R.drawable.ic_save,
                         {
                             MessageDialogFragment.newInstance(
                                 requireActivity(),
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt
index bb3df5bd00..0135a95beb 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt
@@ -10,17 +10,18 @@ import kotlinx.coroutines.flow.StateFlow
 interface GameProperty {
     @get:StringRes
     val titleId: Int
-        get() = -1
 
     @get:StringRes
     val descriptionId: Int
-        get() = -1
+
+    @get:DrawableRes
+    val iconId: Int
 }
 
 data class SubmenuProperty(
     override val titleId: Int,
     override val descriptionId: Int,
-    @DrawableRes val iconId: Int,
+    override val iconId: Int,
     val details: (() -> String)? = null,
     val detailsFlow: StateFlow<String>? = null,
     val action: () -> Unit
@@ -29,6 +30,7 @@ data class SubmenuProperty(
 data class InstallableProperty(
     override val titleId: Int,
     override val descriptionId: Int,
+    override val iconId: Int,
     val install: (() -> Unit)? = null,
     val export: (() -> Unit)? = null
 ) : GameProperty
diff --git a/src/android/app/src/main/res/drawable/ic_save.xml b/src/android/app/src/main/res/drawable/ic_save.xml
index a9af3d9cf2..5acc2bbab8 100644
--- a/src/android/app/src/main/res/drawable/ic_save.xml
+++ b/src/android/app/src/main/res/drawable/ic_save.xml
@@ -1,10 +1,9 @@
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
     android:width="24dp"
     android:height="24dp"
-    android:viewportWidth="960"
-    android:viewportHeight="960"
-    android:tint="?attr/colorControlNormal">
+    android:viewportWidth="24"
+    android:viewportHeight="24">
     <path
-        android:fillColor="@android:color/white"
-        android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L647,120Q663,120 677.5,126Q692,132 703,143L817,257Q828,268 834,282.5Q840,297 840,313L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM760,314L646,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L760,760Q760,760 760,760Q760,760 760,760L760,314ZM480,720Q530,720 565,685Q600,650 600,600Q600,550 565,515Q530,480 480,480Q430,480 395,515Q360,550 360,600Q360,650 395,685Q430,720 480,720ZM280,400L560,400Q577,400 588.5,388.5Q600,377 600,360L600,280Q600,263 588.5,251.5Q577,240 560,240L280,240Q263,240 251.5,251.5Q240,263 240,280L240,360Q240,377 251.5,388.5Q263,400 280,400ZM200,314L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200L200,200L200,314Z"/>
+        android:fillColor="?attr/colorControlNormal"
+        android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" />
 </vector>
diff --git a/src/android/app/src/main/res/layout-w1000dp/card_installable_icon.xml b/src/android/app/src/main/res/layout-w1000dp/card_installable_icon.xml
new file mode 100644
index 0000000000..59ee1aad30
--- /dev/null
+++ b/src/android/app/src/main/res/layout-w1000dp/card_installable_icon.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.google.android.material.card.MaterialCardView 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"
+    style="?attr/materialCardViewOutlinedStyle"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginHorizontal="16dp"
+    android:layout_marginVertical="12dp">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:orientation="horizontal"
+        android:gravity="center_vertical"
+        android:paddingHorizontal="24dp"
+        android:paddingVertical="16dp">
+
+        <ImageView
+            android:id="@+id/icon"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_marginEnd="20dp"
+            android:layout_gravity="center_vertical"
+            app:tint="?attr/colorOnSurface"
+            tools:src="@drawable/ic_settings" />
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="16dp"
+            android:layout_weight="1"
+            android:orientation="vertical">
+
+            <com.google.android.material.textview.MaterialTextView
+                android:id="@+id/title"
+                style="@style/TextAppearance.Material3.TitleMedium"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/user_data"
+                android:textAlignment="viewStart" />
+
+            <com.google.android.material.textview.MaterialTextView
+                android:id="@+id/description"
+                style="@style/TextAppearance.Material3.BodyMedium"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="6dp"
+                android:text="@string/user_data_description"
+                android:textAlignment="viewStart" />
+
+        </LinearLayout>
+
+        <Button
+            android:id="@+id/button_export"
+            style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:contentDescription="@string/export"
+            android:tooltipText="@string/export"
+            android:visibility="gone"
+            app:icon="@drawable/ic_export"
+            tools:visibility="visible" />
+
+        <Button
+            android:id="@+id/button_install"
+            style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:layout_marginStart="12dp"
+            android:contentDescription="@string/string_import"
+            android:tooltipText="@string/string_import"
+            android:visibility="gone"
+            app:icon="@drawable/ic_import"
+            tools:visibility="visible" />
+
+    </LinearLayout>
+
+</com.google.android.material.card.MaterialCardView>
diff --git a/src/android/app/src/main/res/layout/card_installable_icon.xml b/src/android/app/src/main/res/layout/card_installable_icon.xml
new file mode 100644
index 0000000000..4ae5423b10
--- /dev/null
+++ b/src/android/app/src/main/res/layout/card_installable_icon.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.google.android.material.card.MaterialCardView 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"
+    style="?attr/materialCardViewOutlinedStyle"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginHorizontal="16dp"
+    android:layout_marginVertical="12dp">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:orientation="horizontal"
+        android:gravity="center_vertical"
+        android:paddingHorizontal="24dp"
+        android:paddingVertical="16dp">
+
+        <ImageView
+            android:id="@+id/icon"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_marginEnd="20dp"
+            android:layout_gravity="center_vertical"
+            app:tint="?attr/colorOnSurface"
+            tools:src="@drawable/ic_settings" />
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="16dp"
+            android:layout_weight="1"
+            android:orientation="vertical">
+
+            <com.google.android.material.textview.MaterialTextView
+                android:id="@+id/title"
+                style="@style/TextAppearance.Material3.TitleMedium"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/user_data"
+                android:textAlignment="viewStart" />
+
+            <com.google.android.material.textview.MaterialTextView
+                android:id="@+id/description"
+                style="@style/TextAppearance.Material3.BodyMedium"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="6dp"
+                android:text="@string/user_data_description"
+                android:textAlignment="viewStart" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <Button
+                android:id="@+id/button_install"
+                style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_vertical"
+                android:contentDescription="@string/string_import"
+                android:tooltipText="@string/string_import"
+                android:visibility="gone"
+                app:icon="@drawable/ic_import"
+                tools:visibility="visible" />
+
+            <Button
+                android:id="@+id/button_export"
+                style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="center_vertical"
+                android:layout_marginTop="8dp"
+                android:contentDescription="@string/export"
+                android:tooltipText="@string/export"
+                android:visibility="gone"
+                app:icon="@drawable/ic_export"
+                tools:visibility="visible" />
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+</com.google.android.material.card.MaterialCardView>