From 6c7e284f64dd4f2f02014dd6488924a0343a3292 Mon Sep 17 00:00:00 2001
From: Abandoned Cart <twistedumbrella@gmail.com>
Date: Thu, 15 Jun 2023 22:36:03 -0400
Subject: [PATCH 1/2] android: Add support for concurrent installs

---
 .../fragments/InstallDialogFragment.kt        |  62 +++++++++
 .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 118 +++++++++++++-----
 .../app/src/main/res/values/strings.xml       |  14 ++-
 3 files changed, 154 insertions(+), 40 deletions(-)
 create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallDialogFragment.kt

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallDialogFragment.kt
new file mode 100644
index 0000000000..d8850f9415
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallDialogFragment.kt
@@ -0,0 +1,62 @@
+// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+package org.yuzu.yuzu_emu.fragments
+
+import android.app.Dialog
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import androidx.fragment.app.DialogFragment
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import org.yuzu.yuzu_emu.R
+
+class InstallDialogFragment : DialogFragment() {
+    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+        val titleId = requireArguments().getInt(TITLE)
+        val description = requireArguments().getString(DESCRIPTION)
+        val helpLinkId = requireArguments().getInt(HELP_LINK)
+
+        val dialog = MaterialAlertDialogBuilder(requireContext())
+            .setPositiveButton(R.string.close, null)
+            .setTitle(titleId)
+            .setMessage(description)
+
+        if (helpLinkId != 0) {
+            dialog.setNeutralButton(R.string.learn_more) { _, _ ->
+                openLink(getString(helpLinkId))
+            }
+        }
+
+        return dialog.show()
+    }
+
+    private fun openLink(link: String) {
+        val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
+        startActivity(intent)
+    }
+
+    companion object {
+        const val TAG = "MessageDialogFragment"
+
+        private const val TITLE = "Title"
+        private const val DESCRIPTION = "Description"
+        private const val HELP_LINK = "Link"
+
+        fun newInstance(
+            titleId: Int,
+            description: String,
+            helpLinkId: Int = 0
+        ): InstallDialogFragment {
+            val dialog = InstallDialogFragment()
+            val bundle = Bundle()
+            bundle.apply {
+                putInt(TITLE, titleId)
+                putString(DESCRIPTION, description)
+                putInt(HELP_LINK, helpLinkId)
+            }
+            dialog.arguments = bundle
+            return dialog
+        }
+    }
+}
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 cc1d87f1b4..5257d7b360 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
@@ -4,6 +4,7 @@
 package org.yuzu.yuzu_emu.ui.main
 
 import android.content.Intent
+import android.net.Uri
 import android.os.Bundle
 import android.view.View
 import android.view.ViewGroup.MarginLayoutParams
@@ -42,6 +43,7 @@ import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
 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.InstallDialogFragment
 import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
 import org.yuzu.yuzu_emu.model.GamesViewModel
 import org.yuzu.yuzu_emu.model.HomeViewModel
@@ -481,62 +483,110 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
             }
         }
 
-    val installGameUpdate =
-        registerForActivityResult(ActivityResultContracts.OpenDocument()) {
-            if (it == null) {
-                return@registerForActivityResult
-            }
-
+    val installGameUpdate = registerForActivityResult(
+        ActivityResultContracts.OpenMultipleDocuments()
+    ) { documents: List<Uri> ->
+        if (documents.isNotEmpty()) {
             IndeterminateProgressDialogFragment.newInstance(
                 this@MainActivity,
                 R.string.install_game_content
             ) {
-                val result = NativeLibrary.installFileToNand(it.toString())
+                var installSuccess = 0
+                var installOverwrite = 0
+                var errorBaseGame = 0
+                var errorExtension = 0
+                var errorOther = 0
+                var errorTotal = 0
                 lifecycleScope.launch {
-                    withContext(Dispatchers.Main) {
-                        when (result) {
+                    documents.forEach {
+                        when (NativeLibrary.installFileToNand(it.toString())) {
                             NativeLibrary.InstallFileToNandResult.Success -> {
-                                Toast.makeText(
-                                    applicationContext,
-                                    R.string.install_game_content_success,
-                                    Toast.LENGTH_SHORT
-                                ).show()
+                                installSuccess += 1
                             }
 
                             NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> {
-                                Toast.makeText(
-                                    applicationContext,
-                                    R.string.install_game_content_success_overwrite,
-                                    Toast.LENGTH_SHORT
-                                ).show()
+                                installOverwrite += 1
                             }
 
                             NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> {
-                                MessageDialogFragment.newInstance(
-                                    R.string.install_game_content_failure,
-                                    R.string.install_game_content_failure_base
-                                ).show(supportFragmentManager, MessageDialogFragment.TAG)
+                                errorBaseGame += 1
                             }
 
                             NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> {
-                                MessageDialogFragment.newInstance(
-                                    R.string.install_game_content_failure,
-                                    R.string.install_game_content_failure_file_extension,
-                                    R.string.install_game_content_help_link
-                                ).show(supportFragmentManager, MessageDialogFragment.TAG)
+                                errorExtension += 1
                             }
 
                             else -> {
-                                MessageDialogFragment.newInstance(
-                                    R.string.install_game_content_failure,
-                                    R.string.install_game_content_failure_description,
-                                    R.string.install_game_content_help_link
-                                ).show(supportFragmentManager, MessageDialogFragment.TAG)
+                                errorOther += 1
                             }
                         }
                     }
+                    withContext(Dispatchers.Main) {
+                        val separator = System.getProperty("line.separator") ?: "\n"
+                        val installResult = StringBuilder()
+                        if (installSuccess > 0) {
+                            installResult.append(
+                                getString(
+                                    R.string.install_game_content_success_install,
+                                    installSuccess
+                                )
+                            )
+                            installResult.append(separator)
+                        }
+                        if (installOverwrite > 0) {
+                            installResult.append(
+                                getString(
+                                    R.string.install_game_content_success_overwrite,
+                                    installOverwrite
+                                )
+                            )
+                            installResult.append(separator)
+                        }
+                        errorTotal = errorBaseGame + errorExtension + errorOther
+                        if (errorTotal > 0) {
+                            installResult.append(separator)
+                            installResult.append(
+                                getString(
+                                    R.string.install_game_content_failed_count,
+
+                                )
+                            )
+                            installResult.append(separator)
+                            if (errorBaseGame > 0) {
+                                installResult.append(separator)
+                                installResult.append(
+                                    getString(R.string.install_game_content_failure_base)
+                                )
+                                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) {
+                                installResult.append(
+                                    getString(R.string.install_game_content_failure_description)
+                                )
+                                installResult.append(separator)
+                            }
+                            InstallDialogFragment.newInstance(
+                                R.string.install_game_content_failure,
+                                installResult.toString().trim(),
+                                R.string.install_game_content_help_link
+                            ).show(supportFragmentManager, MessageDialogFragment.TAG)
+                        } else {
+                            InstallDialogFragment.newInstance(
+                                R.string.install_game_content_success,
+                                installResult.toString().trim(),
+                            ).show(supportFragmentManager, MessageDialogFragment.TAG)
+                        }
+                    }
                 }
-                return@newInstance result
+                return@newInstance installSuccess + installOverwrite + errorTotal
             }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
         }
+    }
 }
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index cc1d8c39d9..75eca30a13 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -104,12 +104,14 @@
     <string name="share_log_missing">No log file found</string>
     <string name="install_game_content">Install game content</string>
     <string name="install_game_content_description">Install game updates or DLC</string>
-    <string name="install_game_content_failure">Error installing file to NAND</string>
-    <string name="install_game_content_failure_description">Game content installation failed. Please ensure content is valid and that the prod.keys file is installed.</string>
-    <string name="install_game_content_failure_base">Installation of base games isn\'t permitted in order to avoid possible conflicts. Please select an update or DLC instead.</string>
-    <string name="install_game_content_failure_file_extension">The selected file type is not supported. Only NSP and XCI content is supported for this action. Please verify the game content is valid.</string>
-    <string name="install_game_content_success">Game content installed successfully</string>
-    <string name="install_game_content_success_overwrite">Game content was overwritten successfully</string>
+    <string name="install_game_content_failure">Error installing file(s) to NAND</string>
+    <string name="install_game_content_failure_description">Please ensure content(s) are valid and that the prod.keys file is installed.</string>
+    <string name="install_game_content_failure_base">Installation of base games isn\'t permitted in order to avoid possible conflicts.</string>
+    <string name="install_game_content_failure_file_extension">Only NSP and XCI content is supported. Please verify the game content(s) are valid.</string>
+    <string name="install_game_content_failed_count">%1$d installation error(s)</string>
+    <string name="install_game_content_success">Game content(s) installed successfully</string>
+    <string name="install_game_content_success_install">%1$d installed successfully</string>
+    <string name="install_game_content_success_overwrite">%1$d overwritten successfully</string>
     <string name="install_game_content_help_link">https://yuzu-emu.org/help/quickstart/#dumping-installed-updates</string>
 
     <!-- About screen strings -->

From 1a85d8804a044d689e53b2497be01b65c76c34d2 Mon Sep 17 00:00:00 2001
From: Abandoned Cart <twistedumbrella@gmail.com>
Date: Fri, 16 Jun 2023 07:50:47 -0400
Subject: [PATCH 2/2] android: Generalize string message dialog

---
 ...logFragment.kt => LongMessageDialogFragment.kt} |  8 ++++----
 .../java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 14 +++++++-------
 2 files changed, 11 insertions(+), 11 deletions(-)
 rename src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/{InstallDialogFragment.kt => LongMessageDialogFragment.kt} (89%)

diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LongMessageDialogFragment.kt
similarity index 89%
rename from src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallDialogFragment.kt
rename to src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LongMessageDialogFragment.kt
index d8850f9415..b29b627e9e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LongMessageDialogFragment.kt
@@ -11,7 +11,7 @@ import androidx.fragment.app.DialogFragment
 import com.google.android.material.dialog.MaterialAlertDialogBuilder
 import org.yuzu.yuzu_emu.R
 
-class InstallDialogFragment : DialogFragment() {
+class LongMessageDialogFragment : DialogFragment() {
     override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
         val titleId = requireArguments().getInt(TITLE)
         val description = requireArguments().getString(DESCRIPTION)
@@ -37,7 +37,7 @@ class InstallDialogFragment : DialogFragment() {
     }
 
     companion object {
-        const val TAG = "MessageDialogFragment"
+        const val TAG = "LongMessageDialogFragment"
 
         private const val TITLE = "Title"
         private const val DESCRIPTION = "Description"
@@ -47,8 +47,8 @@ class InstallDialogFragment : DialogFragment() {
             titleId: Int,
             description: String,
             helpLinkId: Int = 0
-        ): InstallDialogFragment {
-            val dialog = InstallDialogFragment()
+        ): LongMessageDialogFragment {
+            val dialog = LongMessageDialogFragment()
             val bundle = Bundle()
             bundle.apply {
                 putInt(TITLE, titleId)
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 5257d7b360..3086cfad38 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
@@ -43,7 +43,7 @@ import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
 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.InstallDialogFragment
+import org.yuzu.yuzu_emu.fragments.LongMessageDialogFragment
 import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
 import org.yuzu.yuzu_emu.model.GamesViewModel
 import org.yuzu.yuzu_emu.model.HomeViewModel
@@ -548,7 +548,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                             installResult.append(
                                 getString(
                                     R.string.install_game_content_failed_count,
-
+                                    errorTotal
                                 )
                             )
                             installResult.append(separator)
@@ -572,16 +572,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
                                 )
                                 installResult.append(separator)
                             }
-                            InstallDialogFragment.newInstance(
+                            LongMessageDialogFragment.newInstance(
                                 R.string.install_game_content_failure,
                                 installResult.toString().trim(),
                                 R.string.install_game_content_help_link
-                            ).show(supportFragmentManager, MessageDialogFragment.TAG)
+                            ).show(supportFragmentManager, LongMessageDialogFragment.TAG)
                         } else {
-                            InstallDialogFragment.newInstance(
+                            LongMessageDialogFragment.newInstance(
                                 R.string.install_game_content_success,
-                                installResult.toString().trim(),
-                            ).show(supportFragmentManager, MessageDialogFragment.TAG)
+                                installResult.toString().trim()
+                            ).show(supportFragmentManager, LongMessageDialogFragment.TAG)
                         }
                     }
                 }