From 26f9d1f1223ad88d42383de75abfd3226cf03a2e Mon Sep 17 00:00:00 2001
From: Charles Lombardo <clombardo169@gmail.com>
Date: Thu, 28 Sep 2023 13:30:59 -0400
Subject: [PATCH 1/2] android: Use application context for all FileUtil
 functions

---
 .../java/org/yuzu/yuzu_emu/NativeLibrary.kt   | 14 ++++-----
 .../java/org/yuzu/yuzu_emu/YuzuApplication.kt |  2 +-
 .../fragments/HomeSettingsFragment.kt         |  2 +-
 .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 10 +++----
 .../org/yuzu/yuzu_emu/utils/DocumentsTree.kt  |  7 ++---
 .../java/org/yuzu/yuzu_emu/utils/FileUtil.kt  | 29 +++++++++----------
 .../org/yuzu/yuzu_emu/utils/GameHelper.kt     |  4 +--
 .../yuzu/yuzu_emu/utils/GpuDriverHelper.kt    |  7 +++--
 8 files changed, 34 insertions(+), 41 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 6e39e542b9..115f72710a 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
@@ -15,13 +15,9 @@ import androidx.annotation.Keep
 import androidx.fragment.app.DialogFragment
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import java.lang.ref.WeakReference
-import org.yuzu.yuzu_emu.YuzuApplication.Companion.appContext
 import org.yuzu.yuzu_emu.activities.EmulationActivity
 import org.yuzu.yuzu_emu.utils.DocumentsTree.Companion.isNativePath
-import org.yuzu.yuzu_emu.utils.FileUtil.exists
-import org.yuzu.yuzu_emu.utils.FileUtil.getFileSize
-import org.yuzu.yuzu_emu.utils.FileUtil.isDirectory
-import org.yuzu.yuzu_emu.utils.FileUtil.openContentUri
+import org.yuzu.yuzu_emu.utils.FileUtil
 import org.yuzu.yuzu_emu.utils.Log
 import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
 
@@ -75,7 +71,7 @@ object NativeLibrary {
         return if (isNativePath(path!!)) {
             YuzuApplication.documentsTree!!.openContentUri(path, openmode)
         } else {
-            openContentUri(appContext, path, openmode)
+            FileUtil.openContentUri(path, openmode)
         }
     }
 
@@ -85,7 +81,7 @@ object NativeLibrary {
         return if (isNativePath(path!!)) {
             YuzuApplication.documentsTree!!.getFileSize(path)
         } else {
-            getFileSize(appContext, path)
+            FileUtil.getFileSize(path)
         }
     }
 
@@ -95,7 +91,7 @@ object NativeLibrary {
         return if (isNativePath(path!!)) {
             YuzuApplication.documentsTree!!.exists(path)
         } else {
-            exists(appContext, path)
+            FileUtil.exists(path)
         }
     }
 
@@ -105,7 +101,7 @@ object NativeLibrary {
         return if (isNativePath(path!!)) {
             YuzuApplication.documentsTree!!.isDirectory(path)
         } else {
-            isDirectory(appContext, path)
+            FileUtil.isDirectory(path)
         }
     }
 
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
index 9561748cbb..8c053670c6 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/YuzuApplication.kt
@@ -47,7 +47,7 @@ class YuzuApplication : Application() {
         application = this
         documentsTree = DocumentsTree()
         DirectoryInitialization.start()
-        GpuDriverHelper.initializeDriverParameters(applicationContext)
+        GpuDriverHelper.initializeDriverParameters()
         NativeLibrary.logDeviceInfo()
 
         createNotificationChannels()
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 8923c0ea29..18857db2d7 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
@@ -304,7 +304,7 @@ class HomeSettingsFragment : Fragment() {
             .setMessage(driverName)
             .setNegativeButton(android.R.string.cancel, null)
             .setNeutralButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int ->
-                GpuDriverHelper.installDefaultDriver(requireContext())
+                GpuDriverHelper.installDefaultDriver()
                 Toast.makeText(
                     requireContext(),
                     R.string.select_gpu_driver_use_default,
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 0fa5df5e5f..ac96c82071 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
@@ -343,7 +343,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
 
         val dstPath = DirectoryInitialization.userDirectory + "/keys/"
         if (FileUtil.copyUriToInternalStorage(
-                applicationContext,
                 result,
                 dstPath,
                 "prod.keys"
@@ -446,7 +445,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
 
             val dstPath = DirectoryInitialization.userDirectory + "/keys/"
             if (FileUtil.copyUriToInternalStorage(
-                    applicationContext,
                     result,
                     dstPath,
                     "key_retail.bin"
@@ -493,20 +491,20 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                 withContext(Dispatchers.IO) {
                     // Ignore file exceptions when a user selects an invalid zip
                     try {
-                        GpuDriverHelper.installCustomDriver(applicationContext, result)
+                        GpuDriverHelper.installCustomDriver(result)
                     } catch (_: IOException) {
                     }
 
                     withContext(Dispatchers.Main) {
                         installationDialog.dismiss()
 
-                        val driverName = GpuDriverHelper.customDriverName
-                        if (driverName != null) {
+                        val driverData = GpuDriverHelper.customDriverData
+                        if (driverData.name != null) {
                             Toast.makeText(
                                 applicationContext,
                                 getString(
                                     R.string.select_gpu_driver_install_success,
-                                    driverName
+                                    driverData.name
                                 ),
                                 Toast.LENGTH_SHORT
                             ).show()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt
index cf226ad945..eafcf9e427 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DocumentsTree.kt
@@ -7,7 +7,6 @@ import android.net.Uri
 import androidx.documentfile.provider.DocumentFile
 import java.io.File
 import java.util.*
-import org.yuzu.yuzu_emu.YuzuApplication
 import org.yuzu.yuzu_emu.model.MinimalDocumentFile
 
 class DocumentsTree {
@@ -22,7 +21,7 @@ class DocumentsTree {
 
     fun openContentUri(filepath: String, openMode: String?): Int {
         val node = resolvePath(filepath) ?: return -1
-        return FileUtil.openContentUri(YuzuApplication.appContext, node.uri.toString(), openMode)
+        return FileUtil.openContentUri(node.uri.toString(), openMode)
     }
 
     fun getFileSize(filepath: String): Long {
@@ -30,7 +29,7 @@ class DocumentsTree {
         return if (node == null || node.isDirectory) {
             0
         } else {
-            FileUtil.getFileSize(YuzuApplication.appContext, node.uri.toString())
+            FileUtil.getFileSize(node.uri.toString())
         }
     }
 
@@ -67,7 +66,7 @@ class DocumentsTree {
      * @param parent parent node of this level
      */
     private fun structTree(parent: DocumentsNode) {
-        val documents = FileUtil.listFiles(YuzuApplication.appContext, parent.uri!!)
+        val documents = FileUtil.listFiles(parent.uri!!)
         for (document in documents) {
             val node = DocumentsNode(document)
             node.parent = parent
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 c3f53f1c52..a5f89bba6c 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
@@ -3,7 +3,6 @@
 
 package org.yuzu.yuzu_emu.utils
 
-import android.content.Context
 import android.database.Cursor
 import android.net.Uri
 import android.provider.DocumentsContract
@@ -29,6 +28,8 @@ object FileUtil {
     const val APPLICATION_OCTET_STREAM = "application/octet-stream"
     const val TEXT_PLAIN = "text/plain"
 
+    private val context get() = YuzuApplication.appContext
+
     /**
      * Create a file from directory with filename.
      * @param context Application context
@@ -36,11 +37,11 @@ object FileUtil {
      * @param filename file display name.
      * @return boolean
      */
-    fun createFile(context: Context?, directory: String?, filename: String): DocumentFile? {
+    fun createFile(directory: String?, filename: String): DocumentFile? {
         var decodedFilename = filename
         try {
             val directoryUri = Uri.parse(directory)
-            val parent = DocumentFile.fromTreeUri(context!!, directoryUri) ?: return null
+            val parent = DocumentFile.fromTreeUri(context, directoryUri) ?: return null
             decodedFilename = URLDecoder.decode(decodedFilename, DECODE_METHOD)
             var mimeType = APPLICATION_OCTET_STREAM
             if (decodedFilename.endsWith(".txt")) {
@@ -56,16 +57,15 @@ object FileUtil {
 
     /**
      * Create a directory from directory with filename.
-     * @param context Application context
      * @param directory parent path for directory.
      * @param directoryName directory display name.
      * @return boolean
      */
-    fun createDir(context: Context?, directory: String?, directoryName: String?): DocumentFile? {
+    fun createDir(directory: String?, directoryName: String?): DocumentFile? {
         var decodedDirectoryName = directoryName
         try {
             val directoryUri = Uri.parse(directory)
-            val parent = DocumentFile.fromTreeUri(context!!, directoryUri) ?: return null
+            val parent = DocumentFile.fromTreeUri(context, directoryUri) ?: return null
             decodedDirectoryName = URLDecoder.decode(decodedDirectoryName, DECODE_METHOD)
             val isExist = parent.findFile(decodedDirectoryName)
             return isExist ?: parent.createDirectory(decodedDirectoryName)
@@ -77,13 +77,12 @@ object FileUtil {
 
     /**
      * Open content uri and return file descriptor to JNI.
-     * @param context Application context
      * @param path Native content uri path
      * @param openMode will be one of "r", "r", "rw", "wa", "rwa"
      * @return file descriptor
      */
     @JvmStatic
-    fun openContentUri(context: Context, path: String, openMode: String?): Int {
+    fun openContentUri(path: String, openMode: String?): Int {
         try {
             val uri = Uri.parse(path)
             val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, openMode!!)
@@ -103,11 +102,10 @@ object FileUtil {
     /**
      * Reference:  https://stackoverflow.com/questions/42186820/documentfile-is-very-slow
      * This function will be faster than DoucmentFile.listFiles
-     * @param context Application context
      * @param uri Directory uri.
      * @return CheapDocument lists.
      */
-    fun listFiles(context: Context, uri: Uri): Array<MinimalDocumentFile> {
+    fun listFiles(uri: Uri): Array<MinimalDocumentFile> {
         val resolver = context.contentResolver
         val columns = arrayOf(
             DocumentsContract.Document.COLUMN_DOCUMENT_ID,
@@ -145,7 +143,7 @@ object FileUtil {
      * @param path Native content uri path
      * @return bool
      */
-    fun exists(context: Context, path: String?): Boolean {
+    fun exists(path: String?): Boolean {
         var c: Cursor? = null
         try {
             val mUri = Uri.parse(path)
@@ -165,7 +163,7 @@ object FileUtil {
      * @param path content uri path
      * @return bool
      */
-    fun isDirectory(context: Context, path: String): Boolean {
+    fun isDirectory(path: String): Boolean {
         val resolver = context.contentResolver
         val columns = arrayOf(
             DocumentsContract.Document.COLUMN_MIME_TYPE
@@ -210,10 +208,10 @@ object FileUtil {
         return filename
     }
 
-    fun getFilesName(context: Context, path: String): Array<String> {
+    fun getFilesName(path: String): Array<String> {
         val uri = Uri.parse(path)
         val files: MutableList<String> = ArrayList()
-        for (file in listFiles(context, uri)) {
+        for (file in listFiles(uri)) {
             files.add(file.filename)
         }
         return files.toTypedArray()
@@ -225,7 +223,7 @@ object FileUtil {
      * @return long file size
      */
     @JvmStatic
-    fun getFileSize(context: Context, path: String): Long {
+    fun getFileSize(path: String): Long {
         val resolver = context.contentResolver
         val columns = arrayOf(
             DocumentsContract.Document.COLUMN_SIZE
@@ -246,7 +244,6 @@ object FileUtil {
     }
 
     fun copyUriToInternalStorage(
-        context: Context,
         sourceUri: Uri?,
         destinationParentPath: String,
         destinationFilename: String
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 e0ee29c9ba..9001ca9ab3 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
@@ -30,7 +30,7 @@ object GameHelper {
         // Ensure keys are loaded so that ROM metadata can be decrypted.
         NativeLibrary.reloadKeys()
 
-        addGamesRecursive(games, FileUtil.listFiles(context, gamesUri), 3)
+        addGamesRecursive(games, FileUtil.listFiles(gamesUri), 3)
 
         // Cache list of games found on disk
         val serializedGames = mutableSetOf<String>()
@@ -58,7 +58,7 @@ object GameHelper {
             if (it.isDirectory) {
                 addGamesRecursive(
                     games,
-                    FileUtil.listFiles(YuzuApplication.appContext, it.uri),
+                    FileUtil.listFiles(it.uri),
                     depth - 1
                 )
             } else {
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 1d4695a2af..296a8f1cfe 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
@@ -13,6 +13,7 @@ import java.io.IOException
 import java.util.zip.ZipInputStream
 import org.yuzu.yuzu_emu.NativeLibrary
 import org.yuzu.yuzu_emu.utils.FileUtil.copyUriToInternalStorage
+import org.yuzu.yuzu_emu.YuzuApplication
 
 object GpuDriverHelper {
     private const val META_JSON_FILENAME = "meta.json"
@@ -61,6 +62,7 @@ object GpuDriverHelper {
 
             // Initialize the driver installation directory.
             driverInstallationPath = context.filesDir.canonicalPath + "/gpu_driver/"
+                .filesDir.canonicalPath + "/gpu_driver/"
         } catch (e: IOException) {
             throw RuntimeException(e)
         }
@@ -70,6 +72,7 @@ object GpuDriverHelper {
 
         // Initialize hook libraries directory.
         hookLibPath = context.applicationInfo.nativeLibraryDir + "/"
+        hookLibPath = YuzuApplication.appContext.applicationInfo.nativeLibraryDir + "/"
 
         // Initialize GPU driver.
         NativeLibrary.initializeGpuDriver(
@@ -81,15 +84,15 @@ object GpuDriverHelper {
     }
 
     fun installDefaultDriver(context: Context) {
+    fun installDefaultDriver() {
         // Removing the installed driver will result in the backend using the default system driver.
         val driverInstallationDir = File(driverInstallationPath!!)
         deleteRecursive(driverInstallationDir)
-        initializeDriverParameters(context)
     }
 
     fun installCustomDriver(context: Context, driverPathUri: Uri?) {
         // Revert to system default in the event the specified driver is bad.
-        installDefaultDriver(context)
+        installDefaultDriver()
 
         // Ensure we have directories.
         initializeDirectories()

From a5fb9de6faf4c75d231f414949306459de8b0926 Mon Sep 17 00:00:00 2001
From: Charles Lombardo <clombardo169@gmail.com>
Date: Fri, 29 Sep 2023 01:58:03 -0400
Subject: [PATCH 2/2] android: Add GPU driver management fragment

Implements a GPU driver manager that saves all drivers to the user data directory and asynchronously installs drivers when they're needed.
---
 .../yuzu/yuzu_emu/adapters/DriverAdapter.kt   | 117 +++++++++
 .../fragments/DriverManagerFragment.kt        | 185 ++++++++++++++
 .../fragments/DriversLoadingDialogFragment.kt |  75 ++++++
 .../yuzu_emu/fragments/EmulationFragment.kt   |  29 ++-
 .../fragments/HomeSettingsFragment.kt         |  41 +---
 .../IndeterminateProgressDialogFragment.kt    |   8 +-
 .../yuzu/yuzu_emu/model/DriverViewModel.kt    | 158 ++++++++++++
 .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt |  60 +----
 .../java/org/yuzu/yuzu_emu/utils/FileUtil.kt  |  68 +++---
 .../yuzu/yuzu_emu/utils/GpuDriverHelper.kt    | 227 +++++++++++-------
 .../yuzu/yuzu_emu/utils/GpuDriverMetadata.kt  | 112 +++++++--
 .../app/src/main/res/drawable/ic_build.xml    |   9 +
 .../app/src/main/res/drawable/ic_delete.xml   |   9 +
 .../main/res/layout/card_driver_option.xml    |  89 +++++++
 .../res/layout/fragment_driver_manager.xml    |  48 ++++
 .../main/res/navigation/home_navigation.xml   |   7 +
 .../app/src/main/res/values-de/strings.xml    |   2 -
 .../app/src/main/res/values-es/strings.xml    |   2 -
 .../app/src/main/res/values-fr/strings.xml    |   2 -
 .../app/src/main/res/values-it/strings.xml    |   2 -
 .../app/src/main/res/values-ja/strings.xml    |   2 -
 .../app/src/main/res/values-ko/strings.xml    |   2 -
 .../app/src/main/res/values-nb/strings.xml    |   2 -
 .../app/src/main/res/values-pl/strings.xml    |   2 -
 .../src/main/res/values-pt-rBR/strings.xml    |   2 -
 .../src/main/res/values-pt-rPT/strings.xml    |   2 -
 .../app/src/main/res/values-ru/strings.xml    |   2 -
 .../app/src/main/res/values-uk/strings.xml    |   2 -
 .../src/main/res/values-zh-rCN/strings.xml    |   2 -
 .../src/main/res/values-zh-rTW/strings.xml    |   2 -
 .../app/src/main/res/values/dimens.xml        |   2 +
 .../app/src/main/res/values/strings.xml       |   7 +-
 32 files changed, 1013 insertions(+), 266 deletions(-)
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt
 create mode 100644 src/android/app/src/main/res/drawable/ic_build.xml
 create mode 100644 src/android/app/src/main/res/drawable/ic_delete.xml
 create mode 100644 src/android/app/src/main/res/layout/card_driver_option.xml
 create mode 100644 src/android/app/src/main/res/layout/fragment_driver_manager.xml

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
new file mode 100644
index 0000000000..0e818cab99
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
@@ -0,0 +1,117 @@
+// 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.recyclerview.widget.AsyncDifferConfig
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.CardDriverOptionBinding
+import org.yuzu.yuzu_emu.model.DriverViewModel
+import org.yuzu.yuzu_emu.utils.GpuDriverHelper
+import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
+
+class DriverAdapter(private val driverViewModel: DriverViewModel) :
+    ListAdapter<Pair<String, GpuDriverMetadata>, DriverAdapter.DriverViewHolder>(
+        AsyncDifferConfig.Builder(DiffCallback()).build()
+    ) {
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder {
+        val binding =
+            CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+        return DriverViewHolder(binding)
+    }
+
+    override fun getItemCount(): Int = currentList.size
+
+    override fun onBindViewHolder(holder: DriverViewHolder, position: Int) =
+        holder.bind(currentList[position])
+
+    private fun onSelectDriver(position: Int) {
+        driverViewModel.setSelectedDriverIndex(position)
+        notifyItemChanged(driverViewModel.previouslySelectedDriver)
+        notifyItemChanged(driverViewModel.selectedDriver)
+    }
+
+    private fun onDeleteDriver(driverData: Pair<String, GpuDriverMetadata>, position: Int) {
+        if (driverViewModel.selectedDriver > position) {
+            driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1)
+        }
+        if (GpuDriverHelper.customDriverData == driverData.second) {
+            driverViewModel.setSelectedDriverIndex(0)
+        }
+        driverViewModel.driversToDelete.add(driverData.first)
+        driverViewModel.removeDriver(driverData)
+        notifyItemRemoved(position)
+        notifyItemChanged(driverViewModel.selectedDriver)
+    }
+
+    inner class DriverViewHolder(val binding: CardDriverOptionBinding) :
+        RecyclerView.ViewHolder(binding.root) {
+        private lateinit var driverData: Pair<String, GpuDriverMetadata>
+
+        fun bind(driverData: Pair<String, GpuDriverMetadata>) {
+            this.driverData = driverData
+            val driver = driverData.second
+
+            binding.apply {
+                radioButton.isChecked = driverViewModel.selectedDriver == bindingAdapterPosition
+                root.setOnClickListener {
+                    onSelectDriver(bindingAdapterPosition)
+                }
+                buttonDelete.setOnClickListener {
+                    onDeleteDriver(driverData, bindingAdapterPosition)
+                }
+
+                // Delay marquee by 3s
+                title.postDelayed(
+                    {
+                        title.isSelected = true
+                        title.ellipsize = TextUtils.TruncateAt.MARQUEE
+                        version.isSelected = true
+                        version.ellipsize = TextUtils.TruncateAt.MARQUEE
+                        description.isSelected = true
+                        description.ellipsize = TextUtils.TruncateAt.MARQUEE
+                    },
+                    3000
+                )
+                if (driver.name == null) {
+                    title.setText(R.string.system_gpu_driver)
+                    description.text = ""
+                    version.text = ""
+                    version.visibility = View.GONE
+                    description.visibility = View.GONE
+                    buttonDelete.visibility = View.GONE
+                } else {
+                    title.text = driver.name
+                    version.text = driver.version
+                    description.text = driver.description
+                    version.visibility = View.VISIBLE
+                    description.visibility = View.VISIBLE
+                    buttonDelete.visibility = View.VISIBLE
+                }
+            }
+        }
+    }
+
+    private class DiffCallback : DiffUtil.ItemCallback<Pair<String, GpuDriverMetadata>>() {
+        override fun areItemsTheSame(
+            oldItem: Pair<String, GpuDriverMetadata>,
+            newItem: Pair<String, GpuDriverMetadata>
+        ): Boolean {
+            return oldItem.first == newItem.first
+        }
+
+        override fun areContentsTheSame(
+            oldItem: Pair<String, GpuDriverMetadata>,
+            newItem: Pair<String, GpuDriverMetadata>
+        ): Boolean {
+            return oldItem.second == newItem.second
+        }
+    }
+}
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
new file mode 100644
index 0000000000..10b1d35476
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
@@ -0,0 +1,185 @@
+// 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.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.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation.findNavController
+import androidx.recyclerview.widget.GridLayoutManager
+import com.google.android.material.transition.MaterialSharedAxis
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.adapters.DriverAdapter
+import org.yuzu.yuzu_emu.databinding.FragmentDriverManagerBinding
+import org.yuzu.yuzu_emu.model.DriverViewModel
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.utils.FileUtil
+import org.yuzu.yuzu_emu.utils.GpuDriverHelper
+import java.io.IOException
+
+class DriverManagerFragment : Fragment() {
+    private var _binding: FragmentDriverManagerBinding? = null
+    private val binding get() = _binding!!
+
+    private val homeViewModel: HomeViewModel by activityViewModels()
+    private val driverViewModel: DriverViewModel by activityViewModels()
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        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 = FragmentDriverManagerBinding.inflate(inflater)
+        return binding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        homeViewModel.setNavigationVisibility(visible = false, animated = true)
+        homeViewModel.setStatusBarShadeVisibility(visible = false)
+
+        if (!driverViewModel.isInteractionAllowed) {
+            DriversLoadingDialogFragment().show(
+                childFragmentManager,
+                DriversLoadingDialogFragment.TAG
+            )
+        }
+
+        binding.toolbarDrivers.setNavigationOnClickListener {
+            binding.root.findNavController().popBackStack()
+        }
+
+        binding.buttonInstall.setOnClickListener {
+            getDriver.launch(arrayOf("application/zip"))
+        }
+
+        binding.listDrivers.apply {
+            layoutManager = GridLayoutManager(
+                requireContext(),
+                resources.getInteger(R.integer.grid_columns)
+            )
+            adapter = DriverAdapter(driverViewModel)
+        }
+
+        viewLifecycleOwner.lifecycleScope.apply {
+            launch {
+                driverViewModel.driverList.collectLatest {
+                    (binding.listDrivers.adapter as DriverAdapter).submitList(it)
+                }
+            }
+            launch {
+                driverViewModel.newDriverInstalled.collect {
+                    if (_binding != null && it) {
+                        (binding.listDrivers.adapter as DriverAdapter).apply {
+                            notifyItemChanged(driverViewModel.previouslySelectedDriver)
+                            notifyItemChanged(driverViewModel.selectedDriver)
+                            driverViewModel.setNewDriverInstalled(false)
+                        }
+                    }
+                }
+            }
+        }
+
+        setInsets()
+    }
+
+    // Start installing requested driver
+    override fun onStop() {
+        super.onStop()
+        driverViewModel.onCloseDriverManager()
+    }
+
+    private fun setInsets() =
+        ViewCompat.setOnApplyWindowInsetsListener(
+            binding.root
+        ) { _: View, windowInsets: WindowInsetsCompat ->
+            val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+            val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+
+            val leftInsets = barInsets.left + cutoutInsets.left
+            val rightInsets = barInsets.right + cutoutInsets.right
+
+            val mlpAppBar = binding.toolbarDrivers.layoutParams as ViewGroup.MarginLayoutParams
+            mlpAppBar.leftMargin = leftInsets
+            mlpAppBar.rightMargin = rightInsets
+            binding.toolbarDrivers.layoutParams = mlpAppBar
+
+            val mlplistDrivers = binding.listDrivers.layoutParams as ViewGroup.MarginLayoutParams
+            mlplistDrivers.leftMargin = leftInsets
+            mlplistDrivers.rightMargin = rightInsets
+            binding.listDrivers.layoutParams = mlplistDrivers
+
+            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
+
+            binding.listDrivers.updatePadding(
+                bottom = barInsets.bottom +
+                    resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
+            )
+
+            windowInsets
+        }
+
+    private val getDriver =
+        registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+            if (result == null) {
+                return@registerForActivityResult
+            }
+
+            IndeterminateProgressDialogFragment.newInstance(
+                requireActivity(),
+                R.string.installing_driver,
+                false
+            ) {
+                // Ignore file exceptions when a user selects an invalid zip
+                try {
+                    GpuDriverHelper.copyDriverToInternalStorage(result)
+                } catch (_: IOException) {
+                    return@newInstance getString(R.string.select_gpu_driver_error)
+                }
+
+                val driverData = GpuDriverHelper.customDriverData
+                if (driverData.name == null) {
+                    return@newInstance getString(R.string.select_gpu_driver_error)
+                }
+
+                val driverInList =
+                    driverViewModel.driverList.value.firstOrNull { it.second == driverData }
+                if (driverInList != null) {
+                    return@newInstance getString(R.string.driver_already_installed)
+                } else {
+                    driverViewModel.addDriver(
+                        Pair(
+                            "${GpuDriverHelper.driverStoragePath}/${FileUtil.getFilename(result)}",
+                            driverData
+                        )
+                    )
+                    driverViewModel.setNewDriverInstalled(true)
+                }
+                return@newInstance Any()
+            }.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG)
+        }
+}
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
new file mode 100644
index 0000000000..f8c34346a6
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt
@@ -0,0 +1,75 @@
+// 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.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import kotlinx.coroutines.launch
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
+import org.yuzu.yuzu_emu.model.DriverViewModel
+
+class DriversLoadingDialogFragment : DialogFragment() {
+    private val driverViewModel: DriverViewModel by activityViewModels()
+
+    private lateinit var binding: DialogProgressBarBinding
+
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        binding = DialogProgressBarBinding.inflate(layoutInflater)
+        binding.progressBar.isIndeterminate = true
+
+        isCancelable = false
+
+        return MaterialAlertDialogBuilder(requireContext())
+            .setTitle(R.string.loading)
+            .setView(binding.root)
+            .create()
+    }
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View = binding.root
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        viewLifecycleOwner.lifecycleScope.apply {
+            launch {
+                repeatOnLifecycle(Lifecycle.State.RESUMED) {
+                    driverViewModel.areDriversLoading.collect { checkForDismiss() }
+                }
+            }
+            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()
+        }
+    }
+
+    companion object {
+        const val TAG = "DriversLoadingDialogFragment"
+    }
+}
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 e6ad2aa77a..598a9d42bf 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
@@ -39,6 +39,7 @@ import androidx.window.layout.WindowLayoutInfo
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.google.android.material.slider.Slider
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.launch
 import org.yuzu.yuzu_emu.HomeNavigationDirections
@@ -50,6 +51,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.model.DriverViewModel
 import org.yuzu.yuzu_emu.model.Game
 import org.yuzu.yuzu_emu.model.EmulationViewModel
 import org.yuzu.yuzu_emu.overlay.InputOverlay
@@ -70,6 +72,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
     private lateinit var game: Game
 
     private val emulationViewModel: EmulationViewModel by activityViewModels()
+    private val driverViewModel: DriverViewModel by activityViewModels()
 
     private var isInFoldableLayout = false
 
@@ -299,6 +302,21 @@ 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)
+                        }
+                    }
+                }
+            }
         }
     }
 
@@ -332,17 +350,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
         }
     }
 
-    override fun onResume() {
-        super.onResume()
-        if (!DirectoryInitialization.areDirectoriesReady) {
-            DirectoryInitialization.start()
-        }
-
-        updateScreenLayout()
-
-        emulationState.run(emulationActivity!!.isActivityRecreated)
-    }
-
     override fun onPause() {
         if (emulationState.isRunning && emulationActivity?.isInPictureInPictureMode != true) {
             emulationState.pause()
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 18857db2d7..fd97850755 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
@@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.fragments
 
 import android.Manifest
 import android.content.ActivityNotFoundException
-import android.content.DialogInterface
 import android.content.Intent
 import android.content.pm.PackageManager
 import android.os.Bundle
@@ -28,7 +27,6 @@ import androidx.fragment.app.activityViewModels
 import androidx.navigation.findNavController
 import androidx.navigation.fragment.findNavController
 import androidx.recyclerview.widget.LinearLayoutManager
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.google.android.material.transition.MaterialSharedAxis
 import org.yuzu.yuzu_emu.BuildConfig
 import org.yuzu.yuzu_emu.HomeNavigationDirections
@@ -37,6 +35,7 @@ import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter
 import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding
 import org.yuzu.yuzu_emu.features.DocumentProvider
 import org.yuzu.yuzu_emu.features.settings.model.Settings
+import org.yuzu.yuzu_emu.model.DriverViewModel
 import org.yuzu.yuzu_emu.model.HomeSetting
 import org.yuzu.yuzu_emu.model.HomeViewModel
 import org.yuzu.yuzu_emu.ui.main.MainActivity
@@ -50,6 +49,7 @@ class HomeSettingsFragment : Fragment() {
     private lateinit var mainActivity: MainActivity
 
     private val homeViewModel: HomeViewModel by activityViewModels()
+    private val driverViewModel: DriverViewModel by activityViewModels()
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -107,13 +107,17 @@ class HomeSettingsFragment : Fragment() {
             )
             add(
                 HomeSetting(
-                    R.string.install_gpu_driver,
+                    R.string.gpu_driver_manager,
                     R.string.install_gpu_driver_description,
-                    R.drawable.ic_exit,
-                    { driverInstaller() },
+                    R.drawable.ic_build,
+                    {
+                        binding.root.findNavController()
+                            .navigate(R.id.action_homeSettingsFragment_to_driverManagerFragment)
+                    },
                     { GpuDriverHelper.supportsCustomDriverLoading() },
                     R.string.custom_driver_not_supported,
-                    R.string.custom_driver_not_supported_description
+                    R.string.custom_driver_not_supported_description,
+                    driverViewModel.selectedDriverMetadata
                 )
             )
             add(
@@ -292,31 +296,6 @@ class HomeSettingsFragment : Fragment() {
         }
     }
 
-    private fun driverInstaller() {
-        // Get the driver name for the dialog message.
-        var driverName = GpuDriverHelper.customDriverName
-        if (driverName == null) {
-            driverName = getString(R.string.system_gpu_driver)
-        }
-
-        MaterialAlertDialogBuilder(requireContext())
-            .setTitle(getString(R.string.select_gpu_driver_title))
-            .setMessage(driverName)
-            .setNegativeButton(android.R.string.cancel, null)
-            .setNeutralButton(R.string.select_gpu_driver_default) { _: DialogInterface?, _: Int ->
-                GpuDriverHelper.installDefaultDriver()
-                Toast.makeText(
-                    requireContext(),
-                    R.string.select_gpu_driver_use_default,
-                    Toast.LENGTH_SHORT
-                ).show()
-            }
-            .setPositiveButton(R.string.select_gpu_driver_install) { _: DialogInterface?, _: Int ->
-                mainActivity.getDriver.launch(arrayOf("application/zip"))
-            }
-            .show()
-    }
-
     private fun shareLog() {
         val file = DocumentFile.fromSingleUri(
             mainActivity,
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 f128deda8a..7e467814d9 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
@@ -10,8 +10,8 @@ import android.view.View
 import android.view.ViewGroup
 import android.widget.Toast
 import androidx.appcompat.app.AlertDialog
-import androidx.appcompat.app.AppCompatActivity
 import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentActivity
 import androidx.fragment.app.activityViewModels
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.ViewModelProvider
@@ -78,6 +78,10 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
                                     requireActivity().supportFragmentManager,
                                     MessageDialogFragment.TAG
                                 )
+
+                                else -> {
+                                    // Do nothing
+                                }
                             }
                             taskViewModel.clear()
                         }
@@ -115,7 +119,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
         private const val CANCELLABLE = "Cancellable"
 
         fun newInstance(
-            activity: AppCompatActivity,
+            activity: FragmentActivity,
             titleId: Int,
             cancellable: Boolean = false,
             task: () -> Any
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
new file mode 100644
index 0000000000..62945ad650
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt
@@ -0,0 +1,158 @@
+// 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.StateFlow
+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.utils.FileUtil
+import org.yuzu.yuzu_emu.utils.GpuDriverHelper
+import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
+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 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
+
+    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
+                    }
+                }
+
+                // 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
+            }
+        }
+    }
+
+    fun setSelectedDriverIndex(value: Int) {
+        if (selectedDriver != -1) {
+            previouslySelectedDriver = selectedDriver
+        }
+        selectedDriver = value
+    }
+
+    fun setNewDriverInstalled(value: Boolean) {
+        _newDriverInstalled.value = value
+    }
+
+    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
+                ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
+        } else {
+            setSelectedDriverIndex(driverIndex)
+        }
+    }
+
+    fun removeDriver(driverData: Pair<String, GpuDriverMetadata>) {
+        _driverList.value.remove(driverData)
+    }
+
+    fun onCloseDriverManager() {
+        _isDeletingDrivers.value = true
+        viewModelScope.launch {
+            withContext(Dispatchers.IO) {
+                driversToDelete.forEach {
+                    val driver = File(it)
+                    if (driver.exists()) {
+                        driver.delete()
+                    }
+                }
+                driversToDelete.clear()
+                _isDeletingDrivers.value = false
+            }
+        }
+
+        if (GpuDriverHelper.customDriverData == driverList.value[selectedDriver].second) {
+            return
+        }
+
+        _isDriverReady.value = false
+        viewModelScope.launch {
+            withContext(Dispatchers.IO) {
+                if (selectedDriver == 0) {
+                    GpuDriverHelper.installDefaultDriver()
+                    setDriverReady()
+                    return@withContext
+                }
+
+                val driverToInstall = File(driverList.value[selectedDriver].first)
+                if (driverToInstall.exists()) {
+                    GpuDriverHelper.installCustomDriver(driverToInstall)
+                } else {
+                    GpuDriverHelper.installDefaultDriver()
+                }
+                setDriverReady()
+            }
+        }
+    }
+
+    private fun setDriverReady() {
+        _isDriverReady.value = true
+        _selectedDriverMetadata.value = GpuDriverHelper.customDriverData.name
+            ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
+    }
+}
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 ac96c82071..233aa4101a 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
@@ -29,12 +29,10 @@ import androidx.navigation.fragment.NavHostFragment
 import androidx.navigation.ui.setupWithNavController
 import androidx.preference.PreferenceManager
 import com.google.android.material.color.MaterialColors
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import com.google.android.material.navigation.NavigationBarView
 import kotlinx.coroutines.CoroutineScope
 import java.io.File
 import java.io.FilenameFilter
-import java.io.IOException
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
@@ -43,7 +41,6 @@ import org.yuzu.yuzu_emu.NativeLibrary
 import org.yuzu.yuzu_emu.R
 import org.yuzu.yuzu_emu.activities.EmulationActivity
 import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
-import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
 import org.yuzu.yuzu_emu.features.DocumentProvider
 import org.yuzu.yuzu_emu.features.settings.model.Settings
 import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
@@ -346,7 +343,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                 result,
                 dstPath,
                 "prod.keys"
-            )
+            ) != null
         ) {
             if (NativeLibrary.reloadKeys()) {
                 Toast.makeText(
@@ -448,7 +445,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                     result,
                     dstPath,
                     "key_retail.bin"
-                )
+                ) != null
             ) {
                 if (NativeLibrary.reloadKeys()) {
                     Toast.makeText(
@@ -467,59 +464,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
             }
         }
 
-    val getDriver =
-        registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
-            if (result == null) {
-                return@registerForActivityResult
-            }
-
-            val takeFlags =
-                Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
-            contentResolver.takePersistableUriPermission(
-                result,
-                takeFlags
-            )
-
-            val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
-            progressBinding.progressBar.isIndeterminate = true
-            val installationDialog = MaterialAlertDialogBuilder(this)
-                .setTitle(R.string.installing_driver)
-                .setView(progressBinding.root)
-                .show()
-
-            lifecycleScope.launch {
-                withContext(Dispatchers.IO) {
-                    // Ignore file exceptions when a user selects an invalid zip
-                    try {
-                        GpuDriverHelper.installCustomDriver(result)
-                    } catch (_: IOException) {
-                    }
-
-                    withContext(Dispatchers.Main) {
-                        installationDialog.dismiss()
-
-                        val driverData = GpuDriverHelper.customDriverData
-                        if (driverData.name != null) {
-                            Toast.makeText(
-                                applicationContext,
-                                getString(
-                                    R.string.select_gpu_driver_install_success,
-                                    driverData.name
-                                ),
-                                Toast.LENGTH_SHORT
-                            ).show()
-                        } else {
-                            Toast.makeText(
-                                applicationContext,
-                                R.string.select_gpu_driver_error,
-                                Toast.LENGTH_LONG
-                            ).show()
-                        }
-                    }
-                }
-            }
-        }
-
     val installGameUpdate = registerForActivityResult(
         ActivityResultContracts.OpenMultipleDocuments()
     ) { documents: List<Uri> ->
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 a5f89bba6c..5ee74a52cc 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
@@ -10,7 +10,6 @@ import androidx.documentfile.provider.DocumentFile
 import kotlinx.coroutines.flow.StateFlow
 import java.io.BufferedInputStream
 import java.io.File
-import java.io.FileOutputStream
 import java.io.IOException
 import java.io.InputStream
 import java.net.URLDecoder
@@ -20,6 +19,8 @@ import org.yuzu.yuzu_emu.YuzuApplication
 import org.yuzu.yuzu_emu.model.MinimalDocumentFile
 import org.yuzu.yuzu_emu.model.TaskState
 import java.io.BufferedOutputStream
+import java.lang.NullPointerException
+import java.nio.charset.StandardCharsets
 import java.util.zip.ZipOutputStream
 
 object FileUtil {
@@ -243,43 +244,38 @@ object FileUtil {
         return size
     }
 
+    /**
+     * Creates an input stream with a given [Uri] and copies its data to the given path. This will
+     * overwrite any pre-existing files.
+     *
+     * @param sourceUri The [Uri] to copy data from
+     * @param destinationParentPath Destination directory
+     * @param destinationFilename Optionally renames the file once copied
+     */
     fun copyUriToInternalStorage(
-        sourceUri: Uri?,
+        sourceUri: Uri,
         destinationParentPath: String,
-        destinationFilename: String
-    ): Boolean {
-        var input: InputStream? = null
-        var output: FileOutputStream? = null
+        destinationFilename: String = ""
+    ): File? =
         try {
-            input = context.contentResolver.openInputStream(sourceUri!!)
-            output = FileOutputStream("$destinationParentPath/$destinationFilename")
-            val buffer = ByteArray(1024)
-            var len: Int
-            while (input!!.read(buffer).also { len = it } != -1) {
-                output.write(buffer, 0, len)
+            val fileName =
+                if (destinationFilename == "") getFilename(sourceUri) else "/$destinationFilename"
+            val inputStream = context.contentResolver.openInputStream(sourceUri)!!
+
+            val destinationFile = File("$destinationParentPath$fileName")
+            if (destinationFile.exists()) {
+                destinationFile.delete()
             }
-            output.flush()
-            return true
-        } catch (e: Exception) {
-            Log.error("[FileUtil]: Cannot copy file, error: " + e.message)
-        } finally {
-            if (input != null) {
-                try {
-                    input.close()
-                } catch (e: IOException) {
-                    Log.error("[FileUtil]: Cannot close input file, error: " + e.message)
-                }
-            }
-            if (output != null) {
-                try {
-                    output.close()
-                } catch (e: IOException) {
-                    Log.error("[FileUtil]: Cannot close output file, error: " + e.message)
-                }
+
+            destinationFile.outputStream().use { fos ->
+                inputStream.use { it.copyTo(fos) }
             }
+            destinationFile
+        } catch (e: IOException) {
+            null
+        } catch (e: NullPointerException) {
+            null
         }
-        return false
-    }
 
     /**
      * Extracts the given zip file into the given directory.
@@ -365,4 +361,12 @@ object FileUtil {
         return fileName.substring(fileName.lastIndexOf(".") + 1)
             .lowercase()
     }
+
+    @Throws(IOException::class)
+    fun getStringFromFile(file: File): String =
+        String(file.readBytes(), StandardCharsets.UTF_8)
+
+    @Throws(IOException::class)
+    fun getStringFromInputStream(stream: InputStream): String =
+        String(stream.readBytes(), StandardCharsets.UTF_8)
 }
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 296a8f1cfe..f6882ce6ce 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
@@ -3,65 +3,32 @@
 
 package org.yuzu.yuzu_emu.utils
 
-import android.content.Context
 import android.net.Uri
+import android.os.Build
 import java.io.BufferedInputStream
 import java.io.File
-import java.io.FileInputStream
-import java.io.FileOutputStream
 import java.io.IOException
-import java.util.zip.ZipInputStream
 import org.yuzu.yuzu_emu.NativeLibrary
-import org.yuzu.yuzu_emu.utils.FileUtil.copyUriToInternalStorage
 import org.yuzu.yuzu_emu.YuzuApplication
+import java.util.zip.ZipException
+import java.util.zip.ZipFile
 
 object GpuDriverHelper {
     private const val META_JSON_FILENAME = "meta.json"
-    private const val DRIVER_INTERNAL_FILENAME = "gpu_driver.zip"
     private var fileRedirectionPath: String? = null
-    private var driverInstallationPath: String? = null
+    var driverInstallationPath: String? = null
     private var hookLibPath: String? = null
 
-    @Throws(IOException::class)
-    private fun unzip(zipFilePath: String, destDir: String) {
-        val dir = File(destDir)
+    val driverStoragePath get() = DirectoryInitialization.userDirectory!! + "/gpu_drivers/"
 
-        // Create output directory if it doesn't exist
-        if (!dir.exists()) dir.mkdirs()
-
-        // Unpack the files.
-        val inputStream = FileInputStream(zipFilePath)
-        val zis = ZipInputStream(BufferedInputStream(inputStream))
-        val buffer = ByteArray(1024)
-        var ze = zis.nextEntry
-        while (ze != null) {
-            val newFile = File(destDir, ze.name)
-            val canonicalPath = newFile.canonicalPath
-            if (!canonicalPath.startsWith(destDir + ze.name)) {
-                throw SecurityException("Zip file attempted path traversal! " + ze.name)
-            }
-
-            newFile.parentFile!!.mkdirs()
-            val fos = FileOutputStream(newFile)
-            var len: Int
-            while (zis.read(buffer).also { len = it } > 0) {
-                fos.write(buffer, 0, len)
-            }
-            fos.close()
-            zis.closeEntry()
-            ze = zis.nextEntry
-        }
-        zis.closeEntry()
-    }
-
-    fun initializeDriverParameters(context: Context) {
+    fun initializeDriverParameters() {
         try {
             // Initialize the file redirection directory.
-            fileRedirectionPath =
-                context.getExternalFilesDir(null)!!.canonicalPath + "/gpu/vk_file_redirect/"
+            fileRedirectionPath = YuzuApplication.appContext
+                .getExternalFilesDir(null)!!.canonicalPath + "/gpu/vk_file_redirect/"
 
             // Initialize the driver installation directory.
-            driverInstallationPath = context.filesDir.canonicalPath + "/gpu_driver/"
+            driverInstallationPath = YuzuApplication.appContext
                 .filesDir.canonicalPath + "/gpu_driver/"
         } catch (e: IOException) {
             throw RuntimeException(e)
@@ -71,69 +38,169 @@ object GpuDriverHelper {
         initializeDirectories()
 
         // Initialize hook libraries directory.
-        hookLibPath = context.applicationInfo.nativeLibraryDir + "/"
         hookLibPath = YuzuApplication.appContext.applicationInfo.nativeLibraryDir + "/"
 
         // Initialize GPU driver.
         NativeLibrary.initializeGpuDriver(
             hookLibPath,
             driverInstallationPath,
-            customDriverLibraryName,
+            customDriverData.libraryName,
             fileRedirectionPath
         )
     }
 
-    fun installDefaultDriver(context: Context) {
-    fun installDefaultDriver() {
-        // Removing the installed driver will result in the backend using the default system driver.
-        val driverInstallationDir = File(driverInstallationPath!!)
-        deleteRecursive(driverInstallationDir)
+    fun getDrivers(): MutableList<Pair<String, GpuDriverMetadata>> {
+        val driverZips = File(driverStoragePath).listFiles()
+        val drivers: MutableList<Pair<String, GpuDriverMetadata>> =
+            driverZips
+                ?.mapNotNull {
+                    val metadata = getMetadataFromZip(it)
+                    metadata.name?.let { _ -> Pair(it.path, metadata) }
+                }
+                ?.sortedByDescending { it: Pair<String, GpuDriverMetadata> -> it.second.name }
+                ?.distinct()
+                ?.toMutableList() ?: mutableListOf()
+
+        // TODO: Get system driver information
+        drivers.add(0, Pair("", GpuDriverMetadata()))
+        return drivers
     }
 
-    fun installCustomDriver(context: Context, driverPathUri: Uri?) {
+    fun installDefaultDriver() {
+        // Removing the installed driver will result in the backend using the default system driver.
+        File(driverInstallationPath!!).deleteRecursively()
+        initializeDriverParameters()
+    }
+
+    fun copyDriverToInternalStorage(driverUri: Uri): Boolean {
+        // Ensure we have directories.
+        initializeDirectories()
+
+        // Copy the zip file URI to user data
+        val copiedFile =
+            FileUtil.copyUriToInternalStorage(driverUri, driverStoragePath) ?: return false
+
+        // Validate driver
+        val metadata = getMetadataFromZip(copiedFile)
+        if (metadata.name == null) {
+            copiedFile.delete()
+            return false
+        }
+
+        if (metadata.minApi > Build.VERSION.SDK_INT) {
+            copiedFile.delete()
+            return false
+        }
+        return true
+    }
+
+    /**
+     * Copies driver zip into user data directory so that it can be exported along with
+     * other user data and also unzipped into the installation directory
+     */
+    fun installCustomDriver(driverUri: Uri): Boolean {
         // Revert to system default in the event the specified driver is bad.
         installDefaultDriver()
 
         // Ensure we have directories.
         initializeDirectories()
 
-        // Copy the zip file URI into our private storage.
-        copyUriToInternalStorage(
-            context,
-            driverPathUri,
-            driverInstallationPath!!,
-            DRIVER_INTERNAL_FILENAME
-        )
+        // Copy the zip file URI to user data
+        val copiedFile =
+            FileUtil.copyUriToInternalStorage(driverUri, driverStoragePath) ?: return false
+
+        // Validate driver
+        val metadata = getMetadataFromZip(copiedFile)
+        if (metadata.name == null) {
+            copiedFile.delete()
+            return false
+        }
+
+        if (metadata.minApi > Build.VERSION.SDK_INT) {
+            copiedFile.delete()
+            return false
+        }
 
         // Unzip the driver.
         try {
-            unzip(driverInstallationPath + DRIVER_INTERNAL_FILENAME, driverInstallationPath!!)
+            FileUtil.unzipToInternalStorage(
+                BufferedInputStream(copiedFile.inputStream()),
+                File(driverInstallationPath!!)
+            )
         } catch (e: SecurityException) {
-            return
+            return false
         }
 
         // Initialize the driver parameters.
-        initializeDriverParameters(context)
+        initializeDriverParameters()
+
+        return true
+    }
+
+    /**
+     * Unzips driver into installation directory
+     */
+    fun installCustomDriver(driver: File): Boolean {
+        // Revert to system default in the event the specified driver is bad.
+        installDefaultDriver()
+
+        // Ensure we have directories.
+        initializeDirectories()
+
+        // Validate driver
+        val metadata = getMetadataFromZip(driver)
+        if (metadata.name == null) {
+            driver.delete()
+            return false
+        }
+
+        // Unzip the driver to the private installation directory
+        try {
+            FileUtil.unzipToInternalStorage(
+                BufferedInputStream(driver.inputStream()),
+                File(driverInstallationPath!!)
+            )
+        } catch (e: SecurityException) {
+            return false
+        }
+
+        // Initialize the driver parameters.
+        initializeDriverParameters()
+
+        return true
+    }
+
+    /**
+     * Takes in a zip file and reads the meta.json file for presentation to the UI
+     *
+     * @param driver Zip containing driver and meta.json file
+     * @return A non-null [GpuDriverMetadata] instance that may have null members
+     */
+    fun getMetadataFromZip(driver: File): GpuDriverMetadata {
+        try {
+            ZipFile(driver).use { zf ->
+                val entries = zf.entries()
+                while (entries.hasMoreElements()) {
+                    val entry = entries.nextElement()
+                    if (!entry.isDirectory && entry.name.lowercase().contains(".json")) {
+                        zf.getInputStream(entry).use {
+                            return GpuDriverMetadata(it, entry.size)
+                        }
+                    }
+                }
+            }
+        } catch (_: ZipException) {
+        }
+        return GpuDriverMetadata()
     }
 
     external fun supportsCustomDriverLoading(): Boolean
 
     // Parse the custom driver metadata to retrieve the name.
-    val customDriverName: String?
-        get() {
-            val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME)
-            return metadata.name
-        }
+    val customDriverData: GpuDriverMetadata
+        get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME))
 
-    // Parse the custom driver metadata to retrieve the library name.
-    private val customDriverLibraryName: String?
-        get() {
-            // Parse the custom driver metadata to retrieve the library name.
-            val metadata = GpuDriverMetadata(driverInstallationPath + META_JSON_FILENAME)
-            return metadata.libraryName
-        }
-
-    private fun initializeDirectories() {
+    fun initializeDirectories() {
         // Ensure the file redirection directory exists.
         val fileRedirectionDir = File(fileRedirectionPath!!)
         if (!fileRedirectionDir.exists()) {
@@ -144,14 +211,10 @@ object GpuDriverHelper {
         if (!driverInstallationDir.exists()) {
             driverInstallationDir.mkdirs()
         }
-    }
-
-    private fun deleteRecursive(fileOrDirectory: File) {
-        if (fileOrDirectory.isDirectory) {
-            for (child in fileOrDirectory.listFiles()!!) {
-                deleteRecursive(child)
-            }
+        // Ensure the driver storage directory exists
+        val driverStorageDirectory = File(driverStoragePath)
+        if (!driverStorageDirectory.exists()) {
+            driverStorageDirectory.mkdirs()
         }
-        fileOrDirectory.delete()
     }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt
index a4e64070a8..511a4171a1 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverMetadata.kt
@@ -4,29 +4,29 @@
 package org.yuzu.yuzu_emu.utils
 
 import java.io.IOException
-import java.nio.charset.StandardCharsets
-import java.nio.file.Files
-import java.nio.file.Paths
 import org.json.JSONException
 import org.json.JSONObject
+import java.io.File
+import java.io.InputStream
 
-class GpuDriverMetadata(metadataFilePath: String) {
-    var name: String? = null
-    var description: String? = null
-    var author: String? = null
-    var vendor: String? = null
-    var driverVersion: String? = null
-    var minApi = 0
-    var libraryName: String? = null
+class GpuDriverMetadata {
+    /**
+     * Tries to get driver metadata information from a meta.json [File]
+     *
+     * @param metadataFile meta.json file provided with a GPU driver
+     */
+    constructor(metadataFile: File) {
+        if (metadataFile.length() > MAX_META_SIZE_BYTES) {
+            return
+        }
 
-    init {
         try {
-            val json = JSONObject(getStringFromFile(metadataFilePath))
+            val json = JSONObject(FileUtil.getStringFromFile(metadataFile))
             name = json.getString("name")
             description = json.getString("description")
             author = json.getString("author")
             vendor = json.getString("vendor")
-            driverVersion = json.getString("driverVersion")
+            version = json.getString("driverVersion")
             minApi = json.getInt("minApi")
             libraryName = json.getString("libraryName")
         } catch (e: JSONException) {
@@ -36,12 +36,84 @@ class GpuDriverMetadata(metadataFilePath: String) {
         }
     }
 
-    companion object {
-        @Throws(IOException::class)
-        private fun getStringFromFile(filePath: String): String {
-            val path = Paths.get(filePath)
-            val bytes = Files.readAllBytes(path)
-            return String(bytes, StandardCharsets.UTF_8)
+    /**
+     * Tries to get driver metadata information from an input stream that's intended to be
+     * from a zip file
+     *
+     * @param metadataStream ZipEntry input stream
+     * @param size Size of the file in bytes
+     */
+    constructor(metadataStream: InputStream, size: Long) {
+        if (size > MAX_META_SIZE_BYTES) {
+            return
+        }
+
+        try {
+            val json = JSONObject(FileUtil.getStringFromInputStream(metadataStream))
+            name = json.getString("name")
+            description = json.getString("description")
+            author = json.getString("author")
+            vendor = json.getString("vendor")
+            version = json.getString("driverVersion")
+            minApi = json.getInt("minApi")
+            libraryName = json.getString("libraryName")
+        } catch (e: JSONException) {
+            // JSON is malformed, ignore and treat as unsupported metadata.
+        } catch (e: IOException) {
+            // File is inaccessible, ignore and treat as unsupported metadata.
         }
     }
+
+    /**
+     * Creates an empty metadata instance
+     */
+    constructor()
+
+    override fun equals(other: Any?): Boolean {
+        if (other !is GpuDriverMetadata) {
+            return false
+        }
+
+        return other.name == name &&
+            other.description == description &&
+            other.author == author &&
+            other.vendor == vendor &&
+            other.version == version &&
+            other.minApi == minApi &&
+            other.libraryName == libraryName
+    }
+
+    override fun hashCode(): Int {
+        var result = name?.hashCode() ?: 0
+        result = 31 * result + (description?.hashCode() ?: 0)
+        result = 31 * result + (author?.hashCode() ?: 0)
+        result = 31 * result + (vendor?.hashCode() ?: 0)
+        result = 31 * result + (version?.hashCode() ?: 0)
+        result = 31 * result + minApi
+        result = 31 * result + (libraryName?.hashCode() ?: 0)
+        return result
+    }
+
+    override fun toString(): String =
+        """
+            Name - $name
+            Description - $description
+            Author - $author
+            Vendor - $vendor
+            Version - $version
+            Min API - $minApi
+            Library Name - $libraryName
+        """.trimMargin().trimIndent()
+
+    var name: String? = null
+    var description: String? = null
+    var author: String? = null
+    var vendor: String? = null
+    var version: String? = null
+    var minApi = 0
+    var libraryName: String? = null
+
+    companion object {
+        private const val MAX_META_SIZE_BYTES = 500000
+    }
 }
diff --git a/src/android/app/src/main/res/drawable/ic_build.xml b/src/android/app/src/main/res/drawable/ic_build.xml
new file mode 100644
index 0000000000..91d52f1b83
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_build.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="?attr/colorControlNormal"
+        android:pathData="M22.7,19l-9.1,-9.1c0.9,-2.3 0.4,-5 -1.5,-6.9 -2,-2 -5,-2.4 -7.4,-1.3L9,6 6,9 1.6,4.7C0.4,7.1 0.9,10.1 2.9,12.1c1.9,1.9 4.6,2.4 6.9,1.5l9.1,9.1c0.4,0.4 1,0.4 1.4,0l2.3,-2.3c0.5,-0.4 0.5,-1.1 0.1,-1.4z" />
+</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_delete.xml b/src/android/app/src/main/res/drawable/ic_delete.xml
new file mode 100644
index 0000000000..d26a797116
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_delete.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path
+        android:fillColor="?attr/colorControlNormal"
+        android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
+</vector>
diff --git a/src/android/app/src/main/res/layout/card_driver_option.xml b/src/android/app/src/main/res/layout/card_driver_option.xml
new file mode 100644
index 0000000000..1dd9a6d7d3
--- /dev/null
+++ b/src/android/app/src/main/res/layout/card_driver_option.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"
+    android:background="?attr/selectableItemBackground"
+    android:clickable="true"
+    android:focusable="true">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:layout_gravity="center"
+        android:padding="16dp">
+
+        <RadioButton
+            android:id="@+id/radio_button"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:clickable="false"
+            android:checked="false" />
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:orientation="vertical"
+            android:layout_gravity="center_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:ellipsize="none"
+                android:marqueeRepeatLimit="marquee_forever"
+                android:requiresFadingEdge="horizontal"
+                android:singleLine="true"
+                android:textAlignment="viewStart"
+                tools:text="@string/select_gpu_driver_default" />
+
+            <com.google.android.material.textview.MaterialTextView
+                android:id="@+id/version"
+                style="@style/TextAppearance.Material3.BodyMedium"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="6dp"
+                android:ellipsize="none"
+                android:marqueeRepeatLimit="marquee_forever"
+                android:requiresFadingEdge="horizontal"
+                android:singleLine="true"
+                android:textAlignment="viewStart"
+                tools:text="@string/install_gpu_driver_description" />
+
+            <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:ellipsize="none"
+                android:marqueeRepeatLimit="marquee_forever"
+                android:requiresFadingEdge="horizontal"
+                android:singleLine="true"
+                android:textAlignment="viewStart"
+                tools:text="@string/install_gpu_driver_description" />
+
+        </LinearLayout>
+
+        <Button
+            android:id="@+id/button_delete"
+            style="@style/Widget.Material3.Button.IconButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:contentDescription="@string/delete"
+            android:tooltipText="@string/delete"
+            app:icon="@drawable/ic_delete"
+            app:iconTint="?attr/colorControlNormal" />
+
+    </LinearLayout>
+
+</com.google.android.material.card.MaterialCardView>
diff --git a/src/android/app/src/main/res/layout/fragment_driver_manager.xml b/src/android/app/src/main/res/layout/fragment_driver_manager.xml
new file mode 100644
index 0000000000..6cea2d164a
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_driver_manager.xml
@@ -0,0 +1,48 @@
+<?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_licenses"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="?attr/colorSurface">
+
+    <androidx.coordinatorlayout.widget.CoordinatorLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <com.google.android.material.appbar.AppBarLayout
+            android:id="@+id/appbar_drivers"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:fitsSystemWindows="true"
+            app:liftOnScrollTargetViewId="@id/list_drivers">
+
+            <com.google.android.material.appbar.MaterialToolbar
+                android:id="@+id/toolbar_drivers"
+                android:layout_width="match_parent"
+                android:layout_height="?attr/actionBarSize"
+                app:navigationIcon="@drawable/ic_back"
+                app:title="@string/gpu_driver_manager" />
+
+        </com.google.android.material.appbar.AppBarLayout>
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/list_drivers"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:clipToPadding="false"
+            app:layout_behavior="@string/appbar_scrolling_view_behavior" />
+
+    </androidx.coordinatorlayout.widget.CoordinatorLayout>
+
+    <com.google.android.material.floatingactionbutton.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/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml
index 2356b802bf..82749359d2 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -22,6 +22,9 @@
         <action
             android:id="@+id/action_homeSettingsFragment_to_installableFragment"
             app:destination="@id/installableFragment" />
+        <action
+            android:id="@+id/action_homeSettingsFragment_to_driverManagerFragment"
+            app:destination="@id/driverManagerFragment" />
     </fragment>
 
     <fragment
@@ -95,5 +98,9 @@
         android:id="@+id/installableFragment"
         android:name="org.yuzu.yuzu_emu.fragments.InstallableFragment"
         android:label="InstallableFragment" />
+    <fragment
+        android:id="@+id/driverManagerFragment"
+        android:name="org.yuzu.yuzu_emu.fragments.DriverManagerFragment"
+        android:label="DriverManagerFragment" />
 
 </navigation>
diff --git a/src/android/app/src/main/res/values-de/strings.xml b/src/android/app/src/main/res/values-de/strings.xml
index dd0f36392e..72a47fbdbb 100644
--- a/src/android/app/src/main/res/values-de/strings.xml
+++ b/src/android/app/src/main/res/values-de/strings.xml
@@ -168,9 +168,7 @@
     <string name="select_gpu_driver_title">Möchtest du deinen aktuellen GPU-Treiber ersetzen?</string>
     <string name="select_gpu_driver_install">Installieren</string>
     <string name="select_gpu_driver_default">Standard</string>
-    <string name="select_gpu_driver_install_success">%s wurde installiert</string>
     <string name="select_gpu_driver_use_default">Standard GPU-Treiber wird verwendet</string>
-    <string name="select_gpu_driver_error">Ungültiger Treiber ausgewählt, Standard-Treiber wird verwendet!</string>
     <string name="system_gpu_driver">System GPU-Treiber</string>
     <string name="installing_driver">Treiber wird installiert...</string>
 
diff --git a/src/android/app/src/main/res/values-es/strings.xml b/src/android/app/src/main/res/values-es/strings.xml
index d398f862f0..e5bdd58894 100644
--- a/src/android/app/src/main/res/values-es/strings.xml
+++ b/src/android/app/src/main/res/values-es/strings.xml
@@ -171,9 +171,7 @@
     <string name="select_gpu_driver_title">¿Quiere reemplazar el driver de GPU actual?</string>
     <string name="select_gpu_driver_install">Instalar</string>
     <string name="select_gpu_driver_default">Predeterminado</string>
-    <string name="select_gpu_driver_install_success">Instalado %s</string>
     <string name="select_gpu_driver_use_default">Usando el driver de GPU por defecto </string>
-    <string name="select_gpu_driver_error">¡Driver no válido, utilizando el predeterminado del sistema!</string>
     <string name="system_gpu_driver">Driver GPU del sistema</string>
     <string name="installing_driver">Instalando driver...</string>
 
diff --git a/src/android/app/src/main/res/values-fr/strings.xml b/src/android/app/src/main/res/values-fr/strings.xml
index a7abd90770..1e02828aac 100644
--- a/src/android/app/src/main/res/values-fr/strings.xml
+++ b/src/android/app/src/main/res/values-fr/strings.xml
@@ -171,9 +171,7 @@
     <string name="select_gpu_driver_title">Souhaitez vous remplacer votre pilote actuel ?</string>
     <string name="select_gpu_driver_install">Installer</string>
     <string name="select_gpu_driver_default">Défaut</string>
-    <string name="select_gpu_driver_install_success">%s Installé</string>
     <string name="select_gpu_driver_use_default">Utilisation du pilote de GPU par défaut</string>
-    <string name="select_gpu_driver_error">Pilote non valide sélectionné, utilisation du paramètre par défaut du système !</string>
     <string name="system_gpu_driver">Pilote du GPU du système</string>
     <string name="installing_driver">Installation du pilote...</string>
 
diff --git a/src/android/app/src/main/res/values-it/strings.xml b/src/android/app/src/main/res/values-it/strings.xml
index b181618016..09c9345b0f 100644
--- a/src/android/app/src/main/res/values-it/strings.xml
+++ b/src/android/app/src/main/res/values-it/strings.xml
@@ -171,9 +171,7 @@
     <string name="select_gpu_driver_title">Vuoi sostituire il driver della tua GPU attuale?</string>
     <string name="select_gpu_driver_install">Installa</string>
     <string name="select_gpu_driver_default">Predefinito</string>
-    <string name="select_gpu_driver_install_success">Installato%s</string>
     <string name="select_gpu_driver_use_default">Utilizza il driver predefinito della GPU.</string>
-    <string name="select_gpu_driver_error">Il driver selezionato è invalido, è in utilizzo quello predefinito di sistema!</string>
     <string name="system_gpu_driver">Driver GPU del sistema</string>
     <string name="installing_driver">Installando i driver...</string>
 
diff --git a/src/android/app/src/main/res/values-ja/strings.xml b/src/android/app/src/main/res/values-ja/strings.xml
index 88fa5a0bb6..a0ea78befa 100644
--- a/src/android/app/src/main/res/values-ja/strings.xml
+++ b/src/android/app/src/main/res/values-ja/strings.xml
@@ -170,9 +170,7 @@
     <string name="select_gpu_driver_title">現在のGPUドライバーを置き換えますか?</string>
     <string name="select_gpu_driver_install">インストール</string>
     <string name="select_gpu_driver_default">デフォルト</string>
-    <string name="select_gpu_driver_install_success">%s をインストールしました</string>
     <string name="select_gpu_driver_use_default">デフォルトのGPUドライバーを使用します</string>
-    <string name="select_gpu_driver_error">選択されたドライバが無効なため、システムのデフォルトを使用します!</string>
     <string name="system_gpu_driver">システムのGPUドライバ</string>
     <string name="installing_driver">インストール中…</string>
 
diff --git a/src/android/app/src/main/res/values-ko/strings.xml b/src/android/app/src/main/res/values-ko/strings.xml
index 4b658255c3..214f95706d 100644
--- a/src/android/app/src/main/res/values-ko/strings.xml
+++ b/src/android/app/src/main/res/values-ko/strings.xml
@@ -171,9 +171,7 @@
     <string name="select_gpu_driver_title">현재 사용 중인 GPU 드라이버를 교체하겠습니까?</string>
     <string name="select_gpu_driver_install">설치</string>
     <string name="select_gpu_driver_default">기본값</string>
-    <string name="select_gpu_driver_install_success">설치된 %s</string>
     <string name="select_gpu_driver_use_default">기본 GPU 드라이버 사용</string>
-    <string name="select_gpu_driver_error">시스템 기본값을 사용하여 잘못된 드라이버를 선택했습니다!</string>
     <string name="system_gpu_driver">시스템 GPU 드라이버</string>
     <string name="installing_driver">드라이버 설치 중...</string>
 
diff --git a/src/android/app/src/main/res/values-nb/strings.xml b/src/android/app/src/main/res/values-nb/strings.xml
index dd602a3894..5443cef429 100644
--- a/src/android/app/src/main/res/values-nb/strings.xml
+++ b/src/android/app/src/main/res/values-nb/strings.xml
@@ -171,9 +171,7 @@
     <string name="select_gpu_driver_title">Ønsker du å bytte ut din nåværende GPU-driver?</string>
     <string name="select_gpu_driver_install">Installer</string>
     <string name="select_gpu_driver_default">Standard</string>
-    <string name="select_gpu_driver_install_success">Installert %s</string>
     <string name="select_gpu_driver_use_default">Bruk av standard GPU-driver</string>
-    <string name="select_gpu_driver_error">Ugyldig driver valgt, bruker systemstandard!</string>
     <string name="system_gpu_driver">Systemets GPU-driver</string>
     <string name="installing_driver">Installerer driver...</string>
 
diff --git a/src/android/app/src/main/res/values-pl/strings.xml b/src/android/app/src/main/res/values-pl/strings.xml
index 2fdd1f9527..899e233d0a 100644
--- a/src/android/app/src/main/res/values-pl/strings.xml
+++ b/src/android/app/src/main/res/values-pl/strings.xml
@@ -171,9 +171,7 @@
     <string name="select_gpu_driver_title">Chcesz zastąpić obecny sterownik układu graficznego?</string>
     <string name="select_gpu_driver_install">Zainstaluj</string>
     <string name="select_gpu_driver_default">Domyślne</string>
-    <string name="select_gpu_driver_install_success">Zainstalowano %s</string>
     <string name="select_gpu_driver_use_default">Aktywny domyślny sterownik GPU</string>
-    <string name="select_gpu_driver_error">Wybrano błędny sterownik, powrót do domyślnego. </string>
     <string name="system_gpu_driver">Systemowy sterownik GPU</string>
     <string name="installing_driver">Instalowanie sterownika...</string>
 
diff --git a/src/android/app/src/main/res/values-pt-rBR/strings.xml b/src/android/app/src/main/res/values-pt-rBR/strings.xml
index 2f26367fe9..caa0953645 100644
--- a/src/android/app/src/main/res/values-pt-rBR/strings.xml
+++ b/src/android/app/src/main/res/values-pt-rBR/strings.xml
@@ -171,9 +171,7 @@
     <string name="select_gpu_driver_title">Queres substituir o driver do GPU atual? </string>
     <string name="select_gpu_driver_install">Instalar</string>
     <string name="select_gpu_driver_default">Padrão</string>
-    <string name="select_gpu_driver_install_success">Instalado%s</string>
     <string name="select_gpu_driver_use_default">Usar o driver padrão do GPU</string>
-    <string name="select_gpu_driver_error">Driver selecionado inválido, a usar o padrão do sistema!</string>
     <string name="system_gpu_driver">Driver do GPU padrão</string>
     <string name="installing_driver">A instalar o Driver...</string>
 
diff --git a/src/android/app/src/main/res/values-pt-rPT/strings.xml b/src/android/app/src/main/res/values-pt-rPT/strings.xml
index 4e1eb4cd7a..0a1a47fbb0 100644
--- a/src/android/app/src/main/res/values-pt-rPT/strings.xml
+++ b/src/android/app/src/main/res/values-pt-rPT/strings.xml
@@ -171,9 +171,7 @@
     <string name="select_gpu_driver_title">Queres substituir o driver do GPU atual? </string>
     <string name="select_gpu_driver_install">Instalar</string>
     <string name="select_gpu_driver_default">Padrão</string>
-    <string name="select_gpu_driver_install_success">Instalado%s</string>
     <string name="select_gpu_driver_use_default">Usar o driver padrão do GPU</string>
-    <string name="select_gpu_driver_error">Driver selecionado inválido, a usar o padrão do sistema!</string>
     <string name="system_gpu_driver">Driver do GPU padrão</string>
     <string name="installing_driver">A instalar o Driver...</string>
 
diff --git a/src/android/app/src/main/res/values-ru/strings.xml b/src/android/app/src/main/res/values-ru/strings.xml
index f5695dc932..0bef035d63 100644
--- a/src/android/app/src/main/res/values-ru/strings.xml
+++ b/src/android/app/src/main/res/values-ru/strings.xml
@@ -171,9 +171,7 @@
     <string name="select_gpu_driver_title">Хотите заменить текущий драйвер ГП?</string>
     <string name="select_gpu_driver_install">Установить</string>
     <string name="select_gpu_driver_default">По умолчанию</string>
-    <string name="select_gpu_driver_install_success">Установлено %s</string>
     <string name="select_gpu_driver_use_default">Используется стандартный драйвер ГП </string>
-    <string name="select_gpu_driver_error">Выбран неверный драйвер, используется стандартный системный!</string>
     <string name="system_gpu_driver">Системный драйвер ГП</string>
     <string name="installing_driver">Установка драйвера...</string>
 
diff --git a/src/android/app/src/main/res/values-uk/strings.xml b/src/android/app/src/main/res/values-uk/strings.xml
index 061bc6f04e..5b789ee986 100644
--- a/src/android/app/src/main/res/values-uk/strings.xml
+++ b/src/android/app/src/main/res/values-uk/strings.xml
@@ -171,9 +171,7 @@
     <string name="select_gpu_driver_title">Хочете замінити поточний драйвер ГП?</string>
     <string name="select_gpu_driver_install">Встановити</string>
     <string name="select_gpu_driver_default">За замовчуванням</string>
-    <string name="select_gpu_driver_install_success">Встановлено %s</string>
     <string name="select_gpu_driver_use_default">Використовується стандартний драйвер ГП</string>
-    <string name="select_gpu_driver_error">Обрано неправильний драйвер, використовується стандартний системний!</string>
     <string name="system_gpu_driver">Системний драйвер ГП</string>
     <string name="installing_driver">Встановлення драйвера...</string>
 
diff --git a/src/android/app/src/main/res/values-zh-rCN/strings.xml b/src/android/app/src/main/res/values-zh-rCN/strings.xml
index fe6dd5eaa6..c0e8857510 100644
--- a/src/android/app/src/main/res/values-zh-rCN/strings.xml
+++ b/src/android/app/src/main/res/values-zh-rCN/strings.xml
@@ -171,9 +171,7 @@
     <string name="select_gpu_driver_title">要取代您当前的 GPU 驱动程序吗?</string>
     <string name="select_gpu_driver_install">安装</string>
     <string name="select_gpu_driver_default">系统默认</string>
-    <string name="select_gpu_driver_install_success">已安装 %s</string>
     <string name="select_gpu_driver_use_default">使用默认 GPU 驱动程序</string>
-    <string name="select_gpu_driver_error">选择的驱动程序无效,将使用系统默认的驱动程序!</string>
     <string name="system_gpu_driver">系统 GPU 驱动程序</string>
     <string name="installing_driver">正在安装驱动程序…</string>
 
diff --git a/src/android/app/src/main/res/values-zh-rTW/strings.xml b/src/android/app/src/main/res/values-zh-rTW/strings.xml
index 9b3e54224b..4a21bf8933 100644
--- a/src/android/app/src/main/res/values-zh-rTW/strings.xml
+++ b/src/android/app/src/main/res/values-zh-rTW/strings.xml
@@ -171,9 +171,7 @@
     <string name="select_gpu_driver_title">要取代您目前的 GPU 驅動程式嗎?</string>
     <string name="select_gpu_driver_install">安裝</string>
     <string name="select_gpu_driver_default">預設</string>
-    <string name="select_gpu_driver_install_success">已安裝 %s</string>
     <string name="select_gpu_driver_use_default">使用預設 GPU 驅動程式</string>
-    <string name="select_gpu_driver_error">選取的驅動程式無效,將使用系統預設驅動程式!</string>
     <string name="system_gpu_driver">系統 GPU 驅動程式</string>
     <string name="installing_driver">正在安裝驅動程式…</string>
 
diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml
index 7b2296d956..ef855ea6fb 100644
--- a/src/android/app/src/main/res/values/dimens.xml
+++ b/src/android/app/src/main/res/values/dimens.xml
@@ -13,6 +13,8 @@
     <dimen name="menu_width">256dp</dimen>
     <dimen name="card_width">165dp</dimen>
     <dimen name="icon_inset">24dp</dimen>
+    <dimen name="spacing_bottom_list_fab">72dp</dimen>
+    <dimen name="spacing_fab">24dp</dimen>
 
     <dimen name="dialog_margin">20dp</dimen>
     <dimen name="elevated_app_bar">3dp</dimen>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index e51edf872c..9e4854221e 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -72,6 +72,7 @@
     <string name="invalid_keys_error">Invalid encryption keys</string>
     <string name="dumping_keys_quickstart_link">https://yuzu-emu.org/help/quickstart/#dumping-decryption-keys</string>
     <string name="install_keys_failure_description">The selected file is incorrect or corrupt. Please redump your keys.</string>
+    <string name="gpu_driver_manager">GPU Driver Manager</string>
     <string name="install_gpu_driver">Install GPU driver</string>
     <string name="install_gpu_driver_description">Install alternative drivers for potentially better performance or accuracy</string>
     <string name="advanced_settings">Advanced settings</string>
@@ -234,15 +235,17 @@
     <string name="export_failed">Export failed</string>
     <string name="import_failed">Import failed</string>
     <string name="cancelling">Cancelling</string>
+    <string name="install">Install</string>
+    <string name="delete">Delete</string>
 
     <!-- GPU driver installation -->
     <string name="select_gpu_driver">Select GPU driver</string>
     <string name="select_gpu_driver_title">Would you like to replace your current GPU driver?</string>
     <string name="select_gpu_driver_install">Install</string>
     <string name="select_gpu_driver_default">Default</string>
-    <string name="select_gpu_driver_install_success">Installed %s</string>
     <string name="select_gpu_driver_use_default">Using default GPU driver</string>
-    <string name="select_gpu_driver_error">Invalid driver selected, using system default!</string>
+    <string name="select_gpu_driver_error">Invalid driver selected</string>
+    <string name="driver_already_installed">Driver already installed</string>
     <string name="system_gpu_driver">System GPU driver</string>
     <string name="installing_driver">Installing driver…</string>