From 19674ec78d0c8b831083de3e7cf2fa29aacf9d4d Mon Sep 17 00:00:00 2001
From: PabloG02 <tioo23000@gmail.com>
Date: Sat, 3 Jun 2023 14:13:20 +0200
Subject: [PATCH 1/7] android: move unzip function to FileUtil and use
 SecurityException

---
 .../fragments/ImportExportSavesFragment.kt    | 34 ++-----------------
 .../java/org/yuzu/yuzu_emu/utils/FileUtil.kt  | 32 +++++++++++++++++
 2 files changed, 34 insertions(+), 32 deletions(-)

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
index 5f107b37d..36e63bb9e 100644
--- 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
@@ -23,17 +23,14 @@ 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 java.io.BufferedInputStream
+import org.yuzu.yuzu_emu.utils.FileUtil
 import java.io.BufferedOutputStream
 import java.io.File
 import java.io.FileOutputStream
 import java.io.FilenameFilter
-import java.io.IOException
-import java.io.InputStream
 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 ImportExportSavesFragment : DialogFragment() {
@@ -124,33 +121,6 @@ class ImportExportSavesFragment : DialogFragment() {
         return true
     }
 
-    /**
-     * Extracts the save files located in the given zip file and copies them to the saves folder.
-     * @exception IOException if the file was being created outside of the target directory
-     */
-    private fun unzip(zipStream: InputStream, destDir: File): Boolean {
-        val zis = ZipInputStream(BufferedInputStream(zipStream))
-        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)) {
-                zis.close()
-                throw IOException("Entry is outside of the target dir: " + entryFile.name)
-            }
-            if (entry.isDirectory) {
-                entryFile.mkdirs()
-            } else {
-                entryFile.parentFile?.mkdirs()
-                entryFile.createNewFile()
-                entryFile.outputStream().use { fos -> zis.copyTo(fos) }
-            }
-            entry = zis.nextEntry
-        }
-        zis.close()
-        return true
-    }
-
     /**
      * Exports the save file located in the given folder path by creating a zip file and sharing it via intent.
      */
@@ -204,7 +174,7 @@ class ImportExportSavesFragment : DialogFragment() {
 
         try {
             CoroutineScope(Dispatchers.IO).launch {
-                unzip(inputZip, cacheSaveDir)
+                FileUtil.unzip(inputZip, cacheSaveDir)
                 cacheSaveDir.list(filterTitleId)?.forEach { savePath ->
                     File(savesFolder, savePath).deleteRecursively()
                     File(cacheSaveDir, savePath).copyRecursively(File(savesFolder, savePath), true)
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 0a7b323b1..593dad8d3 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
@@ -9,10 +9,14 @@ import android.net.Uri
 import android.provider.DocumentsContract
 import androidx.documentfile.provider.DocumentFile
 import org.yuzu.yuzu_emu.model.MinimalDocumentFile
+import java.io.BufferedInputStream
+import java.io.File
 import java.io.FileOutputStream
 import java.io.IOException
 import java.io.InputStream
 import java.net.URLDecoder
+import java.util.zip.ZipEntry
+import java.util.zip.ZipInputStream
 
 object FileUtil {
     const val PATH_TREE = "tree"
@@ -276,6 +280,34 @@ object FileUtil {
         return false
     }
 
+    /**
+     * 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 ->
+            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)
+                }
+                if (entry.isDirectory) {
+                    entryFile.mkdirs()
+                } else {
+                    entryFile.parentFile?.mkdirs()
+                    entryFile.createNewFile()
+                    entryFile.outputStream().use { fos -> zis.copyTo(fos) }
+                }
+                entry = zis.nextEntry
+            }
+        }
+
+        return true
+    }
+
     fun isRootTreeUri(uri: Uri): Boolean {
         val paths = uri.pathSegments
         return paths.size == 2 && PATH_TREE == paths[0]

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 2/7] 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>

From 8713c442e9e5408f8d8a4e937e2c8e5c9c335430 Mon Sep 17 00:00:00 2001
From: PabloG02 <tioo23000@gmail.com>
Date: Sat, 3 Jun 2023 14:15:15 +0200
Subject: [PATCH 3/7] android: add option to share log

---
 .../fragments/HomeSettingsFragment.kt         | 23 +++++++++++++++++++
 .../app/src/main/res/drawable/ic_log.xml      | 10 ++++++++
 .../app/src/main/res/values/strings.xml       |  3 +++
 3 files changed, 36 insertions(+)
 create mode 100644 src/android/app/src/main/res/drawable/ic_log.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 cc4b0157b..0bdbabe79 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
@@ -114,6 +114,11 @@ class HomeSettingsFragment : Fragment() {
                 R.string.install_firmware_description,
                 R.drawable.ic_firmware
             ) { mainActivity.getFirmware.launch(arrayOf("application/zip")) },
+            HomeSetting(
+                R.string.share_log,
+                R.string.share_log_description,
+                R.drawable.ic_log
+            ) { shareLog() },
             HomeSetting(
                 R.string.about,
                 R.string.about_description,
@@ -268,6 +273,24 @@ class HomeSettingsFragment : Fragment() {
             .show()
     }
 
+    private fun shareLog() {
+        val file = DocumentFile.fromSingleUri(
+            mainActivity, DocumentsContract.buildDocumentUri(
+                DocumentProvider.AUTHORITY,
+                "${DocumentProvider.ROOT_ID}/log/yuzu_log.txt"
+            )
+        )!!
+        if (file.exists()) {
+            val intent = Intent(Intent.ACTION_SEND)
+                .setDataAndType(file.uri, FileUtil.TEXT_PLAIN)
+                .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+                .putExtra(Intent.EXTRA_STREAM, file.uri)
+            startActivity(Intent.createChooser(intent, "Share log"))
+        } else {
+            Toast.makeText(requireContext(), getText(R.string.share_log_missing), Toast.LENGTH_SHORT).show()
+        }
+    }
+
     private fun setInsets() =
         ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view: View, windowInsets: WindowInsetsCompat ->
             val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
diff --git a/src/android/app/src/main/res/drawable/ic_log.xml b/src/android/app/src/main/res/drawable/ic_log.xml
new file mode 100644
index 000000000..f55b9ad85
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_log.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="M360,720L600,720Q617,720 628.5,708.5Q640,697 640,680Q640,663 628.5,651.5Q617,640 600,640L360,640Q343,640 331.5,651.5Q320,663 320,680Q320,697 331.5,708.5Q343,720 360,720ZM360,560L600,560Q617,560 628.5,548.5Q640,537 640,520Q640,503 628.5,491.5Q617,480 600,480L360,480Q343,480 331.5,491.5Q320,503 320,520Q320,537 331.5,548.5Q343,560 360,560ZM240,880Q207,880 183.5,856.5Q160,833 160,800L160,160Q160,127 183.5,103.5Q207,80 240,80L527,80Q543,80 557.5,86Q572,92 583,103L777,297Q788,308 794,322.5Q800,337 800,353L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM520,320L520,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800L720,800Q720,800 720,800Q720,800 720,800L720,360L560,360Q543,360 531.5,348.5Q520,337 520,320ZM240,160L240,160L240,320Q240,337 240,348.5Q240,360 240,360L240,360L240,160L240,320Q240,337 240,348.5Q240,360 240,360L240,360L240,800Q240,800 240,800Q240,800 240,800L240,800Q240,800 240,800Q240,800 240,800L240,160Q240,160 240,160Q240,160 240,160Z"/>
+</vector>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 4b3bfcf9d..5d42be5e6 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -102,6 +102,9 @@
     <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>
+    <string name="share_log">Share log</string>
+    <string name="share_log_description">Share the log file</string>
+    <string name="share_log_missing">No log file found</string>
 
     <!-- About screen strings -->
     <string name="gaia_is_not_real">Gaia isn\'t real</string>

From 72597b8ffea24de329366b2beda6b1cad0620fa0 Mon Sep 17 00:00:00 2001
From: PabloG02 <tioo23000@gmail.com>
Date: Sat, 3 Jun 2023 14:16:07 +0200
Subject: [PATCH 4/7] android: update strings

---
 src/android/app/src/main/res/values/strings.xml | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 5d42be5e6..1646b90eb 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -97,13 +97,13 @@
     <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="install_firmware_description">Firmware must be in a ZIP archive and is needed 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>
-    <string name="share_log">Share log</string>
-    <string name="share_log_description">Share the log file</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="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>
 
     <!-- About screen strings -->

From 3733187c147149c995df97d7ac72ab5e4aafd137 Mon Sep 17 00:00:00 2001
From: PabloG02 <tioo23000@gmail.com>
Date: Sun, 4 Jun 2023 02:24:14 +0200
Subject: [PATCH 5/7] Attempt to move the unzip coroutine to a ViewModel

---
 .../IndeterminateProgressDialogFragment.kt    | 40 +++++++++++++++++-
 .../org/yuzu/yuzu_emu/model/TaskViewModel.kt  | 42 +++++++++++++++++++
 .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 39 +++++++----------
 3 files changed, 94 insertions(+), 27 deletions(-)
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt

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 edf7b8a3c..10a897392 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
@@ -2,33 +2,69 @@ package org.yuzu.yuzu_emu.fragments
 
 import android.app.Dialog
 import android.os.Bundle
+import android.widget.Toast
 import androidx.fragment.app.DialogFragment
+import androidx.lifecycle.ViewModelProvider
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
+import org.yuzu.yuzu_emu.model.TaskViewModel
+import java.io.Serializable
+
 
 class IndeterminateProgressDialogFragment : DialogFragment() {
+    private lateinit var taskViewModel: TaskViewModel
+
     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        taskViewModel = ViewModelProvider(requireActivity())[TaskViewModel::class.java]
+
         val titleId = requireArguments().getInt(TITLE)
 
         val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
         progressBinding.progressBar.isIndeterminate = true
-        return MaterialAlertDialogBuilder(requireContext())
+        val dialog = MaterialAlertDialogBuilder(requireContext())
             .setTitle(titleId)
             .setView(progressBinding.root)
-            .show()
+            .create()
+        dialog.setCanceledOnTouchOutside(false)
+
+        taskViewModel.isComplete.observe(this) { complete ->
+            if (complete) {
+                dialog.dismiss()
+                when (val result = taskViewModel.result.value) {
+                    is String -> Toast.makeText(requireContext(), result, Toast.LENGTH_LONG).show()
+                    is MessageDialogFragment -> result.show(
+                        parentFragmentManager,
+                        MessageDialogFragment.TAG
+                    )
+                }
+                taskViewModel.clear()
+            }
+        }
+
+        if (taskViewModel.isRunning.value == false) {
+            val task = requireArguments().getSerializable(TASK) as? () -> Any
+            if (task != null) {
+                taskViewModel.task = task
+                taskViewModel.runTask()
+            }
+        }
+        return dialog
     }
 
     companion object {
         const val TAG = "IndeterminateProgressDialogFragment"
 
         private const val TITLE = "Title"
+        private const val TASK = "Task"
 
         fun newInstance(
             titleId: Int,
+            task: () -> Any
         ): IndeterminateProgressDialogFragment {
             val dialog = IndeterminateProgressDialogFragment()
             val args = Bundle()
             args.putInt(TITLE, titleId)
+            args.putSerializable(TASK, task as Serializable)
             dialog.arguments = args
             return dialog
         }
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
new file mode 100644
index 000000000..23723bceb
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
@@ -0,0 +1,42 @@
+package org.yuzu.yuzu_emu.model
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class TaskViewModel : ViewModel() {
+    private val _result = MutableLiveData<Any>()
+    val result: LiveData<Any> = _result
+
+    private val _isComplete = MutableLiveData<Boolean>()
+    val isComplete: LiveData<Boolean> = _isComplete
+
+    private val _isRunning = MutableLiveData<Boolean>()
+    val isRunning: LiveData<Boolean> = _isRunning
+
+    lateinit var task: () -> Any
+
+    init {
+        clear()
+    }
+
+    fun clear() {
+        _result.value = Any()
+        _isComplete.value = false
+        _isRunning.value = false
+    }
+
+    fun runTask() {
+        if (_isRunning.value == true) return
+        _isRunning.value = true
+
+        viewModelScope.launch(Dispatchers.IO) {
+            val res = task()
+            _result.postValue(res)
+            _isComplete.postValue(true)
+        }
+    }
+}
\ 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 bb8311023..2001ad704 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,7 +26,6 @@ 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
@@ -340,44 +339,34 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                 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) {
+            val task: () -> Any = {
+                var messageToShow: Any
                 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)
-                        }
+                        messageToShow = MessageDialogFragment.newInstance(
+                            R.string.firmware_installed_failure,
+                            R.string.firmware_installed_failure_description
+                        )
                     } 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()
-                        }
+                        messageToShow = getString(R.string.save_file_imported_success)
                     }
                 } catch (e: Exception) {
-                    Toast.makeText(applicationContext, getString(R.string.fatal_error), Toast.LENGTH_LONG)
-                        .show()
+                    messageToShow = getString(R.string.fatal_error)
                 } finally {
                     cacheFirmwareDir.deleteRecursively()
                 }
+                messageToShow
             }
+
+            IndeterminateProgressDialogFragment.newInstance(
+                R.string.firmware_installing,
+                task
+            ).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
         }
 
     val getAmiiboKey =

From 409ff26f029861235b3f7b12400eea82c843244d Mon Sep 17 00:00:00 2001
From: PabloG02 <tioo23000@gmail.com>
Date: Mon, 5 Jun 2023 08:39:49 +0200
Subject: [PATCH 6/7] Address feedback

---
 .../fragments/HomeSettingsFragment.kt         |  9 ++++++--
 .../IndeterminateProgressDialogFragment.kt    | 22 +++++++++----------
 .../org/yuzu/yuzu_emu/model/TaskViewModel.kt  |  9 ++++++--
 .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt |  7 +++---
 4 files changed, 28 insertions(+), 19 deletions(-)

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 0bdbabe79..d2fa46323 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
@@ -275,7 +275,8 @@ class HomeSettingsFragment : Fragment() {
 
     private fun shareLog() {
         val file = DocumentFile.fromSingleUri(
-            mainActivity, DocumentsContract.buildDocumentUri(
+            mainActivity,
+            DocumentsContract.buildDocumentUri(
                 DocumentProvider.AUTHORITY,
                 "${DocumentProvider.ROOT_ID}/log/yuzu_log.txt"
             )
@@ -287,7 +288,11 @@ class HomeSettingsFragment : Fragment() {
                 .putExtra(Intent.EXTRA_STREAM, file.uri)
             startActivity(Intent.createChooser(intent, "Share log"))
         } else {
-            Toast.makeText(requireContext(), getText(R.string.share_log_missing), Toast.LENGTH_SHORT).show()
+            Toast.makeText(
+                requireContext(),
+                getText(R.string.share_log_missing),
+                Toast.LENGTH_SHORT
+            ).show()
         }
     }
 
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 10a897392..c7880d8cc 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
@@ -1,22 +1,24 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
 package org.yuzu.yuzu_emu.fragments
 
 import android.app.Dialog
 import android.os.Bundle
 import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
 import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.activityViewModels
 import androidx.lifecycle.ViewModelProvider
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
 import org.yuzu.yuzu_emu.model.TaskViewModel
-import java.io.Serializable
 
 
 class IndeterminateProgressDialogFragment : DialogFragment() {
-    private lateinit var taskViewModel: TaskViewModel
+    private val taskViewModel: TaskViewModel by activityViewModels()
 
     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
-        taskViewModel = ViewModelProvider(requireActivity())[TaskViewModel::class.java]
-
         val titleId = requireArguments().getInt(TITLE)
 
         val progressBinding = DialogProgressBarBinding.inflate(layoutInflater)
@@ -42,11 +44,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
         }
 
         if (taskViewModel.isRunning.value == false) {
-            val task = requireArguments().getSerializable(TASK) as? () -> Any
-            if (task != null) {
-                taskViewModel.task = task
-                taskViewModel.runTask()
-            }
+            taskViewModel.runTask()
         }
         return dialog
     }
@@ -55,18 +53,18 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
         const val TAG = "IndeterminateProgressDialogFragment"
 
         private const val TITLE = "Title"
-        private const val TASK = "Task"
 
         fun newInstance(
+            activity: AppCompatActivity,
             titleId: Int,
             task: () -> Any
         ): IndeterminateProgressDialogFragment {
             val dialog = IndeterminateProgressDialogFragment()
             val args = Bundle()
+            ViewModelProvider(activity)[TaskViewModel::class.java].task = task
             args.putInt(TITLE, titleId)
-            args.putSerializable(TASK, task as Serializable)
             dialog.arguments = args
             return dialog
         }
     }
-}
\ No newline at end of file
+}
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 23723bceb..27ea725a5 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
@@ -1,3 +1,6 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
 package org.yuzu.yuzu_emu.model
 
 import androidx.lifecycle.LiveData
@@ -30,7 +33,9 @@ class TaskViewModel : ViewModel() {
     }
 
     fun runTask() {
-        if (_isRunning.value == true) return
+        if (_isRunning.value == true) {
+            return
+        }
         _isRunning.value = true
 
         viewModelScope.launch(Dispatchers.IO) {
@@ -39,4 +44,4 @@ class TaskViewModel : ViewModel() {
             _isComplete.postValue(true)
         }
     }
-}
\ 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 2001ad704..6805efb55 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
@@ -345,15 +345,15 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                     FileUtil.unzip(inputZip, cacheFirmwareDir)
                     val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
                     val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
-                    if (unfilteredNumOfFiles != filteredNumOfFiles) {
-                        messageToShow = MessageDialogFragment.newInstance(
+                    messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
+                        MessageDialogFragment.newInstance(
                             R.string.firmware_installed_failure,
                             R.string.firmware_installed_failure_description
                         )
                     } else {
                         firmwarePath.deleteRecursively()
                         cacheFirmwareDir.copyRecursively(firmwarePath, true)
-                        messageToShow = getString(R.string.save_file_imported_success)
+                        getString(R.string.save_file_imported_success)
                     }
                 } catch (e: Exception) {
                     messageToShow = getString(R.string.fatal_error)
@@ -364,6 +364,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
             }
 
             IndeterminateProgressDialogFragment.newInstance(
+                this,
                 R.string.firmware_installing,
                 task
             ).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)

From e1078ec0f48186ea6947271fff0da881d91b7d1c Mon Sep 17 00:00:00 2001
From: bunnei <bunneidev@gmail.com>
Date: Mon, 5 Jun 2023 17:40:43 -0700
Subject: [PATCH 7/7] android: HomeSettingsFragment: Use string resource for
 "Share log".

---
 .../java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt    | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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 d2fa46323..bdc337501 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
@@ -286,7 +286,7 @@ class HomeSettingsFragment : Fragment() {
                 .setDataAndType(file.uri, FileUtil.TEXT_PLAIN)
                 .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
                 .putExtra(Intent.EXTRA_STREAM, file.uri)
-            startActivity(Intent.createChooser(intent, "Share log"))
+            startActivity(Intent.createChooser(intent, getText(R.string.share_log)))
         } else {
             Toast.makeText(
                 requireContext(),