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(),