From 5435f0be5e4d81da5140cea79904252403f108c2 Mon Sep 17 00:00:00 2001
From: PabloG02 <tioo23000@gmail.com>
Date: Sat, 3 Jun 2023 14:14:05 +0200
Subject: [PATCH] android: add option to install firmware

---
 .../fragments/HomeSettingsFragment.kt         |  8 ++-
 .../IndeterminateProgressDialogFragment.kt    | 36 ++++++++++
 .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 65 +++++++++++++++++++
 .../app/src/main/res/drawable/ic_firmware.xml | 10 +++
 .../app/src/main/res/values/strings.xml       |  6 ++
 5 files changed, 124 insertions(+), 1 deletion(-)
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
 create mode 100644 src/android/app/src/main/res/drawable/ic_firmware.xml

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 67bcf8491..cc4b0157b 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 000000000..edf7b8a3c
--- /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 f8bca11bb..bb8311023 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 000000000..61f3485e4
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_firmware.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M160,840Q127,840 103.5,816.5Q80,793 80,760L80,200Q80,167 103.5,143.5Q127,120 160,120L720,120Q753,120 776.5,143.5Q800,167 800,200L800,280L840,280Q857,280 868.5,291.5Q880,303 880,320Q880,337 868.5,348.5Q857,360 840,360L800,360L800,440L840,440Q857,440 868.5,451.5Q880,463 880,480Q880,497 868.5,508.5Q857,520 840,520L800,520L800,600L840,600Q857,600 868.5,611.5Q880,623 880,640Q880,657 868.5,668.5Q857,680 840,680L800,680L800,760Q800,793 776.5,816.5Q753,840 720,840L160,840ZM160,760L720,760Q720,760 720,760Q720,760 720,760L720,200Q720,200 720,200Q720,200 720,200L160,200Q160,200 160,200Q160,200 160,200L160,760Q160,760 160,760Q160,760 160,760ZM280,680L400,680Q417,680 428.5,668.5Q440,657 440,640L440,560Q440,543 428.5,531.5Q417,520 400,520L280,520Q263,520 251.5,531.5Q240,543 240,560L240,640Q240,657 251.5,668.5Q263,680 280,680ZM520,400L600,400Q617,400 628.5,388.5Q640,377 640,360L640,320Q640,303 628.5,291.5Q617,280 600,280L520,280Q503,280 491.5,291.5Q480,303 480,320L480,360Q480,377 491.5,388.5Q503,400 520,400ZM280,480L400,480Q417,480 428.5,468.5Q440,457 440,440L440,320Q440,303 428.5,291.5Q417,280 400,280L280,280Q263,280 251.5,291.5Q240,303 240,320L240,440Q240,457 251.5,468.5Q263,480 280,480ZM520,680L600,680Q617,680 628.5,668.5Q640,657 640,640L640,480Q640,463 628.5,451.5Q617,440 600,440L520,440Q503,440 491.5,451.5Q480,463 480,480L480,640Q480,657 491.5,668.5Q503,680 520,680ZM160,200L160,200Q160,200 160,200Q160,200 160,200L160,760Q160,760 160,760Q160,760 160,760L160,760Q160,760 160,760Q160,760 160,760L160,200Q160,200 160,200Q160,200 160,200Z"/>
+</vector>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index fc24e27f5..4b3bfcf9d 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 @@
     <string name="save_file_invalid_zip_structure_description">The first subfolder name must be the title ID of the game.</string>
     <string name="import_saves">Import</string>
     <string name="export_saves">Export</string>
+    <string name="install_firmware">Install firmware</string>
+    <string name="install_firmware_description">Required to boot some games</string>
+    <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">Check that the ZIP contains a firmware.</string>
 
     <!-- About screen strings -->
     <string name="gaia_is_not_real">Gaia isn\'t real</string>