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 67bcf8491a..cc4b0157b8 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
@@ -19,10 +19,10 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
-import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
+import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
@@ -40,6 +40,7 @@ import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.model.HomeSetting
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.ui.main.MainActivity
+import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
class HomeSettingsFragment : Fragment() {
@@ -108,6 +109,11 @@ class HomeSettingsFragment : Fragment() {
R.string.install_prod_keys_description,
R.drawable.ic_unlock
) { mainActivity.getProdKey.launch(arrayOf("*/*")) },
+ HomeSetting(
+ R.string.install_firmware,
+ R.string.install_firmware_description,
+ R.drawable.ic_firmware
+ ) { mainActivity.getFirmware.launch(arrayOf("application/zip")) },
HomeSetting(
R.string.about,
R.string.about_description,
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
new file mode 100644
index 0000000000..edf7b8a3c2
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
@@ -0,0 +1,36 @@
+package org.yuzu.yuzu_emu.fragments
+
+import android.app.Dialog
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
+
+class IndeterminateProgressDialogFragment : DialogFragment() {
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val titleId = requireArguments().getInt(TITLE)
+
+ val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
+ progressBinding.progressBar.isIndeterminate = true
+ return MaterialAlertDialogBuilder(requireContext())
+ .setTitle(titleId)
+ .setView(progressBinding.root)
+ .show()
+ }
+
+ companion object {
+ const val TAG = "IndeterminateProgressDialogFragment"
+
+ private const val TITLE = "Title"
+
+ fun newInstance(
+ titleId: Int,
+ ): IndeterminateProgressDialogFragment {
+ val dialog = IndeterminateProgressDialogFragment()
+ val args = Bundle()
+ args.putInt(TITLE, titleId)
+ dialog.arguments = args
+ return dialog
+ }
+ }
+}
\ No newline at end of file
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 f8bca11bb6..bb83110230 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
@@ -26,6 +26,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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -37,10 +38,13 @@ import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
+import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.utils.*
+import java.io.File
+import java.io.FilenameFilter
import java.io.IOException
class MainActivity : AppCompatActivity(), ThemeProvider {
@@ -315,6 +319,67 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
}
+ val getFirmware =
+ registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
+ if (result == null)
+ 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 installingFirmwareDialog = IndeterminateProgressDialogFragment.newInstance(
+ R.string.firmware_installing
+ )
+ installingFirmwareDialog.isCancelable = false
+ installingFirmwareDialog.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
+
+ lifecycleScope.launch(Dispatchers.IO) {
+ try {
+ FileUtil.unzip(inputZip, cacheFirmwareDir)
+ val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
+ val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
+ if (unfilteredNumOfFiles != filteredNumOfFiles) {
+ withContext(Dispatchers.Main) {
+ installingFirmwareDialog.dismiss()
+ MessageDialogFragment.newInstance(
+ R.string.firmware_installed_failure,
+ R.string.firmware_installed_failure_description
+ ).show(supportFragmentManager, MessageDialogFragment.TAG)
+ }
+ } else {
+ firmwarePath.deleteRecursively()
+ cacheFirmwareDir.copyRecursively(firmwarePath, true)
+ withContext(Dispatchers.Main) {
+ installingFirmwareDialog.dismiss()
+ Toast.makeText(
+ applicationContext,
+ getString(R.string.save_file_imported_success),
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+ } catch (e: Exception) {
+ Toast.makeText(applicationContext, getString(R.string.fatal_error), Toast.LENGTH_LONG)
+ .show()
+ } finally {
+ cacheFirmwareDir.deleteRecursively()
+ }
+ }
+ }
+
val getAmiiboKey =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result == null)
diff --git a/src/android/app/src/main/res/drawable/ic_firmware.xml b/src/android/app/src/main/res/drawable/ic_firmware.xml
new file mode 100644
index 0000000000..61f3485e4e
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_firmware.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index fc24e27f5c..4b3bfcf9db 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -96,6 +96,12 @@
The first subfolder name must be the title ID of the game.
Import
Export
+ Install firmware
+ Required to boot some games
+ Installing firmware
+ Firmware installed successfully
+ Firmware installation failed.
+ Check that the ZIP contains a firmware.
Gaia isn\'t real