diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
index 816336820f..b63ece9a4f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
@@ -156,22 +156,22 @@ class AddonsFragment : Fragment() {
                 descriptionId = R.string.invalid_directory_description
             )
             if (isValid) {
-                IndeterminateProgressDialogFragment.newInstance(
+                ProgressDialogFragment.newInstance(
                     requireActivity(),
                     R.string.installing_game_content,
                     false
-                ) {
+                ) { progressCallback, _ ->
                     val parentDirectoryName = externalAddonDirectory.name
                     val internalAddonDirectory =
                         File(args.game.addonDir + parentDirectoryName)
                     try {
-                        externalAddonDirectory.copyFilesTo(internalAddonDirectory)
+                        externalAddonDirectory.copyFilesTo(internalAddonDirectory, progressCallback)
                     } catch (_: Exception) {
                         return@newInstance errorMessage
                     }
                     addonViewModel.refreshAddons()
                     return@newInstance getString(R.string.addon_installed_successfully)
-                }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
+                }.show(parentFragmentManager, ProgressDialogFragment.TAG)
             } else {
                 errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG)
             }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
index 9dabb9c416..6c758d80bb 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
@@ -173,11 +173,11 @@ class DriverManagerFragment : Fragment() {
                 return@registerForActivityResult
             }
 
-            IndeterminateProgressDialogFragment.newInstance(
+            ProgressDialogFragment.newInstance(
                 requireActivity(),
                 R.string.installing_driver,
                 false
-            ) {
+            ) { _, _ ->
                 val driverPath =
                     "${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(result)}"
                 val driverFile = File(driverPath)
@@ -213,6 +213,6 @@ class DriverManagerFragment : Fragment() {
                     }
                 }
                 return@newInstance Any()
-            }.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG)
+            }.show(childFragmentManager, ProgressDialogFragment.TAG)
         }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
index b04d1208fa..83a8454349 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
@@ -44,7 +44,6 @@ import org.yuzu.yuzu_emu.utils.FileUtil
 import org.yuzu.yuzu_emu.utils.GameIconUtils
 import org.yuzu.yuzu_emu.utils.GpuDriverHelper
 import org.yuzu.yuzu_emu.utils.MemoryUtil
-import java.io.BufferedInputStream
 import java.io.BufferedOutputStream
 import java.io.File
 
@@ -357,27 +356,17 @@ class GamePropertiesFragment : Fragment() {
                 return@registerForActivityResult
             }
 
-            val inputZip = requireContext().contentResolver.openInputStream(result)
             val savesFolder = File(args.game.saveDir)
             val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
             cacheSaveDir.mkdir()
 
-            if (inputZip == null) {
-                Toast.makeText(
-                    YuzuApplication.appContext,
-                    getString(R.string.fatal_error),
-                    Toast.LENGTH_LONG
-                ).show()
-                return@registerForActivityResult
-            }
-
-            IndeterminateProgressDialogFragment.newInstance(
+            ProgressDialogFragment.newInstance(
                 requireActivity(),
                 R.string.save_files_importing,
                 false
-            ) {
+            ) { _, _ ->
                 try {
-                    FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
+                    FileUtil.unzipToInternalStorage(result.toString(), cacheSaveDir)
                     val files = cacheSaveDir.listFiles()
                     var savesFolderFile: File? = null
                     if (files != null) {
@@ -422,7 +411,7 @@ class GamePropertiesFragment : Fragment() {
                         Toast.LENGTH_LONG
                     ).show()
                 }
-            }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
+            }.show(parentFragmentManager, ProgressDialogFragment.TAG)
         }
 
     /**
@@ -436,11 +425,11 @@ class GamePropertiesFragment : Fragment() {
             return@registerForActivityResult
         }
 
-        IndeterminateProgressDialogFragment.newInstance(
+        ProgressDialogFragment.newInstance(
             requireActivity(),
             R.string.save_files_exporting,
             false
-        ) {
+        ) { _, _ ->
             val saveLocation = args.game.saveDir
             val zipResult = FileUtil.zipFromInternalStorage(
                 File(saveLocation),
@@ -452,6 +441,6 @@ class GamePropertiesFragment : Fragment() {
                 TaskState.Completed -> getString(R.string.export_success)
                 TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
             }
-        }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
+        }.show(parentFragmentManager, ProgressDialogFragment.TAG)
     }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
index 5b4bf2c9ff..7df8e6bf46 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
@@ -34,7 +34,6 @@ import org.yuzu.yuzu_emu.model.TaskState
 import org.yuzu.yuzu_emu.ui.main.MainActivity
 import org.yuzu.yuzu_emu.utils.DirectoryInitialization
 import org.yuzu.yuzu_emu.utils.FileUtil
-import java.io.BufferedInputStream
 import java.io.BufferedOutputStream
 import java.io.File
 import java.math.BigInteger
@@ -195,26 +194,20 @@ class InstallableFragment : Fragment() {
                 return@registerForActivityResult
             }
 
-            val inputZip = requireContext().contentResolver.openInputStream(result)
             val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
             cacheSaveDir.mkdir()
 
-            if (inputZip == null) {
-                Toast.makeText(
-                    YuzuApplication.appContext,
-                    getString(R.string.fatal_error),
-                    Toast.LENGTH_LONG
-                ).show()
-                return@registerForActivityResult
-            }
-
-            IndeterminateProgressDialogFragment.newInstance(
+            ProgressDialogFragment.newInstance(
                 requireActivity(),
                 R.string.save_files_importing,
                 false
-            ) {
+            ) { progressCallback, _ ->
                 try {
-                    FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
+                    FileUtil.unzipToInternalStorage(
+                        result.toString(),
+                        cacheSaveDir,
+                        progressCallback
+                    )
                     val files = cacheSaveDir.listFiles()
                     var successfulImports = 0
                     var failedImports = 0
@@ -287,7 +280,7 @@ class InstallableFragment : Fragment() {
                         Toast.LENGTH_LONG
                     ).show()
                 }
-            }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
+            }.show(parentFragmentManager, ProgressDialogFragment.TAG)
         }
 
     private val exportSaves = registerForActivityResult(
@@ -297,11 +290,11 @@ class InstallableFragment : Fragment() {
             return@registerForActivityResult
         }
 
-        IndeterminateProgressDialogFragment.newInstance(
+        ProgressDialogFragment.newInstance(
             requireActivity(),
             R.string.save_files_exporting,
             false
-        ) {
+        ) { _, _ ->
             val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
             cacheSaveDir.mkdir()
 
@@ -338,6 +331,6 @@ class InstallableFragment : Fragment() {
                 TaskState.Completed -> getString(R.string.export_success)
                 TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
             }
-        }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
+        }.show(parentFragmentManager, ProgressDialogFragment.TAG)
     }
 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt
similarity index 73%
rename from src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
rename to src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt
index 8847e5531a..d201cb80c6 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/ProgressDialogFragment.kt
@@ -23,11 +23,13 @@ import org.yuzu.yuzu_emu.R
 import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
 import org.yuzu.yuzu_emu.model.TaskViewModel
 
-class IndeterminateProgressDialogFragment : DialogFragment() {
+class ProgressDialogFragment : DialogFragment() {
     private val taskViewModel: TaskViewModel by activityViewModels()
 
     private lateinit var binding: DialogProgressBarBinding
 
+    private val PROGRESS_BAR_RESOLUTION = 1000
+
     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
         val titleId = requireArguments().getInt(TITLE)
         val cancellable = requireArguments().getBoolean(CANCELLABLE)
@@ -61,6 +63,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
+        binding.message.isSelected = true
         viewLifecycleOwner.lifecycleScope.apply {
             launch {
                 repeatOnLifecycle(Lifecycle.State.CREATED) {
@@ -97,6 +100,35 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
                     }
                 }
             }
+            launch {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    taskViewModel.progress.collect {
+                        if (it != 0.0) {
+                            binding.progressBar.apply {
+                                isIndeterminate = false
+                                progress = (
+                                    (it / taskViewModel.maxProgress.value) *
+                                        PROGRESS_BAR_RESOLUTION
+                                    ).toInt()
+                                min = 0
+                                max = PROGRESS_BAR_RESOLUTION
+                            }
+                        }
+                    }
+                }
+            }
+            launch {
+                repeatOnLifecycle(Lifecycle.State.CREATED) {
+                    taskViewModel.message.collect {
+                        if (it.isEmpty()) {
+                            binding.message.visibility = View.GONE
+                        } else {
+                            binding.message.visibility = View.VISIBLE
+                            binding.message.text = it
+                        }
+                    }
+                }
+            }
         }
     }
 
@@ -108,6 +140,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
         val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE)
         negativeButton.setOnClickListener {
             alertDialog.setTitle(getString(R.string.cancelling))
+            binding.progressBar.isIndeterminate = true
             taskViewModel.setCancelled(true)
         }
     }
@@ -122,9 +155,12 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
             activity: FragmentActivity,
             titleId: Int,
             cancellable: Boolean = false,
-            task: suspend () -> Any
-        ): IndeterminateProgressDialogFragment {
-            val dialog = IndeterminateProgressDialogFragment()
+            task: suspend (
+                progressCallback: (max: Long, progress: Long) -> Boolean,
+                messageCallback: (message: String) -> Unit
+            ) -> Any
+        ): ProgressDialogFragment {
+            val dialog = ProgressDialogFragment()
             val args = Bundle()
             ViewModelProvider(activity)[TaskViewModel::class.java].task = task
             args.putInt(TITLE, titleId)
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 e59c957335..4361eb9726 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
@@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.launch
 
 class TaskViewModel : ViewModel() {
@@ -23,13 +24,28 @@ class TaskViewModel : ViewModel() {
     val cancelled: StateFlow<Boolean> get() = _cancelled
     private val _cancelled = MutableStateFlow(false)
 
-    lateinit var task: suspend () -> Any
+    private val _progress = MutableStateFlow(0.0)
+    val progress = _progress.asStateFlow()
+
+    private val _maxProgress = MutableStateFlow(0.0)
+    val maxProgress = _maxProgress.asStateFlow()
+
+    private val _message = MutableStateFlow("")
+    val message = _message.asStateFlow()
+
+    lateinit var task: suspend (
+        progressCallback: (max: Long, progress: Long) -> Boolean,
+        messageCallback: (message: String) -> Unit
+    ) -> Any
 
     fun clear() {
         _result.value = Any()
         _isComplete.value = false
         _isRunning.value = false
         _cancelled.value = false
+        _progress.value = 0.0
+        _maxProgress.value = 0.0
+        _message.value = ""
     }
 
     fun setCancelled(value: Boolean) {
@@ -43,7 +59,16 @@ class TaskViewModel : ViewModel() {
         _isRunning.value = true
 
         viewModelScope.launch(Dispatchers.IO) {
-            val res = task()
+            val res = task(
+                { max, progress ->
+                    _maxProgress.value = max.toDouble()
+                    _progress.value = progress.toDouble()
+                    return@task cancelled.value
+                },
+                { message ->
+                    _message.value = message
+                }
+            )
             _result.value = res
             _isComplete.value = true
             _isRunning.value = false
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 644289e250..c2cc29961e 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
@@ -38,12 +38,13 @@ import org.yuzu.yuzu_emu.activities.EmulationActivity
 import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
 import org.yuzu.yuzu_emu.features.settings.model.Settings
 import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
-import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
+import org.yuzu.yuzu_emu.fragments.ProgressDialogFragment
 import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
 import org.yuzu.yuzu_emu.model.AddonViewModel
 import org.yuzu.yuzu_emu.model.DriverViewModel
 import org.yuzu.yuzu_emu.model.GamesViewModel
 import org.yuzu.yuzu_emu.model.HomeViewModel
+import org.yuzu.yuzu_emu.model.InstallResult
 import org.yuzu.yuzu_emu.model.TaskState
 import org.yuzu.yuzu_emu.model.TaskViewModel
 import org.yuzu.yuzu_emu.utils.*
@@ -369,26 +370,23 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                 return@registerForActivityResult
             }
 
-            val inputZip = contentResolver.openInputStream(result)
-            if (inputZip == null) {
-                Toast.makeText(
-                    applicationContext,
-                    getString(R.string.fatal_error),
-                    Toast.LENGTH_LONG
-                ).show()
-                return@registerForActivityResult
-            }
-
             val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") }
 
             val firmwarePath =
                 File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/")
             val cacheFirmwareDir = File("${cacheDir.path}/registered/")
 
-            val task: () -> Any = {
+            ProgressDialogFragment.newInstance(
+                this,
+                R.string.firmware_installing
+            ) { progressCallback, _ ->
                 var messageToShow: Any
                 try {
-                    FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheFirmwareDir)
+                    FileUtil.unzipToInternalStorage(
+                        result.toString(),
+                        cacheFirmwareDir,
+                        progressCallback
+                    )
                     val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
                     val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
                     messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
@@ -404,18 +402,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                         getString(R.string.save_file_imported_success)
                     }
                 } catch (e: Exception) {
+                    Log.error("[MainActivity] Firmware install failed - ${e.message}")
                     messageToShow = getString(R.string.fatal_error)
                 } finally {
                     cacheFirmwareDir.deleteRecursively()
                 }
                 messageToShow
-            }
-
-            IndeterminateProgressDialogFragment.newInstance(
-                this,
-                R.string.firmware_installing,
-                task = task
-            ).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
+            }.show(supportFragmentManager, ProgressDialogFragment.TAG)
         }
 
     val getAmiiboKey =
@@ -474,11 +467,11 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
             return@registerForActivityResult
         }
 
-        IndeterminateProgressDialogFragment.newInstance(
+        ProgressDialogFragment.newInstance(
             this@MainActivity,
             R.string.verifying_content,
             false
-        ) {
+        ) { _, _ ->
             var updatesMatchProgram = true
             for (document in documents) {
                 val valid = NativeLibrary.doesUpdateMatchProgram(
@@ -501,44 +494,42 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                     positiveAction = { homeViewModel.setContentToInstall(documents) }
                 )
             }
-        }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
+        }.show(supportFragmentManager, ProgressDialogFragment.TAG)
     }
 
     private fun installContent(documents: List<Uri>) {
-        IndeterminateProgressDialogFragment.newInstance(
+        ProgressDialogFragment.newInstance(
             this@MainActivity,
             R.string.installing_game_content
-        ) {
+        ) { progressCallback, messageCallback ->
             var installSuccess = 0
             var installOverwrite = 0
             var errorBaseGame = 0
-            var errorExtension = 0
-            var errorOther = 0
+            var error = 0
             documents.forEach {
+                messageCallback.invoke(FileUtil.getFilename(it))
                 when (
-                    NativeLibrary.installFileToNand(
-                        it.toString(),
-                        FileUtil.getExtension(it)
+                    InstallResult.from(
+                        NativeLibrary.installFileToNand(
+                            it.toString(),
+                            progressCallback
+                        )
                     )
                 ) {
-                    NativeLibrary.InstallFileToNandResult.Success -> {
+                    InstallResult.Success -> {
                         installSuccess += 1
                     }
 
-                    NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> {
+                    InstallResult.Overwrite -> {
                         installOverwrite += 1
                     }
 
-                    NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> {
+                    InstallResult.BaseInstallAttempted -> {
                         errorBaseGame += 1
                     }
 
-                    NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> {
-                        errorExtension += 1
-                    }
-
-                    else -> {
-                        errorOther += 1
+                    InstallResult.Failure -> {
+                        error += 1
                     }
                 }
             }
@@ -565,7 +556,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                 )
                 installResult.append(separator)
             }
-            val errorTotal: Int = errorBaseGame + errorExtension + errorOther
+            val errorTotal: Int = errorBaseGame + error
             if (errorTotal > 0) {
                 installResult.append(separator)
                 installResult.append(
@@ -582,14 +573,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                     )
                     installResult.append(separator)
                 }
-                if (errorExtension > 0) {
-                    installResult.append(separator)
-                    installResult.append(
-                        getString(R.string.install_game_content_failure_file_extension)
-                    )
-                    installResult.append(separator)
-                }
-                if (errorOther > 0) {
+                if (error > 0) {
                     installResult.append(
                         getString(R.string.install_game_content_failure_description)
                     )
@@ -608,7 +592,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                     descriptionString = installResult.toString().trim()
                 )
             }
-        }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
+        }.show(supportFragmentManager, ProgressDialogFragment.TAG)
     }
 
     val exportUserData = registerForActivityResult(
@@ -618,16 +602,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
             return@registerForActivityResult
         }
 
-        IndeterminateProgressDialogFragment.newInstance(
+        ProgressDialogFragment.newInstance(
             this,
             R.string.exporting_user_data,
             true
-        ) {
+        ) { progressCallback, _ ->
             val zipResult = FileUtil.zipFromInternalStorage(
                 File(DirectoryInitialization.userDirectory!!),
                 DirectoryInitialization.userDirectory!!,
                 BufferedOutputStream(contentResolver.openOutputStream(result)),
-                taskViewModel.cancelled,
+                progressCallback,
                 compression = false
             )
             return@newInstance when (zipResult) {
@@ -635,7 +619,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                 TaskState.Failed -> R.string.export_failed
                 TaskState.Cancelled -> R.string.user_data_export_cancelled
             }
-        }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
+        }.show(supportFragmentManager, ProgressDialogFragment.TAG)
     }
 
     val importUserData =
@@ -644,10 +628,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                 return@registerForActivityResult
             }
 
-            IndeterminateProgressDialogFragment.newInstance(
+            ProgressDialogFragment.newInstance(
                 this,
                 R.string.importing_user_data
-            ) {
+            ) { progressCallback, _ ->
                 val checkStream =
                     ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result)))
                 var isYuzuBackup = false
@@ -676,8 +660,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                 // Copy archive to internal storage
                 try {
                     FileUtil.unzipToInternalStorage(
-                        BufferedInputStream(contentResolver.openInputStream(result)),
-                        File(DirectoryInitialization.userDirectory!!)
+                        result.toString(),
+                        File(DirectoryInitialization.userDirectory!!),
+                        progressCallback
                     )
                 } catch (e: Exception) {
                     return@newInstance MessageDialogFragment.newInstance(
@@ -694,6 +679,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                 driverViewModel.reloadDriverData()
 
                 return@newInstance getString(R.string.user_data_import_success)
-            }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
+            }.show(supportFragmentManager, ProgressDialogFragment.TAG)
         }
 }
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 b54a19c652..fc2339f5a2 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
@@ -7,7 +7,6 @@ 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.IOException
@@ -19,6 +18,7 @@ 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.io.OutputStream
 import java.lang.NullPointerException
 import java.nio.charset.StandardCharsets
 import java.util.zip.Deflater
@@ -283,12 +283,34 @@ object FileUtil {
 
     /**
      * Extracts the given zip file into the given directory.
+     * @param path String representation of a [Uri] or a typical path delimited by '/'
+     * @param destDir Location to unzip the contents of [path] into
+     * @param progressCallback Lambda that is called with the total number of files and the current
+     * progress through the process. Stops execution as soon as possible if this returns true.
      */
     @Throws(SecurityException::class)
-    fun unzipToInternalStorage(zipStream: BufferedInputStream, destDir: File) {
-        ZipInputStream(zipStream).use { zis ->
+    fun unzipToInternalStorage(
+        path: String,
+        destDir: File,
+        progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }
+    ) {
+        var totalEntries = 0L
+        ZipInputStream(getInputStream(path)).use { zis ->
+            var tempEntry = zis.nextEntry
+            while (tempEntry != null) {
+                tempEntry = zis.nextEntry
+                totalEntries++
+            }
+        }
+
+        var progress = 0L
+        ZipInputStream(getInputStream(path)).use { zis ->
             var entry: ZipEntry? = zis.nextEntry
             while (entry != null) {
+                if (progressCallback.invoke(totalEntries, progress)) {
+                    return@use
+                }
+
                 val newFile = File(destDir, entry.name)
                 val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile
 
@@ -304,6 +326,7 @@ object FileUtil {
                     newFile.outputStream().use { fos -> zis.copyTo(fos) }
                 }
                 entry = zis.nextEntry
+                progress++
             }
         }
     }
@@ -313,14 +336,15 @@ object FileUtil {
      * @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
-     * @param cancelled [StateFlow] that reports whether this process has been cancelled
+     * @param progressCallback Lambda that is called with the total number of files and the current
+     * progress through the process. Stops execution as soon as possible if this returns true.
      * @param compression Disables compression if true
      */
     fun zipFromInternalStorage(
         inputFile: File,
         rootDir: String,
         outputStream: BufferedOutputStream,
-        cancelled: StateFlow<Boolean>? = null,
+        progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false },
         compression: Boolean = true
     ): TaskState {
         try {
@@ -330,8 +354,10 @@ object FileUtil {
                     zos.setLevel(Deflater.NO_COMPRESSION)
                 }
 
+                var count = 0L
+                val totalFiles = inputFile.walkTopDown().count().toLong()
                 inputFile.walkTopDown().forEach { file ->
-                    if (cancelled?.value == true) {
+                    if (progressCallback.invoke(totalFiles, count)) {
                         return TaskState.Cancelled
                     }
 
@@ -343,6 +369,7 @@ object FileUtil {
                         if (file.isFile) {
                             file.inputStream().use { fis -> fis.copyTo(zos) }
                         }
+                        count++
                     }
                 }
             }
@@ -356,9 +383,14 @@ object FileUtil {
     /**
      * Helper function that copies the contents of a DocumentFile folder into a [File]
      * @param file [File] representation of the folder to copy into
+     * @param progressCallback Lambda that is called with the total number of files and the current
+     * progress through the process. Stops execution as soon as possible if this returns true.
      * @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa
      */
-    fun DocumentFile.copyFilesTo(file: File) {
+    fun DocumentFile.copyFilesTo(
+        file: File,
+        progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }
+    ) {
         file.mkdirs()
         if (!this.isDirectory || !file.isDirectory) {
             throw IllegalStateException(
@@ -366,7 +398,13 @@ object FileUtil {
             )
         }
 
+        var count = 0L
+        val totalFiles = this.listFiles().size.toLong()
         this.listFiles().forEach {
+            if (progressCallback.invoke(totalFiles, count)) {
+                return
+            }
+
             val newFile = File(file, it.name!!)
             if (it.isDirectory) {
                 newFile.mkdirs()
@@ -381,6 +419,7 @@ object FileUtil {
                     newFile.outputStream().use { os -> bos.copyTo(os) }
                 }
             }
+            count++
         }
     }
 
@@ -427,6 +466,18 @@ object FileUtil {
         }
     }
 
+    fun getInputStream(path: String) = if (path.contains("content://")) {
+        Uri.parse(path).inputStream()
+    } else {
+        File(path).inputStream()
+    }
+
+    fun getOutputStream(path: String) = if (path.contains("content://")) {
+        Uri.parse(path).outputStream()
+    } else {
+        File(path).outputStream()
+    }
+
     @Throws(IOException::class)
     fun getStringFromFile(file: File): String =
         String(file.readBytes(), StandardCharsets.UTF_8)
@@ -434,4 +485,19 @@ object FileUtil {
     @Throws(IOException::class)
     fun getStringFromInputStream(stream: InputStream): String =
         String(stream.readBytes(), StandardCharsets.UTF_8)
+
+    fun DocumentFile.inputStream(): InputStream =
+        YuzuApplication.appContext.contentResolver.openInputStream(uri)!!
+
+    fun DocumentFile.outputStream(): OutputStream =
+        YuzuApplication.appContext.contentResolver.openOutputStream(uri)!!
+
+    fun Uri.inputStream(): InputStream =
+        YuzuApplication.appContext.contentResolver.openInputStream(this)!!
+
+    fun Uri.outputStream(): OutputStream =
+        YuzuApplication.appContext.contentResolver.openOutputStream(this)!!
+
+    fun Uri.asDocumentFile(): DocumentFile? =
+        DocumentFile.fromSingleUri(YuzuApplication.appContext, this)
 }
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 a8f9dcc349..81212cbee3 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
@@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.utils
 
 import android.net.Uri
 import android.os.Build
-import java.io.BufferedInputStream
 import java.io.File
 import java.io.IOException
 import org.yuzu.yuzu_emu.NativeLibrary
@@ -123,7 +122,7 @@ object GpuDriverHelper {
         // Unzip the driver.
         try {
             FileUtil.unzipToInternalStorage(
-                BufferedInputStream(copiedFile.inputStream()),
+                copiedFile.path,
                 File(driverInstallationPath!!)
             )
         } catch (e: SecurityException) {
@@ -156,7 +155,7 @@ object GpuDriverHelper {
         // Unzip the driver to the private installation directory
         try {
             FileUtil.unzipToInternalStorage(
-                BufferedInputStream(driver.inputStream()),
+                driver.path,
                 File(driverInstallationPath!!)
             )
         } catch (e: SecurityException) {
diff --git a/src/android/app/src/main/res/layout/dialog_progress_bar.xml b/src/android/app/src/main/res/layout/dialog_progress_bar.xml
index 0209ea0826..e61aa52948 100644
--- a/src/android/app/src/main/res/layout/dialog_progress_bar.xml
+++ b/src/android/app/src/main/res/layout/dialog_progress_bar.xml
@@ -1,8 +1,30 @@
 <?xml version="1.0" encoding="utf-8"?>
-<com.google.android.material.progressindicator.LinearProgressIndicator xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:id="@+id/progress_bar"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    android:padding="24dp"
-    app:trackCornerRadius="4dp" />
+    android:orientation="vertical">
+
+    <com.google.android.material.textview.MaterialTextView
+        android:id="@+id/message"
+        style="@style/TextAppearance.Material3.BodyMedium"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="24dp"
+        android:layout_marginTop="12dp"
+        android:layout_marginBottom="6dp"
+        android:ellipsize="marquee"
+        android:marqueeRepeatLimit="marquee_forever"
+        android:requiresFadingEdge="horizontal"
+        android:singleLine="true"
+        android:textAlignment="viewStart"
+        android:visibility="gone" />
+
+    <com.google.android.material.progressindicator.LinearProgressIndicator
+        android:id="@+id/progress_bar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:padding="24dp"
+        app:trackCornerRadius="4dp" />
+
+</LinearLayout>