Merge pull request #12715 from t895/remove-addons
android: Add uninstall addon button
This commit is contained in:
		@@ -21,6 +21,8 @@ import org.yuzu.yuzu_emu.utils.DocumentsTree
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.FileUtil
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.Log
 | 
			
		||||
import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
 | 
			
		||||
import org.yuzu.yuzu_emu.model.InstallResult
 | 
			
		||||
import org.yuzu.yuzu_emu.model.Patch
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class which contains methods that interact
 | 
			
		||||
@@ -235,9 +237,12 @@ object NativeLibrary {
 | 
			
		||||
    /**
 | 
			
		||||
     * Installs a nsp or xci file to nand
 | 
			
		||||
     * @param filename String representation of file uri
 | 
			
		||||
     * @param extension Lowercase string representation of file extension without "."
 | 
			
		||||
     * @return int representation of [InstallResult]
 | 
			
		||||
     */
 | 
			
		||||
    external fun installFileToNand(filename: String, extension: String): Int
 | 
			
		||||
    external fun installFileToNand(
 | 
			
		||||
        filename: String,
 | 
			
		||||
        callback: (max: Long, progress: Long) -> Boolean
 | 
			
		||||
    ): Int
 | 
			
		||||
 | 
			
		||||
    external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean
 | 
			
		||||
 | 
			
		||||
@@ -535,9 +540,29 @@ object NativeLibrary {
 | 
			
		||||
     *
 | 
			
		||||
     * @param path Path to game file. Can be a [Uri].
 | 
			
		||||
     * @param programId String representation of a game's program ID
 | 
			
		||||
     * @return Array of pairs where the first value is the name of an addon and the second is the version
 | 
			
		||||
     * @return Array of available patches
 | 
			
		||||
     */
 | 
			
		||||
    external fun getAddonsForFile(path: String, programId: String): Array<Pair<String, String>>?
 | 
			
		||||
    external fun getPatchesForFile(path: String, programId: String): Array<Patch>?
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Removes an update for a given [programId]
 | 
			
		||||
     * @param programId String representation of a game's program ID
 | 
			
		||||
     */
 | 
			
		||||
    external fun removeUpdate(programId: String)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Removes all DLC for a  [programId]
 | 
			
		||||
     * @param programId String representation of a game's program ID
 | 
			
		||||
     */
 | 
			
		||||
    external fun removeDLC(programId: String)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Removes a mod installed for a given [programId]
 | 
			
		||||
     * @param programId String representation of a game's program ID
 | 
			
		||||
     * @param name The name of a mod as given by [getPatchesForFile]. This corresponds with the name
 | 
			
		||||
     * of the mod's directory in a game's load folder.
 | 
			
		||||
     */
 | 
			
		||||
    external fun removeMod(programId: String, name: String)
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Gets the save location for a specific game
 | 
			
		||||
@@ -609,15 +634,4 @@ object NativeLibrary {
 | 
			
		||||
        const val RELEASED = 0
 | 
			
		||||
        const val PRESSED = 1
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Result from installFileToNand
 | 
			
		||||
     */
 | 
			
		||||
    object InstallFileToNandResult {
 | 
			
		||||
        const val Success = 0
 | 
			
		||||
        const val SuccessFileOverwritten = 1
 | 
			
		||||
        const val Error = 2
 | 
			
		||||
        const val ErrorBaseGame = 3
 | 
			
		||||
        const val ErrorFilenameExtension = 4
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,27 +6,32 @@ package org.yuzu.yuzu_emu.adapters
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding
 | 
			
		||||
import org.yuzu.yuzu_emu.model.Addon
 | 
			
		||||
import org.yuzu.yuzu_emu.model.Patch
 | 
			
		||||
import org.yuzu.yuzu_emu.model.AddonViewModel
 | 
			
		||||
import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
 | 
			
		||||
 | 
			
		||||
class AddonAdapter : AbstractDiffAdapter<Addon, AddonAdapter.AddonViewHolder>() {
 | 
			
		||||
class AddonAdapter(val addonViewModel: AddonViewModel) :
 | 
			
		||||
    AbstractDiffAdapter<Patch, AddonAdapter.AddonViewHolder>() {
 | 
			
		||||
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder {
 | 
			
		||||
        ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
 | 
			
		||||
            .also { return AddonViewHolder(it) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inner class AddonViewHolder(val binding: ListItemAddonBinding) :
 | 
			
		||||
        AbstractViewHolder<Addon>(binding) {
 | 
			
		||||
        override fun bind(model: Addon) {
 | 
			
		||||
        AbstractViewHolder<Patch>(binding) {
 | 
			
		||||
        override fun bind(model: Patch) {
 | 
			
		||||
            binding.root.setOnClickListener {
 | 
			
		||||
                binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked
 | 
			
		||||
                binding.addonCheckbox.isChecked = !binding.addonCheckbox.isChecked
 | 
			
		||||
            }
 | 
			
		||||
            binding.title.text = model.title
 | 
			
		||||
            binding.title.text = model.name
 | 
			
		||||
            binding.version.text = model.version
 | 
			
		||||
            binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
 | 
			
		||||
            binding.addonCheckbox.setOnCheckedChangeListener { _, checked ->
 | 
			
		||||
                model.enabled = checked
 | 
			
		||||
            }
 | 
			
		||||
            binding.addonSwitch.isChecked = model.enabled
 | 
			
		||||
            binding.addonCheckbox.isChecked = model.enabled
 | 
			
		||||
            binding.buttonDelete.setOnClickListener {
 | 
			
		||||
                addonViewModel.setAddonToDelete(model)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -74,7 +74,7 @@ class AddonsFragment : Fragment() {
 | 
			
		||||
 | 
			
		||||
        binding.listAddons.apply {
 | 
			
		||||
            layoutManager = LinearLayoutManager(requireContext())
 | 
			
		||||
            adapter = AddonAdapter()
 | 
			
		||||
            adapter = AddonAdapter(addonViewModel)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        viewLifecycleOwner.lifecycleScope.apply {
 | 
			
		||||
@@ -110,6 +110,21 @@ class AddonsFragment : Fragment() {
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            launch {
 | 
			
		||||
                repeatOnLifecycle(Lifecycle.State.STARTED) {
 | 
			
		||||
                    addonViewModel.addonToDelete.collect {
 | 
			
		||||
                        if (it != null) {
 | 
			
		||||
                            MessageDialogFragment.newInstance(
 | 
			
		||||
                                requireActivity(),
 | 
			
		||||
                                titleId = R.string.confirm_uninstall,
 | 
			
		||||
                                descriptionId = R.string.confirm_uninstall_description,
 | 
			
		||||
                                positiveAction = { addonViewModel.onDeleteAddon(it) }
 | 
			
		||||
                            ).show(parentFragmentManager, MessageDialogFragment.TAG)
 | 
			
		||||
                            addonViewModel.setAddonToDelete(null)
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.buttonInstall.setOnClickListener {
 | 
			
		||||
@@ -156,22 +171,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)
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.model
 | 
			
		||||
 | 
			
		||||
data class Addon(
 | 
			
		||||
    var enabled: Boolean,
 | 
			
		||||
    val title: String,
 | 
			
		||||
    val version: String
 | 
			
		||||
)
 | 
			
		||||
@@ -15,8 +15,8 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
 | 
			
		||||
import java.util.concurrent.atomic.AtomicBoolean
 | 
			
		||||
 | 
			
		||||
class AddonViewModel : ViewModel() {
 | 
			
		||||
    private val _addonList = MutableStateFlow(mutableListOf<Addon>())
 | 
			
		||||
    val addonList get() = _addonList.asStateFlow()
 | 
			
		||||
    private val _patchList = MutableStateFlow(mutableListOf<Patch>())
 | 
			
		||||
    val addonList get() = _patchList.asStateFlow()
 | 
			
		||||
 | 
			
		||||
    private val _showModInstallPicker = MutableStateFlow(false)
 | 
			
		||||
    val showModInstallPicker get() = _showModInstallPicker.asStateFlow()
 | 
			
		||||
@@ -24,6 +24,9 @@ class AddonViewModel : ViewModel() {
 | 
			
		||||
    private val _showModNoticeDialog = MutableStateFlow(false)
 | 
			
		||||
    val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow()
 | 
			
		||||
 | 
			
		||||
    private val _addonToDelete = MutableStateFlow<Patch?>(null)
 | 
			
		||||
    val addonToDelete = _addonToDelete.asStateFlow()
 | 
			
		||||
 | 
			
		||||
    var game: Game? = null
 | 
			
		||||
 | 
			
		||||
    private val isRefreshing = AtomicBoolean(false)
 | 
			
		||||
@@ -40,36 +43,47 @@ class AddonViewModel : ViewModel() {
 | 
			
		||||
        isRefreshing.set(true)
 | 
			
		||||
        viewModelScope.launch {
 | 
			
		||||
            withContext(Dispatchers.IO) {
 | 
			
		||||
                val addonList = mutableListOf<Addon>()
 | 
			
		||||
                val disabledAddons = NativeConfig.getDisabledAddons(game!!.programId)
 | 
			
		||||
                NativeLibrary.getAddonsForFile(game!!.path, game!!.programId)?.forEach {
 | 
			
		||||
                    val name = it.first.replace("[D] ", "")
 | 
			
		||||
                    addonList.add(Addon(!disabledAddons.contains(name), name, it.second))
 | 
			
		||||
                }
 | 
			
		||||
                addonList.sortBy { it.title }
 | 
			
		||||
                _addonList.value = addonList
 | 
			
		||||
                val patchList = (
 | 
			
		||||
                    NativeLibrary.getPatchesForFile(game!!.path, game!!.programId)
 | 
			
		||||
                        ?: emptyArray()
 | 
			
		||||
                    ).toMutableList()
 | 
			
		||||
                patchList.sortBy { it.name }
 | 
			
		||||
                _patchList.value = patchList
 | 
			
		||||
                isRefreshing.set(false)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setAddonToDelete(patch: Patch?) {
 | 
			
		||||
        _addonToDelete.value = patch
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onDeleteAddon(patch: Patch) {
 | 
			
		||||
        when (PatchType.from(patch.type)) {
 | 
			
		||||
            PatchType.Update -> NativeLibrary.removeUpdate(patch.programId)
 | 
			
		||||
            PatchType.DLC -> NativeLibrary.removeDLC(patch.programId)
 | 
			
		||||
            PatchType.Mod -> NativeLibrary.removeMod(patch.programId, patch.name)
 | 
			
		||||
        }
 | 
			
		||||
        refreshAddons()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onCloseAddons() {
 | 
			
		||||
        if (_addonList.value.isEmpty()) {
 | 
			
		||||
        if (_patchList.value.isEmpty()) {
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        NativeConfig.setDisabledAddons(
 | 
			
		||||
            game!!.programId,
 | 
			
		||||
            _addonList.value.mapNotNull {
 | 
			
		||||
            _patchList.value.mapNotNull {
 | 
			
		||||
                if (it.enabled) {
 | 
			
		||||
                    null
 | 
			
		||||
                } else {
 | 
			
		||||
                    it.title
 | 
			
		||||
                    it.name
 | 
			
		||||
                }
 | 
			
		||||
            }.toTypedArray()
 | 
			
		||||
        )
 | 
			
		||||
        NativeConfig.saveGlobalConfig()
 | 
			
		||||
        _addonList.value.clear()
 | 
			
		||||
        _patchList.value.clear()
 | 
			
		||||
        game = null
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,15 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.model
 | 
			
		||||
 | 
			
		||||
enum class InstallResult(val int: Int) {
 | 
			
		||||
    Success(0),
 | 
			
		||||
    Overwrite(1),
 | 
			
		||||
    Failure(2),
 | 
			
		||||
    BaseInstallAttempted(3);
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun from(int: Int): InstallResult = entries.firstOrNull { it.int == int } ?: Success
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,16 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.model
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.Keep
 | 
			
		||||
 | 
			
		||||
@Keep
 | 
			
		||||
data class Patch(
 | 
			
		||||
    var enabled: Boolean,
 | 
			
		||||
    val name: String,
 | 
			
		||||
    val version: String,
 | 
			
		||||
    val type: Int,
 | 
			
		||||
    val programId: String,
 | 
			
		||||
    val titleId: String
 | 
			
		||||
)
 | 
			
		||||
@@ -0,0 +1,14 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
package org.yuzu.yuzu_emu.model
 | 
			
		||||
 | 
			
		||||
enum class PatchType(val int: Int) {
 | 
			
		||||
    Update(0),
 | 
			
		||||
    DLC(1),
 | 
			
		||||
    Mod(2);
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
        fun from(int: Int): PatchType = entries.firstOrNull { it.int == int } ?: Update
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -42,3 +42,19 @@ double GetJDouble(JNIEnv* env, jobject jdouble) {
 | 
			
		||||
jobject ToJDouble(JNIEnv* env, double value) {
 | 
			
		||||
    return env->NewObject(IDCache::GetDoubleClass(), IDCache::GetDoubleConstructor(), value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
s32 GetJInteger(JNIEnv* env, jobject jinteger) {
 | 
			
		||||
    return env->GetIntField(jinteger, IDCache::GetIntegerValueField());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jobject ToJInteger(JNIEnv* env, s32 value) {
 | 
			
		||||
    return env->NewObject(IDCache::GetIntegerClass(), IDCache::GetIntegerConstructor(), value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool GetJBoolean(JNIEnv* env, jobject jboolean) {
 | 
			
		||||
    return env->GetBooleanField(jboolean, IDCache::GetBooleanValueField());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jobject ToJBoolean(JNIEnv* env, bool value) {
 | 
			
		||||
    return env->NewObject(IDCache::GetBooleanClass(), IDCache::GetBooleanConstructor(), value);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
#include <jni.h>
 | 
			
		||||
#include "common/common_types.h"
 | 
			
		||||
 | 
			
		||||
std::string GetJString(JNIEnv* env, jstring jstr);
 | 
			
		||||
jstring ToJString(JNIEnv* env, std::string_view str);
 | 
			
		||||
@@ -13,3 +14,9 @@ jstring ToJString(JNIEnv* env, std::u16string_view str);
 | 
			
		||||
 | 
			
		||||
double GetJDouble(JNIEnv* env, jobject jdouble);
 | 
			
		||||
jobject ToJDouble(JNIEnv* env, double value);
 | 
			
		||||
 | 
			
		||||
s32 GetJInteger(JNIEnv* env, jobject jinteger);
 | 
			
		||||
jobject ToJInteger(JNIEnv* env, s32 value);
 | 
			
		||||
 | 
			
		||||
bool GetJBoolean(JNIEnv* env, jobject jboolean);
 | 
			
		||||
jobject ToJBoolean(JNIEnv* env, bool value);
 | 
			
		||||
 
 | 
			
		||||
@@ -43,10 +43,27 @@ static jfieldID s_overlay_control_data_landscape_position_field;
 | 
			
		||||
static jfieldID s_overlay_control_data_portrait_position_field;
 | 
			
		||||
static jfieldID s_overlay_control_data_foldable_position_field;
 | 
			
		||||
 | 
			
		||||
static jclass s_patch_class;
 | 
			
		||||
static jmethodID s_patch_constructor;
 | 
			
		||||
static jfieldID s_patch_enabled_field;
 | 
			
		||||
static jfieldID s_patch_name_field;
 | 
			
		||||
static jfieldID s_patch_version_field;
 | 
			
		||||
static jfieldID s_patch_type_field;
 | 
			
		||||
static jfieldID s_patch_program_id_field;
 | 
			
		||||
static jfieldID s_patch_title_id_field;
 | 
			
		||||
 | 
			
		||||
static jclass s_double_class;
 | 
			
		||||
static jmethodID s_double_constructor;
 | 
			
		||||
static jfieldID s_double_value_field;
 | 
			
		||||
 | 
			
		||||
static jclass s_integer_class;
 | 
			
		||||
static jmethodID s_integer_constructor;
 | 
			
		||||
static jfieldID s_integer_value_field;
 | 
			
		||||
 | 
			
		||||
static jclass s_boolean_class;
 | 
			
		||||
static jmethodID s_boolean_constructor;
 | 
			
		||||
static jfieldID s_boolean_value_field;
 | 
			
		||||
 | 
			
		||||
static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
 | 
			
		||||
 | 
			
		||||
namespace IDCache {
 | 
			
		||||
@@ -186,6 +203,38 @@ jfieldID GetOverlayControlDataFoldablePositionField() {
 | 
			
		||||
    return s_overlay_control_data_foldable_position_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jclass GetPatchClass() {
 | 
			
		||||
    return s_patch_class;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jmethodID GetPatchConstructor() {
 | 
			
		||||
    return s_patch_constructor;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPatchEnabledField() {
 | 
			
		||||
    return s_patch_enabled_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPatchNameField() {
 | 
			
		||||
    return s_patch_name_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPatchVersionField() {
 | 
			
		||||
    return s_patch_version_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPatchTypeField() {
 | 
			
		||||
    return s_patch_type_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPatchProgramIdField() {
 | 
			
		||||
    return s_patch_program_id_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetPatchTitleIdField() {
 | 
			
		||||
    return s_patch_title_id_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jclass GetDoubleClass() {
 | 
			
		||||
    return s_double_class;
 | 
			
		||||
}
 | 
			
		||||
@@ -198,6 +247,30 @@ jfieldID GetDoubleValueField() {
 | 
			
		||||
    return s_double_value_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jclass GetIntegerClass() {
 | 
			
		||||
    return s_integer_class;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jmethodID GetIntegerConstructor() {
 | 
			
		||||
    return s_integer_constructor;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetIntegerValueField() {
 | 
			
		||||
    return s_integer_value_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jclass GetBooleanClass() {
 | 
			
		||||
    return s_boolean_class;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jmethodID GetBooleanConstructor() {
 | 
			
		||||
    return s_boolean_constructor;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jfieldID GetBooleanValueField() {
 | 
			
		||||
    return s_boolean_value_field;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace IDCache
 | 
			
		||||
 | 
			
		||||
#ifdef __cplusplus
 | 
			
		||||
@@ -278,12 +351,37 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
 | 
			
		||||
        env->GetFieldID(overlay_control_data_class, "foldablePosition", "Lkotlin/Pair;");
 | 
			
		||||
    env->DeleteLocalRef(overlay_control_data_class);
 | 
			
		||||
 | 
			
		||||
    const jclass patch_class = env->FindClass("org/yuzu/yuzu_emu/model/Patch");
 | 
			
		||||
    s_patch_class = reinterpret_cast<jclass>(env->NewGlobalRef(patch_class));
 | 
			
		||||
    s_patch_constructor = env->GetMethodID(
 | 
			
		||||
        patch_class, "<init>",
 | 
			
		||||
        "(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)V");
 | 
			
		||||
    s_patch_enabled_field = env->GetFieldID(patch_class, "enabled", "Z");
 | 
			
		||||
    s_patch_name_field = env->GetFieldID(patch_class, "name", "Ljava/lang/String;");
 | 
			
		||||
    s_patch_version_field = env->GetFieldID(patch_class, "version", "Ljava/lang/String;");
 | 
			
		||||
    s_patch_type_field = env->GetFieldID(patch_class, "type", "I");
 | 
			
		||||
    s_patch_program_id_field = env->GetFieldID(patch_class, "programId", "Ljava/lang/String;");
 | 
			
		||||
    s_patch_title_id_field = env->GetFieldID(patch_class, "titleId", "Ljava/lang/String;");
 | 
			
		||||
    env->DeleteLocalRef(patch_class);
 | 
			
		||||
 | 
			
		||||
    const jclass double_class = env->FindClass("java/lang/Double");
 | 
			
		||||
    s_double_class = reinterpret_cast<jclass>(env->NewGlobalRef(double_class));
 | 
			
		||||
    s_double_constructor = env->GetMethodID(double_class, "<init>", "(D)V");
 | 
			
		||||
    s_double_value_field = env->GetFieldID(double_class, "value", "D");
 | 
			
		||||
    env->DeleteLocalRef(double_class);
 | 
			
		||||
 | 
			
		||||
    const jclass int_class = env->FindClass("java/lang/Integer");
 | 
			
		||||
    s_integer_class = reinterpret_cast<jclass>(env->NewGlobalRef(int_class));
 | 
			
		||||
    s_integer_constructor = env->GetMethodID(int_class, "<init>", "(I)V");
 | 
			
		||||
    s_integer_value_field = env->GetFieldID(int_class, "value", "I");
 | 
			
		||||
    env->DeleteLocalRef(int_class);
 | 
			
		||||
 | 
			
		||||
    const jclass boolean_class = env->FindClass("java/lang/Boolean");
 | 
			
		||||
    s_boolean_class = reinterpret_cast<jclass>(env->NewGlobalRef(boolean_class));
 | 
			
		||||
    s_boolean_constructor = env->GetMethodID(boolean_class, "<init>", "(Z)V");
 | 
			
		||||
    s_boolean_value_field = env->GetFieldID(boolean_class, "value", "Z");
 | 
			
		||||
    env->DeleteLocalRef(boolean_class);
 | 
			
		||||
 | 
			
		||||
    // Initialize Android Storage
 | 
			
		||||
    Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
 | 
			
		||||
 | 
			
		||||
@@ -309,7 +407,10 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
 | 
			
		||||
    env->DeleteGlobalRef(s_string_class);
 | 
			
		||||
    env->DeleteGlobalRef(s_pair_class);
 | 
			
		||||
    env->DeleteGlobalRef(s_overlay_control_data_class);
 | 
			
		||||
    env->DeleteGlobalRef(s_patch_class);
 | 
			
		||||
    env->DeleteGlobalRef(s_double_class);
 | 
			
		||||
    env->DeleteGlobalRef(s_integer_class);
 | 
			
		||||
    env->DeleteGlobalRef(s_boolean_class);
 | 
			
		||||
 | 
			
		||||
    // UnInitialize applets
 | 
			
		||||
    SoftwareKeyboard::CleanupJNI(env);
 | 
			
		||||
 
 | 
			
		||||
@@ -43,8 +43,25 @@ jfieldID GetOverlayControlDataLandscapePositionField();
 | 
			
		||||
jfieldID GetOverlayControlDataPortraitPositionField();
 | 
			
		||||
jfieldID GetOverlayControlDataFoldablePositionField();
 | 
			
		||||
 | 
			
		||||
jclass GetPatchClass();
 | 
			
		||||
jmethodID GetPatchConstructor();
 | 
			
		||||
jfieldID GetPatchEnabledField();
 | 
			
		||||
jfieldID GetPatchNameField();
 | 
			
		||||
jfieldID GetPatchVersionField();
 | 
			
		||||
jfieldID GetPatchTypeField();
 | 
			
		||||
jfieldID GetPatchProgramIdField();
 | 
			
		||||
jfieldID GetPatchTitleIdField();
 | 
			
		||||
 | 
			
		||||
jclass GetDoubleClass();
 | 
			
		||||
jmethodID GetDoubleConstructor();
 | 
			
		||||
jfieldID GetDoubleValueField();
 | 
			
		||||
 | 
			
		||||
jclass GetIntegerClass();
 | 
			
		||||
jmethodID GetIntegerConstructor();
 | 
			
		||||
jfieldID GetIntegerValueField();
 | 
			
		||||
 | 
			
		||||
jclass GetBooleanClass();
 | 
			
		||||
jmethodID GetBooleanConstructor();
 | 
			
		||||
jfieldID GetBooleanValueField();
 | 
			
		||||
 | 
			
		||||
} // namespace IDCache
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@
 | 
			
		||||
#include <core/file_sys/patch_manager.h>
 | 
			
		||||
#include <core/file_sys/savedata_factory.h>
 | 
			
		||||
#include <core/loader/nro.h>
 | 
			
		||||
#include <frontend_common/content_manager.h>
 | 
			
		||||
#include <jni.h>
 | 
			
		||||
 | 
			
		||||
#include "common/detached_tasks.h"
 | 
			
		||||
@@ -100,67 +101,6 @@ void EmulationSession::SetNativeWindow(ANativeWindow* native_window) {
 | 
			
		||||
    m_native_window = native_window;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int EmulationSession::InstallFileToNand(std::string filename, std::string file_extension) {
 | 
			
		||||
    jconst copy_func = [](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
 | 
			
		||||
                          std::size_t block_size) {
 | 
			
		||||
        if (src == nullptr || dest == nullptr) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        if (!dest->Resize(src->GetSize())) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        using namespace Common::Literals;
 | 
			
		||||
        [[maybe_unused]] std::vector<u8> buffer(1_MiB);
 | 
			
		||||
 | 
			
		||||
        for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
 | 
			
		||||
            jconst read = src->Read(buffer.data(), buffer.size(), i);
 | 
			
		||||
            dest->Write(buffer.data(), read, i);
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    enum InstallResult {
 | 
			
		||||
        Success = 0,
 | 
			
		||||
        SuccessFileOverwritten = 1,
 | 
			
		||||
        InstallError = 2,
 | 
			
		||||
        ErrorBaseGame = 3,
 | 
			
		||||
        ErrorFilenameExtension = 4,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    [[maybe_unused]] std::shared_ptr<FileSys::NSP> nsp;
 | 
			
		||||
    if (file_extension == "nsp") {
 | 
			
		||||
        nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read));
 | 
			
		||||
        if (nsp->IsExtractedType()) {
 | 
			
		||||
            return InstallError;
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        return ErrorFilenameExtension;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!nsp) {
 | 
			
		||||
        return InstallError;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (nsp->GetStatus() != Loader::ResultStatus::Success) {
 | 
			
		||||
        return InstallError;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    jconst res = m_system.GetFileSystemController().GetUserNANDContents()->InstallEntry(*nsp, true,
 | 
			
		||||
                                                                                        copy_func);
 | 
			
		||||
 | 
			
		||||
    switch (res) {
 | 
			
		||||
    case FileSys::InstallResult::Success:
 | 
			
		||||
        return Success;
 | 
			
		||||
    case FileSys::InstallResult::OverwriteExisting:
 | 
			
		||||
        return SuccessFileOverwritten;
 | 
			
		||||
    case FileSys::InstallResult::ErrorBaseInstall:
 | 
			
		||||
        return ErrorBaseGame;
 | 
			
		||||
    default:
 | 
			
		||||
        return InstallError;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EmulationSession::InitializeGpuDriver(const std::string& hook_lib_dir,
 | 
			
		||||
                                           const std::string& custom_driver_dir,
 | 
			
		||||
                                           const std::string& custom_driver_name,
 | 
			
		||||
@@ -512,10 +452,20 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jobject
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject instance,
 | 
			
		||||
                                                            jstring j_file,
 | 
			
		||||
                                                            jstring j_file_extension) {
 | 
			
		||||
    return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file),
 | 
			
		||||
                                                             GetJString(env, j_file_extension));
 | 
			
		||||
                                                            jstring j_file, jobject jcallback) {
 | 
			
		||||
    auto jlambdaClass = env->GetObjectClass(jcallback);
 | 
			
		||||
    auto jlambdaInvokeMethod = env->GetMethodID(
 | 
			
		||||
        jlambdaClass, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
 | 
			
		||||
    const auto callback = [env, jcallback, jlambdaInvokeMethod](size_t max, size_t progress) {
 | 
			
		||||
        auto jwasCancelled = env->CallObjectMethod(jcallback, jlambdaInvokeMethod,
 | 
			
		||||
                                                   ToJDouble(env, max), ToJDouble(env, progress));
 | 
			
		||||
        return GetJBoolean(env, jwasCancelled);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return static_cast<int>(
 | 
			
		||||
        ContentManager::InstallNSP(&EmulationSession::GetInstance().System(),
 | 
			
		||||
                                   EmulationSession::GetInstance().System().GetFilesystem().get(),
 | 
			
		||||
                                   GetJString(env, j_file), callback));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj,
 | 
			
		||||
@@ -824,9 +774,9 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env,
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, jobject jobj,
 | 
			
		||||
                                                                    jstring jpath,
 | 
			
		||||
                                                                    jstring jprogramId) {
 | 
			
		||||
jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env, jobject jobj,
 | 
			
		||||
                                                                     jstring jpath,
 | 
			
		||||
                                                                     jstring jprogramId) {
 | 
			
		||||
    const auto path = GetJString(env, jpath);
 | 
			
		||||
    const auto vFile =
 | 
			
		||||
        Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path);
 | 
			
		||||
@@ -843,20 +793,40 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env,
 | 
			
		||||
    FileSys::VirtualFile update_raw;
 | 
			
		||||
    loader->ReadUpdateRaw(update_raw);
 | 
			
		||||
 | 
			
		||||
    auto addons = pm.GetPatchVersionNames(update_raw);
 | 
			
		||||
    auto jemptyString = ToJString(env, "");
 | 
			
		||||
    auto jemptyStringPair = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(),
 | 
			
		||||
                                           jemptyString, jemptyString);
 | 
			
		||||
    jobjectArray jaddonsArray =
 | 
			
		||||
        env->NewObjectArray(addons.size(), IDCache::GetPairClass(), jemptyStringPair);
 | 
			
		||||
    auto patches = pm.GetPatches(update_raw);
 | 
			
		||||
    jobjectArray jpatchArray =
 | 
			
		||||
        env->NewObjectArray(patches.size(), IDCache::GetPatchClass(), nullptr);
 | 
			
		||||
    int i = 0;
 | 
			
		||||
    for (const auto& addon : addons) {
 | 
			
		||||
        jobject jaddon = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(),
 | 
			
		||||
                                        ToJString(env, addon.first), ToJString(env, addon.second));
 | 
			
		||||
        env->SetObjectArrayElement(jaddonsArray, i, jaddon);
 | 
			
		||||
    for (const auto& patch : patches) {
 | 
			
		||||
        jobject jpatch = env->NewObject(
 | 
			
		||||
            IDCache::GetPatchClass(), IDCache::GetPatchConstructor(), patch.enabled,
 | 
			
		||||
            ToJString(env, patch.name), ToJString(env, patch.version),
 | 
			
		||||
            static_cast<jint>(patch.type), ToJString(env, std::to_string(patch.program_id)),
 | 
			
		||||
            ToJString(env, std::to_string(patch.title_id)));
 | 
			
		||||
        env->SetObjectArrayElement(jpatchArray, i, jpatch);
 | 
			
		||||
        ++i;
 | 
			
		||||
    }
 | 
			
		||||
    return jaddonsArray;
 | 
			
		||||
    return jpatchArray;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeUpdate(JNIEnv* env, jobject jobj,
 | 
			
		||||
                                                        jstring jprogramId) {
 | 
			
		||||
    auto program_id = EmulationSession::GetProgramId(env, jprogramId);
 | 
			
		||||
    ContentManager::RemoveUpdate(EmulationSession::GetInstance().System().GetFileSystemController(),
 | 
			
		||||
                                 program_id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeDLC(JNIEnv* env, jobject jobj,
 | 
			
		||||
                                                     jstring jprogramId) {
 | 
			
		||||
    auto program_id = EmulationSession::GetProgramId(env, jprogramId);
 | 
			
		||||
    ContentManager::RemoveAllDLC(&EmulationSession::GetInstance().System(), program_id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeMod(JNIEnv* env, jobject jobj, jstring jprogramId,
 | 
			
		||||
                                                     jstring jname) {
 | 
			
		||||
    auto program_id = EmulationSession::GetProgramId(env, jprogramId);
 | 
			
		||||
    ContentManager::RemoveMod(EmulationSession::GetInstance().System().GetFileSystemController(),
 | 
			
		||||
                              program_id, GetJString(env, jname));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj,
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@
 | 
			
		||||
#include "core/file_sys/registered_cache.h"
 | 
			
		||||
#include "core/hle/service/acc/profile_manager.h"
 | 
			
		||||
#include "core/perf_stats.h"
 | 
			
		||||
#include "frontend_common/content_manager.h"
 | 
			
		||||
#include "jni/applets/software_keyboard.h"
 | 
			
		||||
#include "jni/emu_window/emu_window.h"
 | 
			
		||||
#include "video_core/rasterizer_interface.h"
 | 
			
		||||
@@ -29,7 +30,6 @@ public:
 | 
			
		||||
    void SetNativeWindow(ANativeWindow* native_window);
 | 
			
		||||
    void SurfaceChanged();
 | 
			
		||||
 | 
			
		||||
    int InstallFileToNand(std::string filename, std::string file_extension);
 | 
			
		||||
    void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir,
 | 
			
		||||
                             const std::string& custom_driver_name,
 | 
			
		||||
                             const std::string& file_redirect_dir);
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -14,12 +14,11 @@
 | 
			
		||||
        android:id="@+id/text_container"
 | 
			
		||||
        android:layout_width="0dp"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_marginEnd="16dp"
 | 
			
		||||
        android:orientation="vertical"
 | 
			
		||||
        app:layout_constraintBottom_toBottomOf="@+id/addon_switch"
 | 
			
		||||
        app:layout_constraintEnd_toStartOf="@+id/addon_switch"
 | 
			
		||||
        android:layout_marginEnd="16dp"
 | 
			
		||||
        app:layout_constraintEnd_toStartOf="@+id/addon_checkbox"
 | 
			
		||||
        app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
        app:layout_constraintTop_toTopOf="@+id/addon_switch">
 | 
			
		||||
        app:layout_constraintTop_toTopOf="parent">
 | 
			
		||||
 | 
			
		||||
        <com.google.android.material.textview.MaterialTextView
 | 
			
		||||
            android:id="@+id/title"
 | 
			
		||||
@@ -42,16 +41,29 @@
 | 
			
		||||
 | 
			
		||||
    </LinearLayout>
 | 
			
		||||
 | 
			
		||||
    <com.google.android.material.materialswitch.MaterialSwitch
 | 
			
		||||
        android:id="@+id/addon_switch"
 | 
			
		||||
    <com.google.android.material.checkbox.MaterialCheckBox
 | 
			
		||||
        android:id="@+id/addon_checkbox"
 | 
			
		||||
        android:layout_width="wrap_content"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:focusable="true"
 | 
			
		||||
        android:gravity="center"
 | 
			
		||||
        android:nextFocusLeft="@id/addon_container"
 | 
			
		||||
        app:layout_constraintBottom_toBottomOf="parent"
 | 
			
		||||
        android:layout_marginEnd="8dp"
 | 
			
		||||
        app:layout_constraintTop_toTopOf="@+id/text_container"
 | 
			
		||||
        app:layout_constraintBottom_toBottomOf="@+id/text_container"
 | 
			
		||||
        app:layout_constraintEnd_toStartOf="@+id/button_delete" />
 | 
			
		||||
 | 
			
		||||
    <Button
 | 
			
		||||
        android:id="@+id/button_delete"
 | 
			
		||||
        style="@style/Widget.Material3.Button.IconButton"
 | 
			
		||||
        android:layout_width="wrap_content"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_gravity="center_vertical"
 | 
			
		||||
        android:contentDescription="@string/delete"
 | 
			
		||||
        android:tooltipText="@string/delete"
 | 
			
		||||
        app:icon="@drawable/ic_delete"
 | 
			
		||||
        app:iconTint="?attr/colorControlNormal"
 | 
			
		||||
        app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
        app:layout_constraintStart_toEndOf="@id/text_container"
 | 
			
		||||
        app:layout_constraintTop_toTopOf="parent" />
 | 
			
		||||
        app:layout_constraintTop_toTopOf="@+id/addon_checkbox"
 | 
			
		||||
        app:layout_constraintBottom_toBottomOf="@+id/addon_checkbox" />
 | 
			
		||||
 | 
			
		||||
</androidx.constraintlayout.widget.ConstraintLayout>
 | 
			
		||||
 
 | 
			
		||||
@@ -286,6 +286,7 @@
 | 
			
		||||
    <string name="custom">Custom</string>
 | 
			
		||||
    <string name="notice">Notice</string>
 | 
			
		||||
    <string name="import_complete">Import complete</string>
 | 
			
		||||
    <string name="more_options">More options</string>
 | 
			
		||||
 | 
			
		||||
    <!-- GPU driver installation -->
 | 
			
		||||
    <string name="select_gpu_driver">Select GPU driver</string>
 | 
			
		||||
@@ -348,6 +349,8 @@
 | 
			
		||||
    <string name="verifying_content">Verifying content…</string>
 | 
			
		||||
    <string name="content_install_notice">Content install notice</string>
 | 
			
		||||
    <string name="content_install_notice_description">The content that you selected does not match this game.\nInstall anyway?</string>
 | 
			
		||||
    <string name="confirm_uninstall">Confirm uninstall</string>
 | 
			
		||||
    <string name="confirm_uninstall_description">Are you sure you want to uninstall this addon?</string>
 | 
			
		||||
 | 
			
		||||
    <!-- ROM loading errors -->
 | 
			
		||||
    <string name="loader_error_encrypted">Your ROM is encrypted</string>
 | 
			
		||||
 
 | 
			
		||||
@@ -466,12 +466,12 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
 | 
			
		||||
    return romfs;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile update_raw) const {
 | 
			
		||||
std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
 | 
			
		||||
    if (title_id == 0) {
 | 
			
		||||
        return {};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    std::map<std::string, std::string, std::less<>> out;
 | 
			
		||||
    std::vector<Patch> out;
 | 
			
		||||
    const auto& disabled = Settings::values.disabled_addons[title_id];
 | 
			
		||||
 | 
			
		||||
    // Game Updates
 | 
			
		||||
@@ -482,20 +482,28 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u
 | 
			
		||||
 | 
			
		||||
    const auto update_disabled =
 | 
			
		||||
        std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
 | 
			
		||||
    const auto update_label = update_disabled ? "[D] Update" : "Update";
 | 
			
		||||
    Patch update_patch = {.enabled = !update_disabled,
 | 
			
		||||
                          .name = "Update",
 | 
			
		||||
                          .version = "",
 | 
			
		||||
                          .type = PatchType::Update,
 | 
			
		||||
                          .program_id = title_id,
 | 
			
		||||
                          .title_id = title_id};
 | 
			
		||||
 | 
			
		||||
    if (nacp != nullptr) {
 | 
			
		||||
        out.insert_or_assign(update_label, nacp->GetVersionString());
 | 
			
		||||
        update_patch.version = nacp->GetVersionString();
 | 
			
		||||
        out.push_back(update_patch);
 | 
			
		||||
    } else {
 | 
			
		||||
        if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
 | 
			
		||||
            const auto meta_ver = content_provider.GetEntryVersion(update_tid);
 | 
			
		||||
            if (meta_ver.value_or(0) == 0) {
 | 
			
		||||
                out.insert_or_assign(update_label, "");
 | 
			
		||||
                out.push_back(update_patch);
 | 
			
		||||
            } else {
 | 
			
		||||
                out.insert_or_assign(update_label, FormatTitleVersion(*meta_ver));
 | 
			
		||||
                update_patch.version = FormatTitleVersion(*meta_ver);
 | 
			
		||||
                out.push_back(update_patch);
 | 
			
		||||
            }
 | 
			
		||||
        } else if (update_raw != nullptr) {
 | 
			
		||||
            out.insert_or_assign(update_label, "PACKED");
 | 
			
		||||
            update_patch.version = "PACKED";
 | 
			
		||||
            out.push_back(update_patch);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -539,7 +547,12 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u
 | 
			
		||||
 | 
			
		||||
            const auto mod_disabled =
 | 
			
		||||
                std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end();
 | 
			
		||||
            out.insert_or_assign(mod_disabled ? "[D] " + mod->GetName() : mod->GetName(), types);
 | 
			
		||||
            out.push_back({.enabled = !mod_disabled,
 | 
			
		||||
                           .name = mod->GetName(),
 | 
			
		||||
                           .version = types,
 | 
			
		||||
                           .type = PatchType::Mod,
 | 
			
		||||
                           .program_id = title_id,
 | 
			
		||||
                           .title_id = title_id});
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -557,7 +570,12 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u
 | 
			
		||||
        if (!types.empty()) {
 | 
			
		||||
            const auto mod_disabled =
 | 
			
		||||
                std::find(disabled.begin(), disabled.end(), "SDMC") != disabled.end();
 | 
			
		||||
            out.insert_or_assign(mod_disabled ? "[D] SDMC" : "SDMC", types);
 | 
			
		||||
            out.push_back({.enabled = !mod_disabled,
 | 
			
		||||
                           .name = "SDMC",
 | 
			
		||||
                           .version = types,
 | 
			
		||||
                           .type = PatchType::Mod,
 | 
			
		||||
                           .program_id = title_id,
 | 
			
		||||
                           .title_id = title_id});
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -584,7 +602,12 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u
 | 
			
		||||
 | 
			
		||||
        const auto dlc_disabled =
 | 
			
		||||
            std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end();
 | 
			
		||||
        out.insert_or_assign(dlc_disabled ? "[D] DLC" : "DLC", std::move(list));
 | 
			
		||||
        out.push_back({.enabled = !dlc_disabled,
 | 
			
		||||
                       .name = "DLC",
 | 
			
		||||
                       .version = std::move(list),
 | 
			
		||||
                       .type = PatchType::DLC,
 | 
			
		||||
                       .program_id = title_id,
 | 
			
		||||
                       .title_id = dlc_match.back().title_id});
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return out;
 | 
			
		||||
 
 | 
			
		||||
@@ -26,12 +26,22 @@ class ContentProvider;
 | 
			
		||||
class NCA;
 | 
			
		||||
class NACP;
 | 
			
		||||
 | 
			
		||||
enum class PatchType { Update, DLC, Mod };
 | 
			
		||||
 | 
			
		||||
struct Patch {
 | 
			
		||||
    bool enabled;
 | 
			
		||||
    std::string name;
 | 
			
		||||
    std::string version;
 | 
			
		||||
    PatchType type;
 | 
			
		||||
    u64 program_id;
 | 
			
		||||
    u64 title_id;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// A centralized class to manage patches to games.
 | 
			
		||||
class PatchManager {
 | 
			
		||||
public:
 | 
			
		||||
    using BuildID = std::array<u8, 0x20>;
 | 
			
		||||
    using Metadata = std::pair<std::unique_ptr<NACP>, VirtualFile>;
 | 
			
		||||
    using PatchVersionNames = std::map<std::string, std::string, std::less<>>;
 | 
			
		||||
 | 
			
		||||
    explicit PatchManager(u64 title_id_,
 | 
			
		||||
                          const Service::FileSystem::FileSystemController& fs_controller_,
 | 
			
		||||
@@ -66,9 +76,8 @@ public:
 | 
			
		||||
                                         VirtualFile packed_update_raw = nullptr,
 | 
			
		||||
                                         bool apply_layeredfs = true) const;
 | 
			
		||||
 | 
			
		||||
    // Returns a vector of pairs between patch names and patch versions.
 | 
			
		||||
    // i.e. Update 3.2.2 will return {"Update", "3.2.2"}
 | 
			
		||||
    [[nodiscard]] PatchVersionNames GetPatchVersionNames(VirtualFile update_raw = nullptr) const;
 | 
			
		||||
    // Returns a vector of patches
 | 
			
		||||
    [[nodiscard]] std::vector<Patch> GetPatches(VirtualFile update_raw = nullptr) const;
 | 
			
		||||
 | 
			
		||||
    // If the game update exists, returns the u32 version field in its Meta-type NCA. If that fails,
 | 
			
		||||
    // it will fallback to the Meta-type NCA of the base game. If that fails, the result will be
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
add_library(frontend_common STATIC
 | 
			
		||||
    config.cpp
 | 
			
		||||
    config.h
 | 
			
		||||
    content_manager.h
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
create_target_directory_groups(frontend_common)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										238
									
								
								src/frontend_common/content_manager.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								src/frontend_common/content_manager.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,238 @@
 | 
			
		||||
// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
 | 
			
		||||
// SPDX-License-Identifier: GPL-2.0-or-later
 | 
			
		||||
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <boost/algorithm/string.hpp>
 | 
			
		||||
#include "common/common_types.h"
 | 
			
		||||
#include "common/literals.h"
 | 
			
		||||
#include "core/core.h"
 | 
			
		||||
#include "core/file_sys/common_funcs.h"
 | 
			
		||||
#include "core/file_sys/content_archive.h"
 | 
			
		||||
#include "core/file_sys/mode.h"
 | 
			
		||||
#include "core/file_sys/nca_metadata.h"
 | 
			
		||||
#include "core/file_sys/registered_cache.h"
 | 
			
		||||
#include "core/file_sys/submission_package.h"
 | 
			
		||||
#include "core/hle/service/filesystem/filesystem.h"
 | 
			
		||||
#include "core/loader/loader.h"
 | 
			
		||||
 | 
			
		||||
namespace ContentManager {
 | 
			
		||||
 | 
			
		||||
enum class InstallResult {
 | 
			
		||||
    Success,
 | 
			
		||||
    Overwrite,
 | 
			
		||||
    Failure,
 | 
			
		||||
    BaseInstallAttempted,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * \brief Removes a single installed DLC
 | 
			
		||||
 * \param fs_controller [FileSystemController] reference from the Core::System instance
 | 
			
		||||
 * \param title_id Unique title ID representing the DLC which will be removed
 | 
			
		||||
 * \return 'true' if successful
 | 
			
		||||
 */
 | 
			
		||||
inline bool RemoveDLC(const Service::FileSystem::FileSystemController& fs_controller,
 | 
			
		||||
                      const u64 title_id) {
 | 
			
		||||
    return fs_controller.GetUserNANDContents()->RemoveExistingEntry(title_id) ||
 | 
			
		||||
           fs_controller.GetSDMCContents()->RemoveExistingEntry(title_id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * \brief Removes all DLC for a game
 | 
			
		||||
 * \param system Raw pointer to the system instance
 | 
			
		||||
 * \param program_id Program ID for the game that will have all of its DLC removed
 | 
			
		||||
 * \return Number of DLC removed
 | 
			
		||||
 */
 | 
			
		||||
inline size_t RemoveAllDLC(Core::System* system, const u64 program_id) {
 | 
			
		||||
    size_t count{};
 | 
			
		||||
    const auto& fs_controller = system->GetFileSystemController();
 | 
			
		||||
    const auto dlc_entries = system->GetContentProvider().ListEntriesFilter(
 | 
			
		||||
        FileSys::TitleType::AOC, FileSys::ContentRecordType::Data);
 | 
			
		||||
    std::vector<u64> program_dlc_entries;
 | 
			
		||||
 | 
			
		||||
    for (const auto& entry : dlc_entries) {
 | 
			
		||||
        if (FileSys::GetBaseTitleID(entry.title_id) == program_id) {
 | 
			
		||||
            program_dlc_entries.push_back(entry.title_id);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const auto& entry : program_dlc_entries) {
 | 
			
		||||
        if (RemoveDLC(fs_controller, entry)) {
 | 
			
		||||
            ++count;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return count;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * \brief Removes the installed update for a game
 | 
			
		||||
 * \param fs_controller [FileSystemController] reference from the Core::System instance
 | 
			
		||||
 * \param program_id Program ID for the game that will have its installed update removed
 | 
			
		||||
 * \return 'true' if successful
 | 
			
		||||
 */
 | 
			
		||||
inline bool RemoveUpdate(const Service::FileSystem::FileSystemController& fs_controller,
 | 
			
		||||
                         const u64 program_id) {
 | 
			
		||||
    const auto update_id = program_id | 0x800;
 | 
			
		||||
    return fs_controller.GetUserNANDContents()->RemoveExistingEntry(update_id) ||
 | 
			
		||||
           fs_controller.GetSDMCContents()->RemoveExistingEntry(update_id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * \brief Removes the base content for a game
 | 
			
		||||
 * \param fs_controller [FileSystemController] reference from the Core::System instance
 | 
			
		||||
 * \param program_id Program ID for the game that will have its base content removed
 | 
			
		||||
 * \return 'true' if successful
 | 
			
		||||
 */
 | 
			
		||||
inline bool RemoveBaseContent(const Service::FileSystem::FileSystemController& fs_controller,
 | 
			
		||||
                              const u64 program_id) {
 | 
			
		||||
    return fs_controller.GetUserNANDContents()->RemoveExistingEntry(program_id) ||
 | 
			
		||||
           fs_controller.GetSDMCContents()->RemoveExistingEntry(program_id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * \brief Removes a mod for a game
 | 
			
		||||
 * \param fs_controller [FileSystemController] reference from the Core::System instance
 | 
			
		||||
 * \param program_id Program ID for the game where [mod_name] will be removed
 | 
			
		||||
 * \param mod_name The name of a mod as given by FileSys::PatchManager::GetPatches. This corresponds
 | 
			
		||||
 * with the name of the mod's directory in a game's load folder.
 | 
			
		||||
 * \return 'true' if successful
 | 
			
		||||
 */
 | 
			
		||||
inline bool RemoveMod(const Service::FileSystem::FileSystemController& fs_controller,
 | 
			
		||||
                      const u64 program_id, const std::string& mod_name) {
 | 
			
		||||
    // Check general Mods (LayeredFS and IPS)
 | 
			
		||||
    const auto mod_dir = fs_controller.GetModificationLoadRoot(program_id);
 | 
			
		||||
    if (mod_dir != nullptr) {
 | 
			
		||||
        return mod_dir->DeleteSubdirectoryRecursive(mod_name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check SDMC mod directory (RomFS LayeredFS)
 | 
			
		||||
    const auto sdmc_mod_dir = fs_controller.GetSDMCModificationLoadRoot(program_id);
 | 
			
		||||
    if (sdmc_mod_dir != nullptr) {
 | 
			
		||||
        return sdmc_mod_dir->DeleteSubdirectoryRecursive(mod_name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * \brief Installs an NSP
 | 
			
		||||
 * \param system Raw pointer to the system instance
 | 
			
		||||
 * \param vfs Raw pointer to the VfsFilesystem instance in Core::System
 | 
			
		||||
 * \param filename Path to the NSP file
 | 
			
		||||
 * \param callback Optional callback to report the progress of the installation. The first size_t
 | 
			
		||||
 * parameter is the total size of the virtual file and the second is the current progress. If you
 | 
			
		||||
 * return false to the callback, it will cancel the installation as soon as possible.
 | 
			
		||||
 * \return [InstallResult] representing how the installation finished
 | 
			
		||||
 */
 | 
			
		||||
inline InstallResult InstallNSP(
 | 
			
		||||
    Core::System* system, FileSys::VfsFilesystem* vfs, const std::string& filename,
 | 
			
		||||
    const std::function<bool(size_t, size_t)>& callback = std::function<bool(size_t, size_t)>()) {
 | 
			
		||||
    const auto copy = [callback](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
 | 
			
		||||
                                 std::size_t block_size) {
 | 
			
		||||
        if (src == nullptr || dest == nullptr) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        if (!dest->Resize(src->GetSize())) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        using namespace Common::Literals;
 | 
			
		||||
        std::vector<u8> buffer(1_MiB);
 | 
			
		||||
 | 
			
		||||
        for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
 | 
			
		||||
            if (callback(src->GetSize(), i)) {
 | 
			
		||||
                dest->Resize(0);
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            const auto read = src->Read(buffer.data(), buffer.size(), i);
 | 
			
		||||
            dest->Write(buffer.data(), read, i);
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    std::shared_ptr<FileSys::NSP> nsp;
 | 
			
		||||
    FileSys::VirtualFile file = vfs->OpenFile(filename, FileSys::Mode::Read);
 | 
			
		||||
    if (boost::to_lower_copy(file->GetName()).ends_with(std::string("nsp"))) {
 | 
			
		||||
        nsp = std::make_shared<FileSys::NSP>(file);
 | 
			
		||||
        if (nsp->IsExtractedType()) {
 | 
			
		||||
            return InstallResult::Failure;
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        return InstallResult::Failure;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (nsp->GetStatus() != Loader::ResultStatus::Success) {
 | 
			
		||||
        return InstallResult::Failure;
 | 
			
		||||
    }
 | 
			
		||||
    const auto res =
 | 
			
		||||
        system->GetFileSystemController().GetUserNANDContents()->InstallEntry(*nsp, true, copy);
 | 
			
		||||
    switch (res) {
 | 
			
		||||
    case FileSys::InstallResult::Success:
 | 
			
		||||
        return InstallResult::Success;
 | 
			
		||||
    case FileSys::InstallResult::OverwriteExisting:
 | 
			
		||||
        return InstallResult::Overwrite;
 | 
			
		||||
    case FileSys::InstallResult::ErrorBaseInstall:
 | 
			
		||||
        return InstallResult::BaseInstallAttempted;
 | 
			
		||||
    default:
 | 
			
		||||
        return InstallResult::Failure;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * \brief Installs an NCA
 | 
			
		||||
 * \param vfs Raw pointer to the VfsFilesystem instance in Core::System
 | 
			
		||||
 * \param filename Path to the NCA file
 | 
			
		||||
 * \param registered_cache Raw pointer to the registered cache that the NCA will be installed to
 | 
			
		||||
 * \param title_type Type of NCA package to install
 | 
			
		||||
 * \param callback Optional callback to report the progress of the installation. The first size_t
 | 
			
		||||
 * parameter is the total size of the virtual file and the second is the current progress. If you
 | 
			
		||||
 * return false to the callback, it will cancel the installation as soon as possible.
 | 
			
		||||
 * \return [InstallResult] representing how the installation finished
 | 
			
		||||
 */
 | 
			
		||||
inline InstallResult InstallNCA(
 | 
			
		||||
    FileSys::VfsFilesystem* vfs, const std::string& filename,
 | 
			
		||||
    FileSys::RegisteredCache* registered_cache, const FileSys::TitleType title_type,
 | 
			
		||||
    const std::function<bool(size_t, size_t)>& callback = std::function<bool(size_t, size_t)>()) {
 | 
			
		||||
    const auto copy = [callback](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
 | 
			
		||||
                                 std::size_t block_size) {
 | 
			
		||||
        if (src == nullptr || dest == nullptr) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        if (!dest->Resize(src->GetSize())) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        using namespace Common::Literals;
 | 
			
		||||
        std::vector<u8> buffer(1_MiB);
 | 
			
		||||
 | 
			
		||||
        for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
 | 
			
		||||
            if (callback(src->GetSize(), i)) {
 | 
			
		||||
                dest->Resize(0);
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
            const auto read = src->Read(buffer.data(), buffer.size(), i);
 | 
			
		||||
            dest->Write(buffer.data(), read, i);
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const auto nca = std::make_shared<FileSys::NCA>(vfs->OpenFile(filename, FileSys::Mode::Read));
 | 
			
		||||
    const auto id = nca->GetStatus();
 | 
			
		||||
 | 
			
		||||
    // Game updates necessary are missing base RomFS
 | 
			
		||||
    if (id != Loader::ResultStatus::Success &&
 | 
			
		||||
        id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
 | 
			
		||||
        return InstallResult::Failure;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const auto res = registered_cache->InstallEntry(*nca, title_type, true, copy);
 | 
			
		||||
    if (res == FileSys::InstallResult::Success) {
 | 
			
		||||
        return InstallResult::Success;
 | 
			
		||||
    } else if (res == FileSys::InstallResult::OverwriteExisting) {
 | 
			
		||||
        return InstallResult::Overwrite;
 | 
			
		||||
    } else {
 | 
			
		||||
        return InstallResult::Failure;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace ContentManager
 | 
			
		||||
@@ -122,9 +122,8 @@ void ConfigurePerGameAddons::LoadConfiguration() {
 | 
			
		||||
 | 
			
		||||
    const auto& disabled = Settings::values.disabled_addons[title_id];
 | 
			
		||||
 | 
			
		||||
    for (const auto& patch : pm.GetPatchVersionNames(update_raw)) {
 | 
			
		||||
        const auto name =
 | 
			
		||||
            QString::fromStdString(patch.first).replace(QStringLiteral("[D] "), QString{});
 | 
			
		||||
    for (const auto& patch : pm.GetPatches(update_raw)) {
 | 
			
		||||
        const auto name = QString::fromStdString(patch.name);
 | 
			
		||||
 | 
			
		||||
        auto* const first_item = new QStandardItem;
 | 
			
		||||
        first_item->setText(name);
 | 
			
		||||
@@ -136,7 +135,7 @@ void ConfigurePerGameAddons::LoadConfiguration() {
 | 
			
		||||
        first_item->setCheckState(patch_disabled ? Qt::Unchecked : Qt::Checked);
 | 
			
		||||
 | 
			
		||||
        list_items.push_back(QList<QStandardItem*>{
 | 
			
		||||
            first_item, new QStandardItem{QString::fromStdString(patch.second)}});
 | 
			
		||||
            first_item, new QStandardItem{QString::fromStdString(patch.version)}});
 | 
			
		||||
        item_model->appendRow(list_items.back());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -164,18 +164,19 @@ QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager,
 | 
			
		||||
    QString out;
 | 
			
		||||
    FileSys::VirtualFile update_raw;
 | 
			
		||||
    loader.ReadUpdateRaw(update_raw);
 | 
			
		||||
    for (const auto& kv : patch_manager.GetPatchVersionNames(update_raw)) {
 | 
			
		||||
        const bool is_update = kv.first == "Update" || kv.first == "[D] Update";
 | 
			
		||||
    for (const auto& patch : patch_manager.GetPatches(update_raw)) {
 | 
			
		||||
        const bool is_update = patch.name == "Update";
 | 
			
		||||
        if (!updatable && is_update) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const QString type = QString::fromStdString(kv.first);
 | 
			
		||||
        const QString type =
 | 
			
		||||
            QString::fromStdString(patch.enabled ? patch.name : "[D] " + patch.name);
 | 
			
		||||
 | 
			
		||||
        if (kv.second.empty()) {
 | 
			
		||||
        if (patch.version.empty()) {
 | 
			
		||||
            out.append(QStringLiteral("%1\n").arg(type));
 | 
			
		||||
        } else {
 | 
			
		||||
            auto ver = kv.second;
 | 
			
		||||
            auto ver = patch.version;
 | 
			
		||||
 | 
			
		||||
            // Display container name for packed updates
 | 
			
		||||
            if (is_update && ver == "PACKED") {
 | 
			
		||||
 
 | 
			
		||||
@@ -47,6 +47,7 @@
 | 
			
		||||
#include "core/hle/service/am/applet_oe.h"
 | 
			
		||||
#include "core/hle/service/am/applets/applets.h"
 | 
			
		||||
#include "core/hle/service/set/system_settings_server.h"
 | 
			
		||||
#include "frontend_common/content_manager.h"
 | 
			
		||||
#include "hid_core/frontend/emulated_controller.h"
 | 
			
		||||
#include "hid_core/hid_core.h"
 | 
			
		||||
#include "yuzu/multiplayer/state.h"
 | 
			
		||||
@@ -2476,10 +2477,8 @@ void GMainWindow::OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryT
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void GMainWindow::RemoveBaseContent(u64 program_id, InstalledEntryType type) {
 | 
			
		||||
    const auto& fs_controller = system->GetFileSystemController();
 | 
			
		||||
    const auto res = fs_controller.GetUserNANDContents()->RemoveExistingEntry(program_id) ||
 | 
			
		||||
                     fs_controller.GetSDMCContents()->RemoveExistingEntry(program_id);
 | 
			
		||||
 | 
			
		||||
    const auto res =
 | 
			
		||||
        ContentManager::RemoveBaseContent(system->GetFileSystemController(), program_id);
 | 
			
		||||
    if (res) {
 | 
			
		||||
        QMessageBox::information(this, tr("Successfully Removed"),
 | 
			
		||||
                                 tr("Successfully removed the installed base game."));
 | 
			
		||||
@@ -2491,11 +2490,7 @@ void GMainWindow::RemoveBaseContent(u64 program_id, InstalledEntryType type) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void GMainWindow::RemoveUpdateContent(u64 program_id, InstalledEntryType type) {
 | 
			
		||||
    const auto update_id = program_id | 0x800;
 | 
			
		||||
    const auto& fs_controller = system->GetFileSystemController();
 | 
			
		||||
    const auto res = fs_controller.GetUserNANDContents()->RemoveExistingEntry(update_id) ||
 | 
			
		||||
                     fs_controller.GetSDMCContents()->RemoveExistingEntry(update_id);
 | 
			
		||||
 | 
			
		||||
    const auto res = ContentManager::RemoveUpdate(system->GetFileSystemController(), program_id);
 | 
			
		||||
    if (res) {
 | 
			
		||||
        QMessageBox::information(this, tr("Successfully Removed"),
 | 
			
		||||
                                 tr("Successfully removed the installed update."));
 | 
			
		||||
@@ -2506,22 +2501,7 @@ void GMainWindow::RemoveUpdateContent(u64 program_id, InstalledEntryType type) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void GMainWindow::RemoveAddOnContent(u64 program_id, InstalledEntryType type) {
 | 
			
		||||
    u32 count{};
 | 
			
		||||
    const auto& fs_controller = system->GetFileSystemController();
 | 
			
		||||
    const auto dlc_entries = system->GetContentProvider().ListEntriesFilter(
 | 
			
		||||
        FileSys::TitleType::AOC, FileSys::ContentRecordType::Data);
 | 
			
		||||
 | 
			
		||||
    for (const auto& entry : dlc_entries) {
 | 
			
		||||
        if (FileSys::GetBaseTitleID(entry.title_id) == program_id) {
 | 
			
		||||
            const auto res =
 | 
			
		||||
                fs_controller.GetUserNANDContents()->RemoveExistingEntry(entry.title_id) ||
 | 
			
		||||
                fs_controller.GetSDMCContents()->RemoveExistingEntry(entry.title_id);
 | 
			
		||||
            if (res) {
 | 
			
		||||
                ++count;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const size_t count = ContentManager::RemoveAllDLC(system.get(), program_id);
 | 
			
		||||
    if (count == 0) {
 | 
			
		||||
        QMessageBox::warning(this, GetGameListErrorRemoving(type),
 | 
			
		||||
                             tr("There are no DLC installed for this title."));
 | 
			
		||||
@@ -3290,12 +3270,21 @@ void GMainWindow::OnMenuInstallToNAND() {
 | 
			
		||||
        install_progress->setLabelText(
 | 
			
		||||
            tr("Installing file \"%1\"...").arg(QFileInfo(file).fileName()));
 | 
			
		||||
 | 
			
		||||
        QFuture<InstallResult> future;
 | 
			
		||||
        InstallResult result;
 | 
			
		||||
        QFuture<ContentManager::InstallResult> future;
 | 
			
		||||
        ContentManager::InstallResult result;
 | 
			
		||||
 | 
			
		||||
        if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
 | 
			
		||||
 | 
			
		||||
            future = QtConcurrent::run([this, &file] { return InstallNSP(file); });
 | 
			
		||||
            const auto progress_callback = [this](size_t size, size_t progress) {
 | 
			
		||||
                emit UpdateInstallProgress();
 | 
			
		||||
                if (install_progress->wasCanceled()) {
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
                return false;
 | 
			
		||||
            };
 | 
			
		||||
            future = QtConcurrent::run([this, &file, progress_callback] {
 | 
			
		||||
                return ContentManager::InstallNSP(system.get(), vfs.get(), file.toStdString(),
 | 
			
		||||
                                                  progress_callback);
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            while (!future.isFinished()) {
 | 
			
		||||
                QCoreApplication::processEvents();
 | 
			
		||||
@@ -3311,16 +3300,16 @@ void GMainWindow::OnMenuInstallToNAND() {
 | 
			
		||||
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
 | 
			
		||||
 | 
			
		||||
        switch (result) {
 | 
			
		||||
        case InstallResult::Success:
 | 
			
		||||
        case ContentManager::InstallResult::Success:
 | 
			
		||||
            new_files.append(QFileInfo(file).fileName());
 | 
			
		||||
            break;
 | 
			
		||||
        case InstallResult::Overwrite:
 | 
			
		||||
        case ContentManager::InstallResult::Overwrite:
 | 
			
		||||
            overwritten_files.append(QFileInfo(file).fileName());
 | 
			
		||||
            break;
 | 
			
		||||
        case InstallResult::Failure:
 | 
			
		||||
        case ContentManager::InstallResult::Failure:
 | 
			
		||||
            failed_files.append(QFileInfo(file).fileName());
 | 
			
		||||
            break;
 | 
			
		||||
        case InstallResult::BaseInstallAttempted:
 | 
			
		||||
        case ContentManager::InstallResult::BaseInstallAttempted:
 | 
			
		||||
            failed_files.append(QFileInfo(file).fileName());
 | 
			
		||||
            detected_base_install = true;
 | 
			
		||||
            break;
 | 
			
		||||
@@ -3354,96 +3343,7 @@ void GMainWindow::OnMenuInstallToNAND() {
 | 
			
		||||
    ui->action_Install_File_NAND->setEnabled(true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
InstallResult GMainWindow::InstallNSP(const QString& filename) {
 | 
			
		||||
    const auto qt_raw_copy = [this](const FileSys::VirtualFile& src,
 | 
			
		||||
                                    const FileSys::VirtualFile& dest, std::size_t block_size) {
 | 
			
		||||
        if (src == nullptr || dest == nullptr) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        if (!dest->Resize(src->GetSize())) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        std::vector<u8> buffer(CopyBufferSize);
 | 
			
		||||
 | 
			
		||||
        for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
 | 
			
		||||
            if (install_progress->wasCanceled()) {
 | 
			
		||||
                dest->Resize(0);
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            emit UpdateInstallProgress();
 | 
			
		||||
 | 
			
		||||
            const auto read = src->Read(buffer.data(), buffer.size(), i);
 | 
			
		||||
            dest->Write(buffer.data(), read, i);
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    std::shared_ptr<FileSys::NSP> nsp;
 | 
			
		||||
    if (filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
 | 
			
		||||
        nsp = std::make_shared<FileSys::NSP>(
 | 
			
		||||
            vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
 | 
			
		||||
        if (nsp->IsExtractedType()) {
 | 
			
		||||
            return InstallResult::Failure;
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        return InstallResult::Failure;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (nsp->GetStatus() != Loader::ResultStatus::Success) {
 | 
			
		||||
        return InstallResult::Failure;
 | 
			
		||||
    }
 | 
			
		||||
    const auto res = system->GetFileSystemController().GetUserNANDContents()->InstallEntry(
 | 
			
		||||
        *nsp, true, qt_raw_copy);
 | 
			
		||||
    switch (res) {
 | 
			
		||||
    case FileSys::InstallResult::Success:
 | 
			
		||||
        return InstallResult::Success;
 | 
			
		||||
    case FileSys::InstallResult::OverwriteExisting:
 | 
			
		||||
        return InstallResult::Overwrite;
 | 
			
		||||
    case FileSys::InstallResult::ErrorBaseInstall:
 | 
			
		||||
        return InstallResult::BaseInstallAttempted;
 | 
			
		||||
    default:
 | 
			
		||||
        return InstallResult::Failure;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
InstallResult GMainWindow::InstallNCA(const QString& filename) {
 | 
			
		||||
    const auto qt_raw_copy = [this](const FileSys::VirtualFile& src,
 | 
			
		||||
                                    const FileSys::VirtualFile& dest, std::size_t block_size) {
 | 
			
		||||
        if (src == nullptr || dest == nullptr) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        if (!dest->Resize(src->GetSize())) {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        std::vector<u8> buffer(CopyBufferSize);
 | 
			
		||||
 | 
			
		||||
        for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
 | 
			
		||||
            if (install_progress->wasCanceled()) {
 | 
			
		||||
                dest->Resize(0);
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            emit UpdateInstallProgress();
 | 
			
		||||
 | 
			
		||||
            const auto read = src->Read(buffer.data(), buffer.size(), i);
 | 
			
		||||
            dest->Write(buffer.data(), read, i);
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const auto nca =
 | 
			
		||||
        std::make_shared<FileSys::NCA>(vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
 | 
			
		||||
    const auto id = nca->GetStatus();
 | 
			
		||||
 | 
			
		||||
    // Game updates necessary are missing base RomFS
 | 
			
		||||
    if (id != Loader::ResultStatus::Success &&
 | 
			
		||||
        id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
 | 
			
		||||
        return InstallResult::Failure;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
ContentManager::InstallResult GMainWindow::InstallNCA(const QString& filename) {
 | 
			
		||||
    const QStringList tt_options{tr("System Application"),
 | 
			
		||||
                                 tr("System Archive"),
 | 
			
		||||
                                 tr("System Application Update"),
 | 
			
		||||
@@ -3464,7 +3364,7 @@ InstallResult GMainWindow::InstallNCA(const QString& filename) {
 | 
			
		||||
    if (!ok || index == -1) {
 | 
			
		||||
        QMessageBox::warning(this, tr("Failed to Install"),
 | 
			
		||||
                             tr("The title type you selected for the NCA is invalid."));
 | 
			
		||||
        return InstallResult::Failure;
 | 
			
		||||
        return ContentManager::InstallResult::Failure;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If index is equal to or past Game, add the jump in TitleType.
 | 
			
		||||
@@ -3478,15 +3378,15 @@ InstallResult GMainWindow::InstallNCA(const QString& filename) {
 | 
			
		||||
    auto* registered_cache = is_application ? fs_controller.GetUserNANDContents()
 | 
			
		||||
                                            : fs_controller.GetSystemNANDContents();
 | 
			
		||||
 | 
			
		||||
    const auto res = registered_cache->InstallEntry(*nca, static_cast<FileSys::TitleType>(index),
 | 
			
		||||
                                                    true, qt_raw_copy);
 | 
			
		||||
    if (res == FileSys::InstallResult::Success) {
 | 
			
		||||
        return InstallResult::Success;
 | 
			
		||||
    } else if (res == FileSys::InstallResult::OverwriteExisting) {
 | 
			
		||||
        return InstallResult::Overwrite;
 | 
			
		||||
    } else {
 | 
			
		||||
        return InstallResult::Failure;
 | 
			
		||||
    }
 | 
			
		||||
    const auto progress_callback = [this](size_t size, size_t progress) {
 | 
			
		||||
        emit UpdateInstallProgress();
 | 
			
		||||
        if (install_progress->wasCanceled()) {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
    };
 | 
			
		||||
    return ContentManager::InstallNCA(vfs.get(), filename.toStdString(), registered_cache,
 | 
			
		||||
                                      static_cast<FileSys::TitleType>(index), progress_callback);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void GMainWindow::OnMenuRecentFile() {
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@
 | 
			
		||||
#include "common/announce_multiplayer_room.h"
 | 
			
		||||
#include "common/common_types.h"
 | 
			
		||||
#include "configuration/qt_config.h"
 | 
			
		||||
#include "frontend_common/content_manager.h"
 | 
			
		||||
#include "input_common/drivers/tas_input.h"
 | 
			
		||||
#include "yuzu/compatibility_list.h"
 | 
			
		||||
#include "yuzu/hotkeys.h"
 | 
			
		||||
@@ -124,13 +125,6 @@ enum class EmulatedDirectoryTarget {
 | 
			
		||||
    SDMC,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class InstallResult {
 | 
			
		||||
    Success,
 | 
			
		||||
    Overwrite,
 | 
			
		||||
    Failure,
 | 
			
		||||
    BaseInstallAttempted,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class ReinitializeKeyBehavior {
 | 
			
		||||
    NoWarning,
 | 
			
		||||
    Warning,
 | 
			
		||||
@@ -427,8 +421,7 @@ private:
 | 
			
		||||
    void RemoveCacheStorage(u64 program_id);
 | 
			
		||||
    bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id,
 | 
			
		||||
                               u64* selected_title_id, u8* selected_content_record_type);
 | 
			
		||||
    InstallResult InstallNSP(const QString& filename);
 | 
			
		||||
    InstallResult InstallNCA(const QString& filename);
 | 
			
		||||
    ContentManager::InstallResult InstallNCA(const QString& filename);
 | 
			
		||||
    void MigrateConfigFiles();
 | 
			
		||||
    void UpdateWindowTitle(std::string_view title_name = {}, std::string_view title_version = {},
 | 
			
		||||
                           std::string_view gpu_vendor = {});
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user