android: Use callback to update progress bar dialogs
This commit is contained in:
		@@ -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)
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
        }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
@@ -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<Boolean> 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
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Uri>) {
 | 
			
		||||
        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)
 | 
			
		||||
        }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Boolean>? = 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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,30 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<com.google.android.material.progressindicator.LinearProgressIndicator xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:app="http://schemas.android.com/apk/res-auto"
 | 
			
		||||
    android:id="@+id/progress_bar"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="wrap_content"
 | 
			
		||||
    android:padding="24dp"
 | 
			
		||||
    app:trackCornerRadius="4dp" />
 | 
			
		||||
    android:orientation="vertical">
 | 
			
		||||
 | 
			
		||||
    <com.google.android.material.textview.MaterialTextView
 | 
			
		||||
        android:id="@+id/message"
 | 
			
		||||
        style="@style/TextAppearance.Material3.BodyMedium"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_marginHorizontal="24dp"
 | 
			
		||||
        android:layout_marginTop="12dp"
 | 
			
		||||
        android:layout_marginBottom="6dp"
 | 
			
		||||
        android:ellipsize="marquee"
 | 
			
		||||
        android:marqueeRepeatLimit="marquee_forever"
 | 
			
		||||
        android:requiresFadingEdge="horizontal"
 | 
			
		||||
        android:singleLine="true"
 | 
			
		||||
        android:textAlignment="viewStart"
 | 
			
		||||
        android:visibility="gone" />
 | 
			
		||||
 | 
			
		||||
    <com.google.android.material.progressindicator.LinearProgressIndicator
 | 
			
		||||
        android:id="@+id/progress_bar"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:padding="24dp"
 | 
			
		||||
        app:trackCornerRadius="4dp" />
 | 
			
		||||
 | 
			
		||||
</LinearLayout>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user