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 7745c9fc7..6e39e542b 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
@@ -516,6 +516,11 @@ object NativeLibrary {
      */
     external fun submitInlineKeyboardInput(key_code: Int)
 
+    /**
+     * Creates a generic user directory if it doesn't exist already
+     */
+    external fun initializeEmptyUserDirectory()
+
     /**
      * Button type for use in onTouchEvent
      */
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt
new file mode 100644
index 000000000..e960fbaab
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/InstallableAdapter.kt
@@ -0,0 +1,49 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.adapters
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import org.yuzu.yuzu_emu.databinding.CardInstallableBinding
+import org.yuzu.yuzu_emu.model.Installable
+
+class InstallableAdapter(private val installables: List<Installable>) :
+    RecyclerView.Adapter<InstallableAdapter.InstallableViewHolder>() {
+    override fun onCreateViewHolder(
+        parent: ViewGroup,
+        viewType: Int
+    ): InstallableAdapter.InstallableViewHolder {
+        val binding =
+            CardInstallableBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+        return InstallableViewHolder(binding)
+    }
+
+    override fun getItemCount(): Int = installables.size
+
+    override fun onBindViewHolder(holder: InstallableAdapter.InstallableViewHolder, position: Int) =
+        holder.bind(installables[position])
+
+    inner class InstallableViewHolder(val binding: CardInstallableBinding) :
+        RecyclerView.ViewHolder(binding.root) {
+        lateinit var installable: Installable
+
+        fun bind(installable: Installable) {
+            this.installable = installable
+
+            binding.title.setText(installable.titleId)
+            binding.description.setText(installable.descriptionId)
+
+            if (installable.install != null) {
+                binding.buttonInstall.visibility = View.VISIBLE
+                binding.buttonInstall.setOnClickListener { installable.install.invoke() }
+            }
+            if (installable.export != null) {
+                binding.buttonExport.visibility = View.VISIBLE
+                binding.buttonExport.setOnClickListener { installable.export.invoke() }
+            }
+        }
+    }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt
index 7b8f99872..2ff827c6b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AboutFragment.kt
@@ -26,7 +26,6 @@ import org.yuzu.yuzu_emu.BuildConfig
 import org.yuzu.yuzu_emu.R
 import org.yuzu.yuzu_emu.databinding.FragmentAboutBinding
 import org.yuzu.yuzu_emu.model.HomeViewModel
-import org.yuzu.yuzu_emu.ui.main.MainActivity
 
 class AboutFragment : Fragment() {
     private var _binding: FragmentAboutBinding? = null
@@ -93,12 +92,6 @@ class AboutFragment : Fragment() {
             }
         }
 
-        val mainActivity = requireActivity() as MainActivity
-        binding.buttonExport.setOnClickListener { mainActivity.exportUserData.launch("export.zip") }
-        binding.buttonImport.setOnClickListener {
-            mainActivity.importUserData.launch(arrayOf("application/zip"))
-        }
-
         binding.buttonDiscord.setOnClickListener { openLink(getString(R.string.support_link)) }
         binding.buttonWebsite.setOnClickListener { openLink(getString(R.string.website_link)) }
         binding.buttonGithub.setOnClickListener { openLink(getString(R.string.github_link)) }
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 c119e69c9..8923c0ea2 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
@@ -118,18 +118,13 @@ class HomeSettingsFragment : Fragment() {
             )
             add(
                 HomeSetting(
-                    R.string.install_amiibo_keys,
-                    R.string.install_amiibo_keys_description,
-                    R.drawable.ic_nfc,
-                    { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) }
-                )
-            )
-            add(
-                HomeSetting(
-                    R.string.install_game_content,
-                    R.string.install_game_content_description,
-                    R.drawable.ic_system_update_alt,
-                    { mainActivity.installGameUpdate.launch(arrayOf("*/*")) }
+                    R.string.manage_yuzu_data,
+                    R.string.manage_yuzu_data_description,
+                    R.drawable.ic_install,
+                    {
+                        binding.root.findNavController()
+                            .navigate(R.id.action_homeSettingsFragment_to_installableFragment)
+                    }
                 )
             )
             add(
@@ -148,35 +143,6 @@ class HomeSettingsFragment : Fragment() {
                     homeViewModel.gamesDir
                 )
             )
-            add(
-                HomeSetting(
-                    R.string.manage_save_data,
-                    R.string.import_export_saves_description,
-                    R.drawable.ic_save,
-                    {
-                        ImportExportSavesFragment().show(
-                            parentFragmentManager,
-                            ImportExportSavesFragment.TAG
-                        )
-                    }
-                )
-            )
-            add(
-                HomeSetting(
-                    R.string.install_prod_keys,
-                    R.string.install_prod_keys_description,
-                    R.drawable.ic_unlock,
-                    { mainActivity.getProdKey.launch(arrayOf("*/*")) }
-                )
-            )
-            add(
-                HomeSetting(
-                    R.string.install_firmware,
-                    R.string.install_firmware_description,
-                    R.drawable.ic_firmware,
-                    { mainActivity.getFirmware.launch(arrayOf("application/zip")) }
-                )
-            )
             add(
                 HomeSetting(
                     R.string.share_log,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt
deleted file mode 100644
index ee2d44718..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ImportExportSavesFragment.kt
+++ /dev/null
@@ -1,214 +0,0 @@
-// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-package org.yuzu.yuzu_emu.fragments
-
-import android.app.Dialog
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import android.provider.DocumentsContract
-import android.widget.Toast
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.appcompat.app.AppCompatActivity
-import androidx.documentfile.provider.DocumentFile
-import androidx.fragment.app.DialogFragment
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import java.io.BufferedOutputStream
-import java.io.File
-import java.io.FileOutputStream
-import java.io.FilenameFilter
-import java.time.LocalDateTime
-import java.time.format.DateTimeFormatter
-import java.util.zip.ZipEntry
-import java.util.zip.ZipOutputStream
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import org.yuzu.yuzu_emu.R
-import org.yuzu.yuzu_emu.YuzuApplication
-import org.yuzu.yuzu_emu.features.DocumentProvider
-import org.yuzu.yuzu_emu.getPublicFilesDir
-import org.yuzu.yuzu_emu.utils.FileUtil
-
-class ImportExportSavesFragment : DialogFragment() {
-    private val context = YuzuApplication.appContext
-    private val savesFolder =
-        "${context.getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000"
-
-    // Get first subfolder in saves folder (should be the user folder)
-    private val savesFolderRoot = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: ""
-    private var lastZipCreated: File? = null
-
-    private lateinit var startForResultExportSave: ActivityResultLauncher<Intent>
-    private lateinit var documentPicker: ActivityResultLauncher<Array<String>>
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        val activity = requireActivity() as AppCompatActivity
-
-        val activityResultRegistry = requireActivity().activityResultRegistry
-        startForResultExportSave = activityResultRegistry.register(
-            "startForResultExportSaveKey",
-            ActivityResultContracts.StartActivityForResult()
-        ) {
-            File(context.getPublicFilesDir().canonicalPath, "temp").deleteRecursively()
-        }
-        documentPicker = activityResultRegistry.register(
-            "documentPickerKey",
-            ActivityResultContracts.OpenDocument()
-        ) {
-            it?.let { uri -> importSave(uri, activity) }
-        }
-    }
-
-    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
-        return if (savesFolderRoot == "") {
-            MaterialAlertDialogBuilder(requireContext())
-                .setTitle(R.string.manage_save_data)
-                .setMessage(R.string.import_export_saves_no_profile)
-                .setPositiveButton(android.R.string.ok, null)
-                .show()
-        } else {
-            MaterialAlertDialogBuilder(requireContext())
-                .setTitle(R.string.manage_save_data)
-                .setMessage(R.string.manage_save_data_description)
-                .setNegativeButton(R.string.export_saves) { _, _ ->
-                    exportSave()
-                }
-                .setPositiveButton(R.string.import_saves) { _, _ ->
-                    documentPicker.launch(arrayOf("application/zip"))
-                }
-                .setNeutralButton(android.R.string.cancel, null)
-                .show()
-        }
-    }
-
-    /**
-     * Zips the save files located in the given folder path and creates a new zip file with the current date and time.
-     * @return true if the zip file is successfully created, false otherwise.
-     */
-    private fun zipSave(): Boolean {
-        try {
-            val tempFolder = File(requireContext().getPublicFilesDir().canonicalPath, "temp")
-            tempFolder.mkdirs()
-            val saveFolder = File(savesFolderRoot)
-            val outputZipFile = File(
-                tempFolder,
-                "yuzu saves - ${
-                LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
-                }.zip"
-            )
-            outputZipFile.createNewFile()
-            ZipOutputStream(BufferedOutputStream(FileOutputStream(outputZipFile))).use { zos ->
-                saveFolder.walkTopDown().forEach { file ->
-                    val zipFileName =
-                        file.absolutePath.removePrefix(savesFolderRoot).removePrefix("/")
-                    if (zipFileName == "") {
-                        return@forEach
-                    }
-                    val entry = ZipEntry("$zipFileName${(if (file.isDirectory) "/" else "")}")
-                    zos.putNextEntry(entry)
-                    if (file.isFile) {
-                        file.inputStream().use { fis -> fis.copyTo(zos) }
-                    }
-                }
-            }
-            lastZipCreated = outputZipFile
-        } catch (e: Exception) {
-            return false
-        }
-        return true
-    }
-
-    /**
-     * Exports the save file located in the given folder path by creating a zip file and sharing it via intent.
-     */
-    private fun exportSave() {
-        CoroutineScope(Dispatchers.IO).launch {
-            val wasZipCreated = zipSave()
-            val lastZipFile = lastZipCreated
-            if (!wasZipCreated || lastZipFile == null) {
-                withContext(Dispatchers.Main) {
-                    Toast.makeText(context, "Failed to export save", Toast.LENGTH_LONG).show()
-                }
-                return@launch
-            }
-
-            withContext(Dispatchers.Main) {
-                val file = DocumentFile.fromSingleUri(
-                    context,
-                    DocumentsContract.buildDocumentUri(
-                        DocumentProvider.AUTHORITY,
-                        "${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}"
-                    )
-                )!!
-                val intent = Intent(Intent.ACTION_SEND)
-                    .setDataAndType(file.uri, "application/zip")
-                    .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
-                    .putExtra(Intent.EXTRA_STREAM, file.uri)
-                startForResultExportSave.launch(Intent.createChooser(intent, "Share save file"))
-            }
-        }
-    }
-
-    /**
-     * Imports the save files contained in the zip file, and replaces any existing ones with the new save file.
-     * @param zipUri The Uri of the zip file containing the save file(s) to import.
-     */
-    private fun importSave(zipUri: Uri, activity: AppCompatActivity) {
-        val inputZip = context.contentResolver.openInputStream(zipUri)
-        // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid.
-        var validZip = false
-        val savesFolder = File(savesFolderRoot)
-        val cacheSaveDir = File("${context.cacheDir.path}/saves/")
-        cacheSaveDir.mkdir()
-
-        if (inputZip == null) {
-            Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG)
-                .show()
-            return
-        }
-
-        val filterTitleId =
-            FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) }
-
-        try {
-            CoroutineScope(Dispatchers.IO).launch {
-                FileUtil.unzip(inputZip, cacheSaveDir)
-                cacheSaveDir.list(filterTitleId)?.forEach { savePath ->
-                    File(savesFolder, savePath).deleteRecursively()
-                    File(cacheSaveDir, savePath).copyRecursively(File(savesFolder, savePath), true)
-                    validZip = true
-                }
-
-                withContext(Dispatchers.Main) {
-                    if (!validZip) {
-                        MessageDialogFragment.newInstance(
-                            requireActivity(),
-                            titleId = R.string.save_file_invalid_zip_structure,
-                            descriptionId = R.string.save_file_invalid_zip_structure_description
-                        ).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
-                        return@withContext
-                    }
-                    Toast.makeText(
-                        context,
-                        context.getString(R.string.save_file_imported_success),
-                        Toast.LENGTH_LONG
-                    ).show()
-                }
-
-                cacheSaveDir.deleteRecursively()
-            }
-        } catch (e: Exception) {
-            Toast.makeText(context, context.getString(R.string.fatal_error), Toast.LENGTH_LONG)
-                .show()
-        }
-    }
-
-    companion object {
-        const val TAG = "ImportExportSavesFragment"
-    }
-}
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 0d16a7d37..f128deda8 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
@@ -4,12 +4,12 @@
 package org.yuzu.yuzu_emu.fragments
 
 import android.app.Dialog
-import android.content.DialogInterface
 import android.os.Bundle
 import android.view.LayoutInflater
 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.activityViewModels
@@ -39,9 +39,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
             .setView(binding.root)
 
         if (cancellable) {
-            dialog.setNegativeButton(android.R.string.cancel) { _: DialogInterface, _: Int ->
-                taskViewModel.setCancelled(true)
-            }
+            dialog.setNegativeButton(android.R.string.cancel, null)
         }
 
         val alertDialog = dialog.create()
@@ -98,6 +96,18 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
         }
     }
 
+    // By default, the ProgressDialog will immediately dismiss itself upon a button being pressed.
+    // Setting the OnClickListener again after the dialog is shown overrides this behavior.
+    override fun onResume() {
+        super.onResume()
+        val alertDialog = dialog as AlertDialog
+        val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE)
+        negativeButton.setOnClickListener {
+            alertDialog.setTitle(getString(R.string.cancelling))
+            taskViewModel.setCancelled(true)
+        }
+    }
+
     companion object {
         const val TAG = "IndeterminateProgressDialogFragment"
 
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
new file mode 100644
index 000000000..ec116ab62
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
@@ -0,0 +1,138 @@
+// 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.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updatePadding
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.activityViewModels
+import androidx.navigation.findNavController
+import androidx.recyclerview.widget.GridLayoutManager
+import com.google.android.material.transition.MaterialSharedAxis
+import org.yuzu.yuzu_emu.R
+import org.yuzu.yuzu_emu.adapters.InstallableAdapter
+import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding
+import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.model.Installable
+import org.yuzu.yuzu_emu.ui.main.MainActivity
+
+class InstallableFragment : Fragment() {
+    private var _binding: FragmentInstallablesBinding? = null
+    private val binding get() = _binding!!
+
+    private val homeViewModel: HomeViewModel 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 = FragmentInstallablesBinding.inflate(layoutInflater)
+        return binding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        val mainActivity = requireActivity() as MainActivity
+
+        homeViewModel.setNavigationVisibility(visible = false, animated = true)
+        homeViewModel.setStatusBarShadeVisibility(visible = false)
+
+        binding.toolbarInstallables.setNavigationOnClickListener {
+            binding.root.findNavController().popBackStack()
+        }
+
+        val installables = listOf(
+            Installable(
+                R.string.user_data,
+                R.string.user_data_description,
+                install = { mainActivity.importUserData.launch(arrayOf("application/zip")) },
+                export = { mainActivity.exportUserData.launch("export.zip") }
+            ),
+            Installable(
+                R.string.install_game_content,
+                R.string.install_game_content_description,
+                install = { mainActivity.installGameUpdate.launch(arrayOf("*/*")) }
+            ),
+            Installable(
+                R.string.install_firmware,
+                R.string.install_firmware_description,
+                install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) }
+            ),
+            if (mainActivity.savesFolderRoot != "") {
+                Installable(
+                    R.string.manage_save_data,
+                    R.string.import_export_saves_description,
+                    install = { mainActivity.importSaves.launch(arrayOf("application/zip")) },
+                    export = { mainActivity.exportSave() }
+                )
+            } else {
+                Installable(
+                    R.string.manage_save_data,
+                    R.string.import_export_saves_description,
+                    install = { mainActivity.importSaves.launch(arrayOf("application/zip")) }
+                )
+            },
+            Installable(
+                R.string.install_prod_keys,
+                R.string.install_prod_keys_description,
+                install = { mainActivity.getProdKey.launch(arrayOf("*/*")) }
+            ),
+            Installable(
+                R.string.install_amiibo_keys,
+                R.string.install_amiibo_keys_description,
+                install = { mainActivity.getAmiiboKey.launch(arrayOf("*/*")) }
+            )
+        )
+
+        binding.listInstallables.apply {
+            layoutManager = GridLayoutManager(
+                requireContext(),
+                resources.getInteger(R.integer.grid_columns)
+            )
+            adapter = InstallableAdapter(installables)
+        }
+
+        setInsets()
+    }
+
+    private fun setInsets() =
+        ViewCompat.setOnApplyWindowInsetsListener(
+            binding.root
+        ) { _: View, windowInsets: WindowInsetsCompat ->
+            val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+            val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
+
+            val leftInsets = barInsets.left + cutoutInsets.left
+            val rightInsets = barInsets.right + cutoutInsets.right
+
+            val mlpAppBar = binding.toolbarInstallables.layoutParams as ViewGroup.MarginLayoutParams
+            mlpAppBar.leftMargin = leftInsets
+            mlpAppBar.rightMargin = rightInsets
+            binding.toolbarInstallables.layoutParams = mlpAppBar
+
+            val mlpScrollAbout =
+                binding.listInstallables.layoutParams as ViewGroup.MarginLayoutParams
+            mlpScrollAbout.leftMargin = leftInsets
+            mlpScrollAbout.rightMargin = rightInsets
+            binding.listInstallables.layoutParams = mlpScrollAbout
+
+            binding.listInstallables.updatePadding(bottom = barInsets.bottom)
+
+            windowInsets
+        }
+}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt
new file mode 100644
index 000000000..36a7c97b8
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Installable.kt
@@ -0,0 +1,13 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.model
+
+import androidx.annotation.StringRes
+
+data class Installable(
+    @StringRes val titleId: Int,
+    @StringRes val descriptionId: Int,
+    val install: (() -> Unit)? = null,
+    val export: (() -> Unit)? = null
+)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
index d6418a666..16a794dee 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
@@ -50,3 +50,9 @@ class TaskViewModel : ViewModel() {
         }
     }
 }
+
+enum class TaskState {
+    Completed,
+    Failed,
+    Cancelled
+}
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 ee490abc0..0fa5df5e5 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
@@ -6,6 +6,7 @@ package org.yuzu.yuzu_emu.ui.main
 import android.content.Intent
 import android.net.Uri
 import android.os.Bundle
+import android.provider.DocumentsContract
 import android.view.View
 import android.view.ViewGroup.MarginLayoutParams
 import android.view.WindowManager
@@ -19,6 +20,7 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
 import androidx.core.view.ViewCompat
 import androidx.core.view.WindowCompat
 import androidx.core.view.WindowInsetsCompat
+import androidx.documentfile.provider.DocumentFile
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
@@ -29,6 +31,7 @@ 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
@@ -41,20 +44,23 @@ 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
 import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
+import org.yuzu.yuzu_emu.getPublicFilesDir
 import org.yuzu.yuzu_emu.model.GamesViewModel
 import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.model.TaskState
 import org.yuzu.yuzu_emu.model.TaskViewModel
 import org.yuzu.yuzu_emu.utils.*
 import java.io.BufferedInputStream
 import java.io.BufferedOutputStream
-import java.io.FileInputStream
 import java.io.FileOutputStream
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
 import java.util.zip.ZipEntry
 import java.util.zip.ZipInputStream
-import java.util.zip.ZipOutputStream
 
 class MainActivity : AppCompatActivity(), ThemeProvider {
     private lateinit var binding: ActivityMainBinding
@@ -65,6 +71,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
 
     override var themeId: Int = 0
 
+    private val savesFolder
+        get() = "${getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000"
+
+    // Get first subfolder in saves folder (should be the user folder)
+    val savesFolderRoot get() = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: ""
+    private var lastZipCreated: File? = null
+
     override fun onCreate(savedInstanceState: Bundle?) {
         val splashScreen = installSplashScreen()
         splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
@@ -382,7 +395,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
             val task: () -> Any = {
                 var messageToShow: Any
                 try {
-                    FileUtil.unzip(inputZip, cacheFirmwareDir)
+                    FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheFirmwareDir)
                     val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
                     val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
                     messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
@@ -630,35 +643,17 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
             R.string.exporting_user_data,
             true
         ) {
-            val zos = ZipOutputStream(
-                BufferedOutputStream(contentResolver.openOutputStream(result))
+            val zipResult = FileUtil.zipFromInternalStorage(
+                File(DirectoryInitialization.userDirectory!!),
+                DirectoryInitialization.userDirectory!!,
+                BufferedOutputStream(contentResolver.openOutputStream(result)),
+                taskViewModel.cancelled
             )
-            zos.use { stream ->
-                File(DirectoryInitialization.userDirectory!!).walkTopDown().forEach { file ->
-                    if (taskViewModel.cancelled.value) {
-                        return@newInstance R.string.user_data_export_cancelled
-                    }
-
-                    if (!file.isDirectory) {
-                        val newPath = file.path.substring(
-                            DirectoryInitialization.userDirectory!!.length,
-                            file.path.length
-                        )
-                        stream.putNextEntry(ZipEntry(newPath))
-
-                        val buffer = ByteArray(8096)
-                        var read: Int
-                        FileInputStream(file).use { fis ->
-                            while (fis.read(buffer).also { read = it } != -1) {
-                                stream.write(buffer, 0, read)
-                            }
-                        }
-
-                        stream.closeEntry()
-                    }
-                }
+            return@newInstance when (zipResult) {
+                TaskState.Completed -> getString(R.string.user_data_export_success)
+                TaskState.Failed -> R.string.export_failed
+                TaskState.Cancelled -> R.string.user_data_export_cancelled
             }
-            return@newInstance getString(R.string.user_data_export_success)
         }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
     }
 
@@ -686,43 +681,28 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                     }
                 }
                 if (!isYuzuBackup) {
-                    return@newInstance getString(R.string.invalid_yuzu_backup)
+                    return@newInstance MessageDialogFragment.newInstance(
+                        this,
+                        titleId = R.string.invalid_yuzu_backup,
+                        descriptionId = R.string.user_data_import_failed_description
+                    )
                 }
 
+                // Clear existing user data
                 File(DirectoryInitialization.userDirectory!!).deleteRecursively()
 
-                val zis =
-                    ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result)))
-                val userDirectory = File(DirectoryInitialization.userDirectory!!)
-                val canonicalPath = userDirectory.canonicalPath + '/'
-                zis.use { stream ->
-                    var ze: ZipEntry? = stream.nextEntry
-                    while (ze != null) {
-                        val newFile = File(userDirectory, ze!!.name)
-                        val destinationDirectory =
-                            if (ze!!.isDirectory) newFile else newFile.parentFile
-
-                        if (!newFile.canonicalPath.startsWith(canonicalPath)) {
-                            throw SecurityException(
-                                "Zip file attempted path traversal! ${ze!!.name}"
-                            )
-                        }
-
-                        if (!destinationDirectory.isDirectory && !destinationDirectory.mkdirs()) {
-                            throw IOException("Failed to create directory $destinationDirectory")
-                        }
-
-                        if (!ze!!.isDirectory) {
-                            val buffer = ByteArray(8096)
-                            var read: Int
-                            BufferedOutputStream(FileOutputStream(newFile)).use { bos ->
-                                while (zis.read(buffer).also { read = it } != -1) {
-                                    bos.write(buffer, 0, read)
-                                }
-                            }
-                        }
-                        ze = stream.nextEntry
-                    }
+                // Copy archive to internal storage
+                try {
+                    FileUtil.unzipToInternalStorage(
+                        BufferedInputStream(contentResolver.openInputStream(result)),
+                        File(DirectoryInitialization.userDirectory!!)
+                    )
+                } catch (e: Exception) {
+                    return@newInstance MessageDialogFragment.newInstance(
+                        this,
+                        titleId = R.string.import_failed,
+                        descriptionId = R.string.user_data_import_failed_description
+                    )
                 }
 
                 // Reinitialize relevant data
@@ -732,4 +712,146 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                 return@newInstance getString(R.string.user_data_import_success)
             }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
         }
+
+    /**
+     * Zips the save files located in the given folder path and creates a new zip file with the current date and time.
+     * @return true if the zip file is successfully created, false otherwise.
+     */
+    private fun zipSave(): Boolean {
+        try {
+            val tempFolder = File(getPublicFilesDir().canonicalPath, "temp")
+            tempFolder.mkdirs()
+            val saveFolder = File(savesFolderRoot)
+            val outputZipFile = File(
+                tempFolder,
+                "yuzu saves - ${
+                LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
+                }.zip"
+            )
+            outputZipFile.createNewFile()
+            val result = FileUtil.zipFromInternalStorage(
+                saveFolder,
+                savesFolderRoot,
+                BufferedOutputStream(FileOutputStream(outputZipFile))
+            )
+            if (result == TaskState.Failed) {
+                return false
+            }
+            lastZipCreated = outputZipFile
+        } catch (e: Exception) {
+            return false
+        }
+        return true
+    }
+
+    /**
+     * Exports the save file located in the given folder path by creating a zip file and sharing it via intent.
+     */
+    fun exportSave() {
+        CoroutineScope(Dispatchers.IO).launch {
+            val wasZipCreated = zipSave()
+            val lastZipFile = lastZipCreated
+            if (!wasZipCreated || lastZipFile == null) {
+                withContext(Dispatchers.Main) {
+                    Toast.makeText(
+                        this@MainActivity,
+                        getString(R.string.export_save_failed),
+                        Toast.LENGTH_LONG
+                    ).show()
+                }
+                return@launch
+            }
+
+            withContext(Dispatchers.Main) {
+                val file = DocumentFile.fromSingleUri(
+                    this@MainActivity,
+                    DocumentsContract.buildDocumentUri(
+                        DocumentProvider.AUTHORITY,
+                        "${DocumentProvider.ROOT_ID}/temp/${lastZipFile.name}"
+                    )
+                )!!
+                val intent = Intent(Intent.ACTION_SEND)
+                    .setDataAndType(file.uri, "application/zip")
+                    .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+                    .putExtra(Intent.EXTRA_STREAM, file.uri)
+                startForResultExportSave.launch(
+                    Intent.createChooser(
+                        intent,
+                        getString(R.string.share_save_file)
+                    )
+                )
+            }
+        }
+    }
+
+    private val startForResultExportSave =
+        registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ ->
+            File(getPublicFilesDir().canonicalPath, "temp").deleteRecursively()
+        }
+
+    val importSaves =
+        registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+            if (result == null) {
+                return@registerForActivityResult
+            }
+
+            NativeLibrary.initializeEmptyUserDirectory()
+
+            val inputZip = contentResolver.openInputStream(result)
+            // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid.
+            var validZip = false
+            val savesFolder = File(savesFolderRoot)
+            val cacheSaveDir = File("${applicationContext.cacheDir.path}/saves/")
+            cacheSaveDir.mkdir()
+
+            if (inputZip == null) {
+                Toast.makeText(
+                    applicationContext,
+                    getString(R.string.fatal_error),
+                    Toast.LENGTH_LONG
+                ).show()
+                return@registerForActivityResult
+            }
+
+            val filterTitleId =
+                FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) }
+
+            try {
+                CoroutineScope(Dispatchers.IO).launch {
+                    FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
+                    cacheSaveDir.list(filterTitleId)?.forEach { savePath ->
+                        File(savesFolder, savePath).deleteRecursively()
+                        File(cacheSaveDir, savePath).copyRecursively(
+                            File(savesFolder, savePath),
+                            true
+                        )
+                        validZip = true
+                    }
+
+                    withContext(Dispatchers.Main) {
+                        if (!validZip) {
+                            MessageDialogFragment.newInstance(
+                                this@MainActivity,
+                                titleId = R.string.save_file_invalid_zip_structure,
+                                descriptionId = R.string.save_file_invalid_zip_structure_description
+                            ).show(supportFragmentManager, MessageDialogFragment.TAG)
+                            return@withContext
+                        }
+                        Toast.makeText(
+                            applicationContext,
+                            getString(R.string.save_file_imported_success),
+                            Toast.LENGTH_LONG
+                        ).show()
+                    }
+
+                    cacheSaveDir.deleteRecursively()
+                }
+            } catch (e: Exception) {
+                Toast.makeText(
+                    applicationContext,
+                    getString(R.string.fatal_error),
+                    Toast.LENGTH_LONG
+                ).show()
+            }
+        }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
index 142af5f26..c3f53f1c5 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
@@ -8,6 +8,7 @@ import android.database.Cursor
 import android.net.Uri
 import android.provider.DocumentsContract
 import androidx.documentfile.provider.DocumentFile
+import kotlinx.coroutines.flow.StateFlow
 import java.io.BufferedInputStream
 import java.io.File
 import java.io.FileOutputStream
@@ -18,6 +19,9 @@ import java.util.zip.ZipEntry
 import java.util.zip.ZipInputStream
 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.util.zip.ZipOutputStream
 
 object FileUtil {
     const val PATH_TREE = "tree"
@@ -282,30 +286,65 @@ object FileUtil {
 
     /**
      * Extracts the given zip file into the given directory.
-     * @exception IOException if the file was being created outside of the target directory
      */
     @Throws(SecurityException::class)
-    fun unzip(zipStream: InputStream, destDir: File): Boolean {
-        ZipInputStream(BufferedInputStream(zipStream)).use { zis ->
+    fun unzipToInternalStorage(zipStream: BufferedInputStream, destDir: File) {
+        ZipInputStream(zipStream).use { zis ->
             var entry: ZipEntry? = zis.nextEntry
             while (entry != null) {
-                val entryName = entry.name
-                val entryFile = File(destDir, entryName)
-                if (!entryFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) {
-                    throw SecurityException("Entry is outside of the target dir: " + entryFile.name)
+                val newFile = File(destDir, entry.name)
+                val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile
+
+                if (!newFile.canonicalPath.startsWith(destDir.canonicalPath + File.separator)) {
+                    throw SecurityException("Zip file attempted path traversal! ${entry.name}")
                 }
-                if (entry.isDirectory) {
-                    entryFile.mkdirs()
-                } else {
-                    entryFile.parentFile?.mkdirs()
-                    entryFile.createNewFile()
-                    entryFile.outputStream().use { fos -> zis.copyTo(fos) }
+
+                if (!destinationDirectory.isDirectory && !destinationDirectory.mkdirs()) {
+                    throw IOException("Failed to create directory $destinationDirectory")
+                }
+
+                if (!entry.isDirectory) {
+                    newFile.outputStream().use { fos -> zis.copyTo(fos) }
                 }
                 entry = zis.nextEntry
             }
         }
+    }
 
-        return true
+    /**
+     * Creates a zip file from a directory within internal storage
+     * @param inputFile File representation of the item that will be zipped
+     * @param rootDir Directory containing the inputFile
+     * @param outputStream Stream where the zip file will be output
+     */
+    fun zipFromInternalStorage(
+        inputFile: File,
+        rootDir: String,
+        outputStream: BufferedOutputStream,
+        cancelled: StateFlow<Boolean>? = null
+    ): TaskState {
+        try {
+            ZipOutputStream(outputStream).use { zos ->
+                inputFile.walkTopDown().forEach { file ->
+                    if (cancelled?.value == true) {
+                        return TaskState.Cancelled
+                    }
+
+                    if (!file.isDirectory) {
+                        val entryName =
+                            file.absolutePath.removePrefix(rootDir).removePrefix("/")
+                        val entry = ZipEntry(entryName)
+                        zos.putNextEntry(entry)
+                        if (file.isFile) {
+                            file.inputStream().use { fis -> fis.copyTo(zos) }
+                        }
+                    }
+                }
+            }
+        } catch (e: Exception) {
+            return TaskState.Failed
+        }
+        return TaskState.Completed
     }
 
     fun isRootTreeUri(uri: Uri): Boolean {
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index 71ef2833d..9fa082dd5 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -13,6 +13,8 @@
 
 #include <android/api-level.h>
 #include <android/native_window_jni.h>
+#include <common/fs/fs.h>
+#include <core/file_sys/savedata_factory.h>
 #include <core/loader/nro.h>
 #include <jni.h>
 
@@ -881,4 +883,24 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_submitInlineKeyboardInput(JNIEnv* env
     EmulationSession::GetInstance().SoftwareKeyboard()->SubmitInlineKeyboardInput(j_key_code);
 }
 
+void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeEmptyUserDirectory(JNIEnv* env,
+                                                                        jobject instance) {
+    const auto nand_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir);
+    auto vfs_nand_dir = EmulationSession::GetInstance().System().GetFilesystem()->OpenDirectory(
+        Common::FS::PathToUTF8String(nand_dir), FileSys::Mode::Read);
+
+    Service::Account::ProfileManager manager;
+    const auto user_id = manager.GetUser(static_cast<std::size_t>(0));
+    ASSERT(user_id);
+
+    const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath(
+        EmulationSession::GetInstance().System(), vfs_nand_dir, FileSys::SaveDataSpaceId::NandUser,
+        FileSys::SaveDataType::SaveData, 1, user_id->AsU128(), 0);
+
+    const auto full_path = Common::FS::ConcatPathSafe(nand_dir, user_save_data_path);
+    if (!Common::FS::CreateParentDirs(full_path)) {
+        LOG_WARNING(Frontend, "Failed to create full path of the default user's save directory");
+    }
+}
+
 } // extern "C"
diff --git a/src/android/app/src/main/res/layout/card_installable.xml b/src/android/app/src/main/res/layout/card_installable.xml
new file mode 100644
index 000000000..f5b0e3741
--- /dev/null
+++ b/src/android/app/src/main/res/layout/card_installable.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    style="?attr/materialCardViewOutlinedStyle"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginHorizontal="16dp"
+    android:layout_marginVertical="12dp">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_margin="16dp"
+        android:orientation="horizontal"
+        android:layout_gravity="center">
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_marginEnd="16dp"
+            android:layout_weight="1"
+            android:orientation="vertical">
+
+            <com.google.android.material.textview.MaterialTextView
+                android:id="@+id/title"
+                style="@style/TextAppearance.Material3.TitleMedium"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/user_data"
+                android:textAlignment="viewStart" />
+
+            <com.google.android.material.textview.MaterialTextView
+                android:id="@+id/description"
+                style="@style/TextAppearance.Material3.BodyMedium"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="6dp"
+                android:text="@string/user_data_description"
+                android:textAlignment="viewStart" />
+
+        </LinearLayout>
+
+        <Button
+            android:id="@+id/button_export"
+            style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:contentDescription="@string/export"
+            android:tooltipText="@string/export"
+            android:visibility="gone"
+            app:icon="@drawable/ic_export"
+            tools:visibility="visible" />
+
+        <Button
+            android:id="@+id/button_install"
+            style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:layout_marginStart="12dp"
+            android:contentDescription="@string/string_import"
+            android:tooltipText="@string/string_import"
+            android:visibility="gone"
+            app:icon="@drawable/ic_import"
+            tools:visibility="visible" />
+
+    </LinearLayout>
+
+</com.google.android.material.card.MaterialCardView>
diff --git a/src/android/app/src/main/res/layout/fragment_about.xml b/src/android/app/src/main/res/layout/fragment_about.xml
index 36b350338..3e1d98451 100644
--- a/src/android/app/src/main/res/layout/fragment_about.xml
+++ b/src/android/app/src/main/res/layout/fragment_about.xml
@@ -176,67 +176,6 @@
 
             </LinearLayout>
 
-            <com.google.android.material.divider.MaterialDivider
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:layout_marginHorizontal="20dp" />
-
-            <LinearLayout
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content"
-                android:orientation="horizontal">
-
-                <LinearLayout
-                    android:layout_width="match_parent"
-                    android:layout_height="wrap_content"
-                    android:paddingVertical="16dp"
-                    android:paddingHorizontal="16dp"
-                    android:orientation="vertical"
-                    android:layout_weight="1">
-
-                    <com.google.android.material.textview.MaterialTextView
-                        style="@style/TextAppearance.Material3.TitleMedium"
-                        android:layout_width="match_parent"
-                        android:layout_height="wrap_content"
-                        android:layout_marginHorizontal="24dp"
-                        android:textAlignment="viewStart"
-                        android:text="@string/user_data" />
-
-                    <com.google.android.material.textview.MaterialTextView
-                        style="@style/TextAppearance.Material3.BodyMedium"
-                        android:layout_width="match_parent"
-                        android:layout_height="wrap_content"
-                        android:layout_marginHorizontal="24dp"
-                        android:layout_marginTop="6dp"
-                        android:textAlignment="viewStart"
-                        android:text="@string/user_data_description" />
-
-                </LinearLayout>
-
-                <Button
-                    android:id="@+id/button_import"
-                    style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_gravity="center_vertical"
-                    android:contentDescription="@string/string_import"
-                    android:tooltipText="@string/string_import"
-                    app:icon="@drawable/ic_import" />
-
-                <Button
-                    android:id="@+id/button_export"
-                    style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
-                    android:layout_width="wrap_content"
-                    android:layout_height="wrap_content"
-                    android:layout_marginStart="12dp"
-                    android:layout_marginEnd="24dp"
-                    android:layout_gravity="center_vertical"
-                    android:contentDescription="@string/export"
-                    android:tooltipText="@string/export"
-                    app:icon="@drawable/ic_export" />
-
-            </LinearLayout>
-
             <com.google.android.material.divider.MaterialDivider
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
diff --git a/src/android/app/src/main/res/layout/fragment_installables.xml b/src/android/app/src/main/res/layout/fragment_installables.xml
new file mode 100644
index 000000000..3a4df81a6
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_installables.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/coordinator_licenses"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="?attr/colorSurface">
+
+    <com.google.android.material.appbar.AppBarLayout
+        android:id="@+id/appbar_installables"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:fitsSystemWindows="true">
+
+        <com.google.android.material.appbar.MaterialToolbar
+            android:id="@+id/toolbar_installables"
+            android:layout_width="match_parent"
+            android:layout_height="?attr/actionBarSize"
+            app:title="@string/manage_yuzu_data"
+            app:navigationIcon="@drawable/ic_back" />
+
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/list_installables"
+        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>
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 2e0ce7a3d..2356b802b 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -19,6 +19,9 @@
         <action
             android:id="@+id/action_homeSettingsFragment_to_earlyAccessFragment"
             app:destination="@id/earlyAccessFragment" />
+        <action
+            android:id="@+id/action_homeSettingsFragment_to_installableFragment"
+            app:destination="@id/installableFragment" />
     </fragment>
 
     <fragment
@@ -88,5 +91,9 @@
     <action
         android:id="@+id/action_global_settingsActivity"
         app:destination="@id/settingsActivity" />
+    <fragment
+        android:id="@+id/installableFragment"
+        android:name="org.yuzu.yuzu_emu.fragments.InstallableFragment"
+        android:label="InstallableFragment" />
 
 </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 daaa7ffde..dd0f36392 100644
--- a/src/android/app/src/main/res/values-de/strings.xml
+++ b/src/android/app/src/main/res/values-de/strings.xml
@@ -79,7 +79,6 @@
     <string name="manage_save_data">Speicherdaten verwalten</string>
     <string name="manage_save_data_description">Speicherdaten gefunden. Bitte wähle unten eine Option aus.</string>
     <string name="import_export_saves_description">Speicherdaten importieren oder exportieren</string>
-    <string name="import_export_saves_no_profile">Keine Speicherdaten gefunden. Bitte starte ein Spiel und versuche es erneut.</string>
     <string name="save_file_imported_success">Erfolgreich importiert</string>
     <string name="save_file_invalid_zip_structure">Ungültige Speicherverzeichnisstruktur</string>
     <string name="save_file_invalid_zip_structure_description">Der erste Unterordnername muss die Titel-ID des Spiels sein.</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 e9129cb00..d398f862f 100644
--- a/src/android/app/src/main/res/values-es/strings.xml
+++ b/src/android/app/src/main/res/values-es/strings.xml
@@ -81,7 +81,6 @@
     <string name="manage_save_data">Administrar datos de guardado</string>
     <string name="manage_save_data_description">Guardar los datos encontrados. Por favor, seleccione una opción de abajo.</string>
     <string name="import_export_saves_description">Importar o exportar archivos de guardado</string>
-    <string name="import_export_saves_no_profile">No se han encontrado datos de guardado. Por favor, ejecute un juego y vuelva a intentarlo.</string>
     <string name="save_file_imported_success">Importado correctamente</string>
     <string name="save_file_invalid_zip_structure">Estructura del directorio de guardado no válido</string>
     <string name="save_file_invalid_zip_structure_description">El nombre de la primera subcarpeta debe ser el Title ID del juego.</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 2d99d618e..a7abd9077 100644
--- a/src/android/app/src/main/res/values-fr/strings.xml
+++ b/src/android/app/src/main/res/values-fr/strings.xml
@@ -81,7 +81,6 @@
     <string name="manage_save_data">Gérer les données de sauvegarde</string>
     <string name="manage_save_data_description">Données de sauvegarde trouvées. Veuillez sélectionner une option ci-dessous.</string>
     <string name="import_export_saves_description">Importer ou exporter des fichiers de sauvegarde</string>
-    <string name="import_export_saves_no_profile">Aucune données de sauvegarde trouvées. Veuillez lancer un jeu et réessayer.</string>
     <string name="save_file_imported_success">Importé avec succès</string>
     <string name="save_file_invalid_zip_structure">Structure de répertoire de sauvegarde non valide</string>
     <string name="save_file_invalid_zip_structure_description">Le nom du premier sous-dossier doit être l\'identifiant du titre du jeu.</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 d9c3de385..b18161801 100644
--- a/src/android/app/src/main/res/values-it/strings.xml
+++ b/src/android/app/src/main/res/values-it/strings.xml
@@ -81,7 +81,6 @@
     <string name="manage_save_data">Gestisci i salvataggi</string>
     <string name="manage_save_data_description">Salvataggio non trovato. Seleziona un\'opzione di seguito.</string>
     <string name="import_export_saves_description">Importa o esporta i salvataggi</string>
-    <string name="import_export_saves_no_profile">Nessun salvataggio trovato. Avvia un gioco e riprova.</string>
     <string name="save_file_imported_success">Importato con successo</string>
     <string name="save_file_invalid_zip_structure">La struttura della cartella dei salvataggi è invalida</string>
     <string name="save_file_invalid_zip_structure_description">La prima sotto cartella <b>deve</b> chiamarsi come l\'ID del titolo del gioco.</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 7a226cd5c..88fa5a0bb 100644
--- a/src/android/app/src/main/res/values-ja/strings.xml
+++ b/src/android/app/src/main/res/values-ja/strings.xml
@@ -80,7 +80,6 @@
     <string name="manage_save_data">セーブデータを管理</string>
     <string name="manage_save_data_description">セーブデータが見つかりました。以下のオプションから選択してください。</string>
     <string name="import_export_saves_description">セーブファイルをインポート/エクスポート</string>
-    <string name="import_export_saves_no_profile">セーブデータがありません。ゲームを起動してから再度お試しください。</string>
     <string name="save_file_imported_success">インポートが完了しました</string>
     <string name="save_file_invalid_zip_structure">セーブデータのディレクトリ構造が無効です</string>
     <string name="save_file_invalid_zip_structure_description">最初のサブフォルダ名は、ゲームのタイトルIDである必要があります。</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 427b6e5a0..4b658255c 100644
--- a/src/android/app/src/main/res/values-ko/strings.xml
+++ b/src/android/app/src/main/res/values-ko/strings.xml
@@ -81,7 +81,6 @@
     <string name="manage_save_data">저장 데이터 관리</string>
     <string name="manage_save_data_description">데이터를 저장했습니다. 아래에서 옵션을 선택하세요.</string>
     <string name="import_export_saves_description">저장 파일 가져오기 또는 내보내기</string>
-    <string name="import_export_saves_no_profile">저장 데이터를 찾을 수 없습니다. 게임을 실행한 후 다시 시도하세요.</string>
     <string name="save_file_imported_success">가져오기 성공</string>
     <string name="save_file_invalid_zip_structure">저장 디렉터리 구조가 잘못됨</string>
     <string name="save_file_invalid_zip_structure_description">첫 번째 하위 폴더 이름은 게임의 타이틀 ID여야 합니다.</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 ce8d7a9e4..dd602a389 100644
--- a/src/android/app/src/main/res/values-nb/strings.xml
+++ b/src/android/app/src/main/res/values-nb/strings.xml
@@ -81,7 +81,6 @@
     <string name="manage_save_data">Administrere lagringsdata</string>
     <string name="manage_save_data_description">Lagringsdata funnet. Velg et alternativ nedenfor.</string>
     <string name="import_export_saves_description">Importer eller eksporter lagringsfiler</string>
-    <string name="import_export_saves_no_profile">Ingen lagringsdata funnet. Start et nytt spill og prøv på nytt.</string>
     <string name="save_file_imported_success">Vellykket import</string>
     <string name="save_file_invalid_zip_structure">Ugyldig struktur for lagringskatalog</string>
     <string name="save_file_invalid_zip_structure_description">Det første undermappenavnet må være spillets tittel-ID.</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 c2c24b48f..2fdd1f952 100644
--- a/src/android/app/src/main/res/values-pl/strings.xml
+++ b/src/android/app/src/main/res/values-pl/strings.xml
@@ -81,7 +81,6 @@
     <string name="manage_save_data">Zarządzaj plikami zapisów gier</string>
     <string name="manage_save_data_description">Znaleziono pliki zapisów gier. Wybierz opcję poniżej.</string>
     <string name="import_export_saves_description">Importuj lub wyeksportuj pliki zapisów</string>
-    <string name="import_export_saves_no_profile">Nie znaleziono plików zapisów. Uruchom grę i spróbuj ponownie.</string>
     <string name="save_file_imported_success">Zaimportowano pomyślnie</string>
     <string name="save_file_invalid_zip_structure">Niepoprawna struktura folderów</string>
     <string name="save_file_invalid_zip_structure_description">Pierwszy podkatalog musi zawierać w nazwie numer ID tytułu gry.</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 04f276108..2f26367fe 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
@@ -81,7 +81,6 @@
     <string name="manage_save_data">Gerir dados guardados</string>
     <string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string>
     <string name="import_export_saves_description">Importa ou exporta dados guardados</string>
-    <string name="import_export_saves_no_profile">Dados não encontrados. Por favor lança o jogo e tenta novamente.</string>
     <string name="save_file_imported_success">Importado com sucesso</string>
     <string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string>
     <string name="save_file_invalid_zip_structure_description">O nome da primeira sub pasta tem de ser a ID do jogo.</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 66a3a1a2e..4e1eb4cd7 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
@@ -81,7 +81,6 @@
     <string name="manage_save_data">Gerir dados guardados</string>
     <string name="manage_save_data_description">Dados não encontrados. Por favor seleciona uma opção abaixo.</string>
     <string name="import_export_saves_description">Importa ou exporta dados guardados</string>
-    <string name="import_export_saves_no_profile">Dados não encontrados. Por favor lança o jogo e tenta novamente.</string>
     <string name="save_file_imported_success">Importado com sucesso</string>
     <string name="save_file_invalid_zip_structure">Estrutura de diretório de dados invalida</string>
     <string name="save_file_invalid_zip_structure_description">O nome da primeira sub pasta tem de ser a ID do jogo.</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 f770e954f..f5695dc93 100644
--- a/src/android/app/src/main/res/values-ru/strings.xml
+++ b/src/android/app/src/main/res/values-ru/strings.xml
@@ -81,7 +81,6 @@
     <string name="manage_save_data">Управление данными сохранений</string>
     <string name="manage_save_data_description">Найдено данные сохранений. Пожалуйста, выберите вариант ниже.</string>
     <string name="import_export_saves_description">Импорт или экспорт файлов сохранения</string>
-    <string name="import_export_saves_no_profile">Данные сохранений не найдены. Пожалуйста, запустите игру и повторите попытку.</string>
     <string name="save_file_imported_success">Успешно импортировано</string>
     <string name="save_file_invalid_zip_structure">Недопустимая структура папки сохранения</string>
     <string name="save_file_invalid_zip_structure_description">Название первой вложенной папки должно быть идентификатором игры.</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 ea3ab1b15..061bc6f04 100644
--- a/src/android/app/src/main/res/values-uk/strings.xml
+++ b/src/android/app/src/main/res/values-uk/strings.xml
@@ -81,7 +81,6 @@
     <string name="manage_save_data">Керування даними збережень</string>
     <string name="manage_save_data_description">Знайдено дані збережень. Будь ласка, виберіть варіант нижче.</string>
     <string name="import_export_saves_description">Імпорт або експорт файлів збереження</string>
-    <string name="import_export_saves_no_profile">Дані збережень не знайдено. Будь ласка, запустіть гру та повторіть спробу.</string>
     <string name="save_file_imported_success">Успішно імпортовано</string>
     <string name="save_file_invalid_zip_structure">Неприпустима структура папки збереження</string>
     <string name="save_file_invalid_zip_structure_description">Назва першої вкладеної папки має бути ідентифікатором гри.</string>
diff --git a/src/android/app/src/main/res/values-w600dp/integers.xml b/src/android/app/src/main/res/values-w600dp/integers.xml
new file mode 100644
index 000000000..9975db801
--- /dev/null
+++ b/src/android/app/src/main/res/values-w600dp/integers.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <integer name="grid_columns">2</integer>
+
+</resources>
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 b45a5a528..fe6dd5eaa 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
@@ -81,7 +81,6 @@
     <string name="manage_save_data">管理存档数据</string>
     <string name="manage_save_data_description">已找到存档数据,请选择下方的选项。</string>
     <string name="import_export_saves_description">导入或导出存档</string>
-    <string name="import_export_saves_no_profile">找不到存档数据,请启动游戏并重试。</string>
     <string name="save_file_imported_success">已成功导入存档</string>
     <string name="save_file_invalid_zip_structure">无效的存档目录</string>
     <string name="save_file_invalid_zip_structure_description">第一个子文件夹名称必须为当前游戏的 ID。</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 3aab889e4..9b3e54224 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
@@ -81,7 +81,6 @@
     <string name="manage_save_data">管理儲存資料</string>
     <string name="manage_save_data_description">已找到儲存資料,請選取下方的選項。</string>
     <string name="import_export_saves_description">匯入或匯出儲存檔案</string>
-    <string name="import_export_saves_no_profile">找不到儲存資料,請啟動遊戲並重試。</string>
     <string name="save_file_imported_success">已成功匯入</string>
     <string name="save_file_invalid_zip_structure">無效的儲存目錄結構</string>
     <string name="save_file_invalid_zip_structure_description">首個子資料夾名稱必須為遊戲標題 ID。</string>
diff --git a/src/android/app/src/main/res/values/integers.xml b/src/android/app/src/main/res/values/integers.xml
index 5e39bc7d9..dc527965c 100644
--- a/src/android/app/src/main/res/values/integers.xml
+++ b/src/android/app/src/main/res/values/integers.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
-    <integer name="game_title_lines">2</integer>
+    <integer name="grid_columns">1</integer>
 
     <!-- Default SWITCH landscape layout -->
     <integer name="SWITCH_BUTTON_A_X">760</integer>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 574290479..21a40238c 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -90,7 +90,6 @@
     <string name="manage_save_data">Manage save data</string>
     <string name="manage_save_data_description">Save data found. Please select an option below.</string>
     <string name="import_export_saves_description">Import or export save files</string>
-    <string name="import_export_saves_no_profile">No save data found. Please launch a game and retry.</string>
     <string name="save_file_imported_success">Imported successfully</string>
     <string name="save_file_invalid_zip_structure">Invalid save directory structure</string>
     <string name="save_file_invalid_zip_structure_description">The first subfolder name must be the title ID of the game.</string>
@@ -101,7 +100,7 @@
     <string name="firmware_installing">Installing firmware</string>
     <string name="firmware_installed_success">Firmware installed successfully</string>
     <string name="firmware_installed_failure">Firmware installation failed</string>
-    <string name="firmware_installed_failure_description">Verify that the ZIP contains valid firmware and try again.</string>
+    <string name="firmware_installed_failure_description">Make sure the firmware nca files are at the root of the zip and try again.</string>
     <string name="share_log">Share debug logs</string>
     <string name="share_log_description">Share yuzu\'s log file to debug issues</string>
     <string name="share_log_missing">No log file found</string>
@@ -119,6 +118,10 @@
     <string name="install_game_content_help_link">https://yuzu-emu.org/help/quickstart/#dumping-installed-updates</string>
     <string name="custom_driver_not_supported">Custom drivers not supported</string>
     <string name="custom_driver_not_supported_description">Custom driver loading isn\'t currently supported for this device.\nCheck this option again in the future to see if support was added!</string>
+    <string name="manage_yuzu_data">Manage yuzu data</string>
+    <string name="manage_yuzu_data_description">Import/export firmware, keys, user data, and more!</string>
+    <string name="share_save_file">Share save file</string>
+    <string name="export_save_failed">Failed to export save</string>
 
     <!-- About screen strings -->
     <string name="gaia_is_not_real">Gaia isn\'t real</string>
@@ -138,6 +141,7 @@
     <string name="user_data_export_success">User data exported successfully</string>
     <string name="user_data_import_success">User data imported successfully</string>
     <string name="user_data_export_cancelled">Export cancelled</string>
+    <string name="user_data_import_failed_description">Make sure the user data folders are at the root of the zip folder and contain a config file at config/config.ini and try again.</string>
     <string name="support_link">https://discord.gg/u77vRWY</string>
     <string name="website_link">https://yuzu-emu.org/</string>
     <string name="github_link">https://github.com/yuzu-emu</string>
@@ -227,6 +231,8 @@
     <string name="string_null">Null</string>
     <string name="string_import">Import</string>
     <string name="export">Export</string>
+    <string name="export_failed">Export failed</string>
+    <string name="import_failed">Import failed</string>
     <string name="cancelling">Cancelling</string>
 
     <!-- GPU driver installation -->