From d79d4d5986e952000624edb244839fd1996be4ae Mon Sep 17 00:00:00 2001 From: t895 Date: Fri, 19 Jan 2024 01:06:10 -0500 Subject: [PATCH] android: Use callback to update progress bar dialogs --- .../yuzu/yuzu_emu/fragments/AddonsFragment.kt | 8 +- .../fragments/DriverManagerFragment.kt | 6 +- .../fragments/GamePropertiesFragment.kt | 25 ++--- .../yuzu_emu/fragments/InstallableFragment.kt | 29 ++--- ...gFragment.kt => ProgressDialogFragment.kt} | 44 +++++++- .../org/yuzu/yuzu_emu/model/TaskViewModel.kt | 29 ++++- .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 101 ++++++++---------- .../java/org/yuzu/yuzu_emu/utils/FileUtil.kt | 80 ++++++++++++-- .../yuzu/yuzu_emu/utils/GpuDriverHelper.kt | 5 +- .../main/res/layout/dialog_progress_bar.xml | 30 +++++- 10 files changed, 236 insertions(+), 121 deletions(-) rename src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/{IndeterminateProgressDialogFragment.kt => ProgressDialogFragment.kt} (73%) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt index 816336820..b63ece9a4 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt @@ -156,22 +156,22 @@ class AddonsFragment : Fragment() { descriptionId = R.string.invalid_directory_description ) if (isValid) { - IndeterminateProgressDialogFragment.newInstance( + ProgressDialogFragment.newInstance( requireActivity(), R.string.installing_game_content, false - ) { + ) { progressCallback, _ -> val parentDirectoryName = externalAddonDirectory.name val internalAddonDirectory = File(args.game.addonDir + parentDirectoryName) try { - externalAddonDirectory.copyFilesTo(internalAddonDirectory) + externalAddonDirectory.copyFilesTo(internalAddonDirectory, progressCallback) } catch (_: Exception) { return@newInstance errorMessage } addonViewModel.refreshAddons() return@newInstance getString(R.string.addon_installed_successfully) - }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) + }.show(parentFragmentManager, ProgressDialogFragment.TAG) } else { errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt index 9dabb9c41..6c758d80b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt @@ -173,11 +173,11 @@ class DriverManagerFragment : Fragment() { return@registerForActivityResult } - IndeterminateProgressDialogFragment.newInstance( + ProgressDialogFragment.newInstance( requireActivity(), R.string.installing_driver, false - ) { + ) { _, _ -> val driverPath = "${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(result)}" val driverFile = File(driverPath) @@ -213,6 +213,6 @@ class DriverManagerFragment : Fragment() { } } return@newInstance Any() - }.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG) + }.show(childFragmentManager, ProgressDialogFragment.TAG) } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt index b04d1208f..83a845434 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt @@ -44,7 +44,6 @@ import org.yuzu.yuzu_emu.utils.FileUtil import org.yuzu.yuzu_emu.utils.GameIconUtils import org.yuzu.yuzu_emu.utils.GpuDriverHelper import org.yuzu.yuzu_emu.utils.MemoryUtil -import java.io.BufferedInputStream import java.io.BufferedOutputStream import java.io.File @@ -357,27 +356,17 @@ class GamePropertiesFragment : Fragment() { return@registerForActivityResult } - val inputZip = requireContext().contentResolver.openInputStream(result) val savesFolder = File(args.game.saveDir) val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") cacheSaveDir.mkdir() - if (inputZip == null) { - Toast.makeText( - YuzuApplication.appContext, - getString(R.string.fatal_error), - Toast.LENGTH_LONG - ).show() - return@registerForActivityResult - } - - IndeterminateProgressDialogFragment.newInstance( + ProgressDialogFragment.newInstance( requireActivity(), R.string.save_files_importing, false - ) { + ) { _, _ -> try { - FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) + FileUtil.unzipToInternalStorage(result.toString(), cacheSaveDir) val files = cacheSaveDir.listFiles() var savesFolderFile: File? = null if (files != null) { @@ -422,7 +411,7 @@ class GamePropertiesFragment : Fragment() { Toast.LENGTH_LONG ).show() } - }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) + }.show(parentFragmentManager, ProgressDialogFragment.TAG) } /** @@ -436,11 +425,11 @@ class GamePropertiesFragment : Fragment() { return@registerForActivityResult } - IndeterminateProgressDialogFragment.newInstance( + ProgressDialogFragment.newInstance( requireActivity(), R.string.save_files_exporting, false - ) { + ) { _, _ -> val saveLocation = args.game.saveDir val zipResult = FileUtil.zipFromInternalStorage( File(saveLocation), @@ -452,6 +441,6 @@ class GamePropertiesFragment : Fragment() { TaskState.Completed -> getString(R.string.export_success) TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) } - }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) + }.show(parentFragmentManager, ProgressDialogFragment.TAG) } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt index 5b4bf2c9f..7df8e6bf4 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt @@ -34,7 +34,6 @@ import org.yuzu.yuzu_emu.model.TaskState import org.yuzu.yuzu_emu.ui.main.MainActivity import org.yuzu.yuzu_emu.utils.DirectoryInitialization import org.yuzu.yuzu_emu.utils.FileUtil -import java.io.BufferedInputStream import java.io.BufferedOutputStream import java.io.File import java.math.BigInteger @@ -195,26 +194,20 @@ class InstallableFragment : Fragment() { return@registerForActivityResult } - val inputZip = requireContext().contentResolver.openInputStream(result) val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") cacheSaveDir.mkdir() - if (inputZip == null) { - Toast.makeText( - YuzuApplication.appContext, - getString(R.string.fatal_error), - Toast.LENGTH_LONG - ).show() - return@registerForActivityResult - } - - IndeterminateProgressDialogFragment.newInstance( + ProgressDialogFragment.newInstance( requireActivity(), R.string.save_files_importing, false - ) { + ) { progressCallback, _ -> try { - FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) + FileUtil.unzipToInternalStorage( + result.toString(), + cacheSaveDir, + progressCallback + ) val files = cacheSaveDir.listFiles() var successfulImports = 0 var failedImports = 0 @@ -287,7 +280,7 @@ class InstallableFragment : Fragment() { Toast.LENGTH_LONG ).show() } - }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) + }.show(parentFragmentManager, ProgressDialogFragment.TAG) } private val exportSaves = registerForActivityResult( @@ -297,11 +290,11 @@ class InstallableFragment : Fragment() { return@registerForActivityResult } - IndeterminateProgressDialogFragment.newInstance( + ProgressDialogFragment.newInstance( requireActivity(), R.string.save_files_exporting, false - ) { + ) { _, _ -> val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") cacheSaveDir.mkdir() @@ -338,6 +331,6 @@ class InstallableFragment : Fragment() { TaskState.Completed -> getString(R.string.export_success) TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) } - }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) + }.show(parentFragmentManager, ProgressDialogFragment.TAG) } } 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/ProgressDialogFragment.kt similarity index 73% rename from src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt rename to src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt index 8847e5531..d201cb80c 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/ProgressDialogFragment.kt @@ -23,11 +23,13 @@ import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding import org.yuzu.yuzu_emu.model.TaskViewModel -class IndeterminateProgressDialogFragment : DialogFragment() { +class ProgressDialogFragment : DialogFragment() { private val taskViewModel: TaskViewModel by activityViewModels() private lateinit var binding: DialogProgressBarBinding + private val PROGRESS_BAR_RESOLUTION = 1000 + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val titleId = requireArguments().getInt(TITLE) val cancellable = requireArguments().getBoolean(CANCELLABLE) @@ -61,6 +63,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.message.isSelected = true viewLifecycleOwner.lifecycleScope.apply { launch { repeatOnLifecycle(Lifecycle.State.CREATED) { @@ -97,6 +100,35 @@ class IndeterminateProgressDialogFragment : DialogFragment() { } } } + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + taskViewModel.progress.collect { + if (it != 0.0) { + binding.progressBar.apply { + isIndeterminate = false + progress = ( + (it / taskViewModel.maxProgress.value) * + PROGRESS_BAR_RESOLUTION + ).toInt() + min = 0 + max = PROGRESS_BAR_RESOLUTION + } + } + } + } + } + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + taskViewModel.message.collect { + if (it.isEmpty()) { + binding.message.visibility = View.GONE + } else { + binding.message.visibility = View.VISIBLE + binding.message.text = it + } + } + } + } } } @@ -108,6 +140,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() { val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE) negativeButton.setOnClickListener { alertDialog.setTitle(getString(R.string.cancelling)) + binding.progressBar.isIndeterminate = true taskViewModel.setCancelled(true) } } @@ -122,9 +155,12 @@ class IndeterminateProgressDialogFragment : DialogFragment() { activity: FragmentActivity, titleId: Int, cancellable: Boolean = false, - task: suspend () -> Any - ): IndeterminateProgressDialogFragment { - val dialog = IndeterminateProgressDialogFragment() + task: suspend ( + progressCallback: (max: Long, progress: Long) -> Boolean, + messageCallback: (message: String) -> Unit + ) -> Any + ): ProgressDialogFragment { + val dialog = ProgressDialogFragment() val args = Bundle() ViewModelProvider(activity)[TaskViewModel::class.java].task = task args.putInt(TITLE, titleId) 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 e59c95733..4361eb972 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 @@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch class TaskViewModel : ViewModel() { @@ -23,13 +24,28 @@ class TaskViewModel : ViewModel() { val cancelled: StateFlow get() = _cancelled private val _cancelled = MutableStateFlow(false) - lateinit var task: suspend () -> Any + private val _progress = MutableStateFlow(0.0) + val progress = _progress.asStateFlow() + + private val _maxProgress = MutableStateFlow(0.0) + val maxProgress = _maxProgress.asStateFlow() + + private val _message = MutableStateFlow("") + val message = _message.asStateFlow() + + lateinit var task: suspend ( + progressCallback: (max: Long, progress: Long) -> Boolean, + messageCallback: (message: String) -> Unit + ) -> Any fun clear() { _result.value = Any() _isComplete.value = false _isRunning.value = false _cancelled.value = false + _progress.value = 0.0 + _maxProgress.value = 0.0 + _message.value = "" } fun setCancelled(value: Boolean) { @@ -43,7 +59,16 @@ class TaskViewModel : ViewModel() { _isRunning.value = true viewModelScope.launch(Dispatchers.IO) { - val res = task() + val res = task( + { max, progress -> + _maxProgress.value = max.toDouble() + _progress.value = progress.toDouble() + return@task cancelled.value + }, + { message -> + _message.value = message + } + ) _result.value = res _isComplete.value = true _isRunning.value = false 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 644289e25..c2cc29961 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 @@ -38,12 +38,13 @@ import org.yuzu.yuzu_emu.activities.EmulationActivity import org.yuzu.yuzu_emu.databinding.ActivityMainBinding import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment -import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment +import org.yuzu.yuzu_emu.fragments.ProgressDialogFragment import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.model.AddonViewModel import org.yuzu.yuzu_emu.model.DriverViewModel import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.model.InstallResult import org.yuzu.yuzu_emu.model.TaskState import org.yuzu.yuzu_emu.model.TaskViewModel import org.yuzu.yuzu_emu.utils.* @@ -369,26 +370,23 @@ class MainActivity : AppCompatActivity(), ThemeProvider { 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 task: () -> Any = { + ProgressDialogFragment.newInstance( + this, + R.string.firmware_installing + ) { progressCallback, _ -> var messageToShow: Any try { - FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheFirmwareDir) + FileUtil.unzipToInternalStorage( + result.toString(), + cacheFirmwareDir, + progressCallback + ) val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { @@ -404,18 +402,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider { getString(R.string.save_file_imported_success) } } catch (e: Exception) { + Log.error("[MainActivity] Firmware install failed - ${e.message}") messageToShow = getString(R.string.fatal_error) } finally { cacheFirmwareDir.deleteRecursively() } messageToShow - } - - IndeterminateProgressDialogFragment.newInstance( - this, - R.string.firmware_installing, - task = task - ).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) + }.show(supportFragmentManager, ProgressDialogFragment.TAG) } val getAmiiboKey = @@ -474,11 +467,11 @@ class MainActivity : AppCompatActivity(), ThemeProvider { return@registerForActivityResult } - IndeterminateProgressDialogFragment.newInstance( + ProgressDialogFragment.newInstance( this@MainActivity, R.string.verifying_content, false - ) { + ) { _, _ -> var updatesMatchProgram = true for (document in documents) { val valid = NativeLibrary.doesUpdateMatchProgram( @@ -501,44 +494,42 @@ class MainActivity : AppCompatActivity(), ThemeProvider { positiveAction = { homeViewModel.setContentToInstall(documents) } ) } - }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) + }.show(supportFragmentManager, ProgressDialogFragment.TAG) } private fun installContent(documents: List) { - IndeterminateProgressDialogFragment.newInstance( + ProgressDialogFragment.newInstance( this@MainActivity, R.string.installing_game_content - ) { + ) { progressCallback, messageCallback -> var installSuccess = 0 var installOverwrite = 0 var errorBaseGame = 0 - var errorExtension = 0 - var errorOther = 0 + var error = 0 documents.forEach { + messageCallback.invoke(FileUtil.getFilename(it)) when ( - NativeLibrary.installFileToNand( - it.toString(), - FileUtil.getExtension(it) + InstallResult.from( + NativeLibrary.installFileToNand( + it.toString(), + progressCallback + ) ) ) { - NativeLibrary.InstallFileToNandResult.Success -> { + InstallResult.Success -> { installSuccess += 1 } - NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> { + InstallResult.Overwrite -> { installOverwrite += 1 } - NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> { + InstallResult.BaseInstallAttempted -> { errorBaseGame += 1 } - NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> { - errorExtension += 1 - } - - else -> { - errorOther += 1 + InstallResult.Failure -> { + error += 1 } } } @@ -565,7 +556,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { ) installResult.append(separator) } - val errorTotal: Int = errorBaseGame + errorExtension + errorOther + val errorTotal: Int = errorBaseGame + error if (errorTotal > 0) { installResult.append(separator) installResult.append( @@ -582,14 +573,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { ) 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) { + if (error > 0) { installResult.append( getString(R.string.install_game_content_failure_description) ) @@ -608,7 +592,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { descriptionString = installResult.toString().trim() ) } - }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) + }.show(supportFragmentManager, ProgressDialogFragment.TAG) } val exportUserData = registerForActivityResult( @@ -618,16 +602,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider { return@registerForActivityResult } - IndeterminateProgressDialogFragment.newInstance( + ProgressDialogFragment.newInstance( this, R.string.exporting_user_data, true - ) { + ) { progressCallback, _ -> val zipResult = FileUtil.zipFromInternalStorage( File(DirectoryInitialization.userDirectory!!), DirectoryInitialization.userDirectory!!, BufferedOutputStream(contentResolver.openOutputStream(result)), - taskViewModel.cancelled, + progressCallback, compression = false ) return@newInstance when (zipResult) { @@ -635,7 +619,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { TaskState.Failed -> R.string.export_failed TaskState.Cancelled -> R.string.user_data_export_cancelled } - }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) + }.show(supportFragmentManager, ProgressDialogFragment.TAG) } val importUserData = @@ -644,10 +628,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider { return@registerForActivityResult } - IndeterminateProgressDialogFragment.newInstance( + ProgressDialogFragment.newInstance( this, R.string.importing_user_data - ) { + ) { progressCallback, _ -> val checkStream = ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result))) var isYuzuBackup = false @@ -676,8 +660,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider { // Copy archive to internal storage try { FileUtil.unzipToInternalStorage( - BufferedInputStream(contentResolver.openInputStream(result)), - File(DirectoryInitialization.userDirectory!!) + result.toString(), + File(DirectoryInitialization.userDirectory!!), + progressCallback ) } catch (e: Exception) { return@newInstance MessageDialogFragment.newInstance( @@ -694,6 +679,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider { driverViewModel.reloadDriverData() return@newInstance getString(R.string.user_data_import_success) - }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) + }.show(supportFragmentManager, ProgressDialogFragment.TAG) } } 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 b54a19c65..fc2339f5a 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 @@ -7,7 +7,6 @@ import android.database.Cursor import android.net.Uri import android.provider.DocumentsContract import androidx.documentfile.provider.DocumentFile -import kotlinx.coroutines.flow.StateFlow import java.io.BufferedInputStream import java.io.File import java.io.IOException @@ -19,6 +18,7 @@ import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.model.MinimalDocumentFile import org.yuzu.yuzu_emu.model.TaskState import java.io.BufferedOutputStream +import java.io.OutputStream import java.lang.NullPointerException import java.nio.charset.StandardCharsets import java.util.zip.Deflater @@ -283,12 +283,34 @@ object FileUtil { /** * Extracts the given zip file into the given directory. + * @param path String representation of a [Uri] or a typical path delimited by '/' + * @param destDir Location to unzip the contents of [path] into + * @param progressCallback Lambda that is called with the total number of files and the current + * progress through the process. Stops execution as soon as possible if this returns true. */ @Throws(SecurityException::class) - fun unzipToInternalStorage(zipStream: BufferedInputStream, destDir: File) { - ZipInputStream(zipStream).use { zis -> + fun unzipToInternalStorage( + path: String, + destDir: File, + progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false } + ) { + var totalEntries = 0L + ZipInputStream(getInputStream(path)).use { zis -> + var tempEntry = zis.nextEntry + while (tempEntry != null) { + tempEntry = zis.nextEntry + totalEntries++ + } + } + + var progress = 0L + ZipInputStream(getInputStream(path)).use { zis -> var entry: ZipEntry? = zis.nextEntry while (entry != null) { + if (progressCallback.invoke(totalEntries, progress)) { + return@use + } + val newFile = File(destDir, entry.name) val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile @@ -304,6 +326,7 @@ object FileUtil { newFile.outputStream().use { fos -> zis.copyTo(fos) } } entry = zis.nextEntry + progress++ } } } @@ -313,14 +336,15 @@ object FileUtil { * @param inputFile File representation of the item that will be zipped * @param rootDir Directory containing the inputFile * @param outputStream Stream where the zip file will be output - * @param cancelled [StateFlow] that reports whether this process has been cancelled + * @param progressCallback Lambda that is called with the total number of files and the current + * progress through the process. Stops execution as soon as possible if this returns true. * @param compression Disables compression if true */ fun zipFromInternalStorage( inputFile: File, rootDir: String, outputStream: BufferedOutputStream, - cancelled: StateFlow? = null, + progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }, compression: Boolean = true ): TaskState { try { @@ -330,8 +354,10 @@ object FileUtil { zos.setLevel(Deflater.NO_COMPRESSION) } + var count = 0L + val totalFiles = inputFile.walkTopDown().count().toLong() inputFile.walkTopDown().forEach { file -> - if (cancelled?.value == true) { + if (progressCallback.invoke(totalFiles, count)) { return TaskState.Cancelled } @@ -343,6 +369,7 @@ object FileUtil { if (file.isFile) { file.inputStream().use { fis -> fis.copyTo(zos) } } + count++ } } } @@ -356,9 +383,14 @@ object FileUtil { /** * Helper function that copies the contents of a DocumentFile folder into a [File] * @param file [File] representation of the folder to copy into + * @param progressCallback Lambda that is called with the total number of files and the current + * progress through the process. Stops execution as soon as possible if this returns true. * @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa */ - fun DocumentFile.copyFilesTo(file: File) { + fun DocumentFile.copyFilesTo( + file: File, + progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false } + ) { file.mkdirs() if (!this.isDirectory || !file.isDirectory) { throw IllegalStateException( @@ -366,7 +398,13 @@ object FileUtil { ) } + var count = 0L + val totalFiles = this.listFiles().size.toLong() this.listFiles().forEach { + if (progressCallback.invoke(totalFiles, count)) { + return + } + val newFile = File(file, it.name!!) if (it.isDirectory) { newFile.mkdirs() @@ -381,6 +419,7 @@ object FileUtil { newFile.outputStream().use { os -> bos.copyTo(os) } } } + count++ } } @@ -427,6 +466,18 @@ object FileUtil { } } + fun getInputStream(path: String) = if (path.contains("content://")) { + Uri.parse(path).inputStream() + } else { + File(path).inputStream() + } + + fun getOutputStream(path: String) = if (path.contains("content://")) { + Uri.parse(path).outputStream() + } else { + File(path).outputStream() + } + @Throws(IOException::class) fun getStringFromFile(file: File): String = String(file.readBytes(), StandardCharsets.UTF_8) @@ -434,4 +485,19 @@ object FileUtil { @Throws(IOException::class) fun getStringFromInputStream(stream: InputStream): String = String(stream.readBytes(), StandardCharsets.UTF_8) + + fun DocumentFile.inputStream(): InputStream = + YuzuApplication.appContext.contentResolver.openInputStream(uri)!! + + fun DocumentFile.outputStream(): OutputStream = + YuzuApplication.appContext.contentResolver.openOutputStream(uri)!! + + fun Uri.inputStream(): InputStream = + YuzuApplication.appContext.contentResolver.openInputStream(this)!! + + fun Uri.outputStream(): OutputStream = + YuzuApplication.appContext.contentResolver.openOutputStream(this)!! + + fun Uri.asDocumentFile(): DocumentFile? = + DocumentFile.fromSingleUri(YuzuApplication.appContext, this) } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt index a8f9dcc34..81212cbee 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt @@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.utils import android.net.Uri import android.os.Build -import java.io.BufferedInputStream import java.io.File import java.io.IOException import org.yuzu.yuzu_emu.NativeLibrary @@ -123,7 +122,7 @@ object GpuDriverHelper { // Unzip the driver. try { FileUtil.unzipToInternalStorage( - BufferedInputStream(copiedFile.inputStream()), + copiedFile.path, File(driverInstallationPath!!) ) } catch (e: SecurityException) { @@ -156,7 +155,7 @@ object GpuDriverHelper { // Unzip the driver to the private installation directory try { FileUtil.unzipToInternalStorage( - BufferedInputStream(driver.inputStream()), + driver.path, File(driverInstallationPath!!) ) } catch (e: SecurityException) { diff --git a/src/android/app/src/main/res/layout/dialog_progress_bar.xml b/src/android/app/src/main/res/layout/dialog_progress_bar.xml index 0209ea082..e61aa5294 100644 --- a/src/android/app/src/main/res/layout/dialog_progress_bar.xml +++ b/src/android/app/src/main/res/layout/dialog_progress_bar.xml @@ -1,8 +1,30 @@ - + android:orientation="vertical"> + + + + + +