summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar liamwhite2024-01-20 13:35:03 -0500
committerGravatar GitHub2024-01-20 13:35:03 -0500
commit2faa63167682ca48e84310b8ee1be2edce031f1e (patch)
treeb0bac4ed76764636d4ae545d47c921da8522a4cf /src
parentMerge pull request #12660 from german77/better-vibration (diff)
parentfrontend_common: Add documentation for content_mananger (diff)
downloadyuzu-2faa63167682ca48e84310b8ee1be2edce031f1e.tar.gz
yuzu-2faa63167682ca48e84310b8ee1be2edce031f1e.tar.xz
yuzu-2faa63167682ca48e84310b8ee1be2edce031f1e.zip
Merge pull request #12715 from t895/remove-addons
android: Add uninstall addon button
Diffstat (limited to 'src')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt44
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt21
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt25
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt6
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt25
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt29
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt (renamed from src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt)44
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt10
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt42
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/InstallResult.kt15
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Patch.kt16
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/PatchType.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt29
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt101
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt80
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt5
-rw-r--r--src/android/app/src/main/jni/android_common/android_common.cpp16
-rw-r--r--src/android/app/src/main/jni/android_common/android_common.h7
-rw-r--r--src/android/app/src/main/jni/id_cache.cpp101
-rw-r--r--src/android/app/src/main/jni/id_cache.h17
-rw-r--r--src/android/app/src/main/jni/native.cpp128
-rw-r--r--src/android/app/src/main/jni/native.h2
-rw-r--r--src/android/app/src/main/res/layout/dialog_progress_bar.xml30
-rw-r--r--src/android/app/src/main/res/layout/list_item_addon.xml32
-rw-r--r--src/android/app/src/main/res/values/strings.xml3
-rw-r--r--src/core/file_sys/patch_manager.cpp43
-rw-r--r--src/core/file_sys/patch_manager.h17
-rw-r--r--src/frontend_common/CMakeLists.txt1
-rw-r--r--src/frontend_common/content_manager.h238
-rw-r--r--src/yuzu/configuration/configure_per_game_addons.cpp7
-rw-r--r--src/yuzu/game_list_worker.cpp11
-rw-r--r--src/yuzu/main.cpp166
-rw-r--r--src/yuzu/main.h11
33 files changed, 912 insertions, 424 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
index b7556e353..1c9fb0675 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
@@ -21,6 +21,8 @@ import org.yuzu.yuzu_emu.utils.DocumentsTree
21import org.yuzu.yuzu_emu.utils.FileUtil 21import org.yuzu.yuzu_emu.utils.FileUtil
22import org.yuzu.yuzu_emu.utils.Log 22import org.yuzu.yuzu_emu.utils.Log
23import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable 23import org.yuzu.yuzu_emu.utils.SerializableHelper.serializable
24import org.yuzu.yuzu_emu.model.InstallResult
25import org.yuzu.yuzu_emu.model.Patch
24 26
25/** 27/**
26 * Class which contains methods that interact 28 * Class which contains methods that interact
@@ -235,9 +237,12 @@ object NativeLibrary {
235 /** 237 /**
236 * Installs a nsp or xci file to nand 238 * Installs a nsp or xci file to nand
237 * @param filename String representation of file uri 239 * @param filename String representation of file uri
238 * @param extension Lowercase string representation of file extension without "." 240 * @return int representation of [InstallResult]
239 */ 241 */
240 external fun installFileToNand(filename: String, extension: String): Int 242 external fun installFileToNand(
243 filename: String,
244 callback: (max: Long, progress: Long) -> Boolean
245 ): Int
241 246
242 external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean 247 external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean
243 248
@@ -535,9 +540,29 @@ object NativeLibrary {
535 * 540 *
536 * @param path Path to game file. Can be a [Uri]. 541 * @param path Path to game file. Can be a [Uri].
537 * @param programId String representation of a game's program ID 542 * @param programId String representation of a game's program ID
538 * @return Array of pairs where the first value is the name of an addon and the second is the version 543 * @return Array of available patches
539 */ 544 */
540 external fun getAddonsForFile(path: String, programId: String): Array<Pair<String, String>>? 545 external fun getPatchesForFile(path: String, programId: String): Array<Patch>?
546
547 /**
548 * Removes an update for a given [programId]
549 * @param programId String representation of a game's program ID
550 */
551 external fun removeUpdate(programId: String)
552
553 /**
554 * Removes all DLC for a [programId]
555 * @param programId String representation of a game's program ID
556 */
557 external fun removeDLC(programId: String)
558
559 /**
560 * Removes a mod installed for a given [programId]
561 * @param programId String representation of a game's program ID
562 * @param name The name of a mod as given by [getPatchesForFile]. This corresponds with the name
563 * of the mod's directory in a game's load folder.
564 */
565 external fun removeMod(programId: String, name: String)
541 566
542 /** 567 /**
543 * Gets the save location for a specific game 568 * Gets the save location for a specific game
@@ -609,15 +634,4 @@ object NativeLibrary {
609 const val RELEASED = 0 634 const val RELEASED = 0
610 const val PRESSED = 1 635 const val PRESSED = 1
611 } 636 }
612
613 /**
614 * Result from installFileToNand
615 */
616 object InstallFileToNandResult {
617 const val Success = 0
618 const val SuccessFileOverwritten = 1
619 const val Error = 2
620 const val ErrorBaseGame = 3
621 const val ErrorFilenameExtension = 4
622 }
623} 637}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt
index 94c151325..ff254d9b7 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt
@@ -6,27 +6,32 @@ package org.yuzu.yuzu_emu.adapters
6import android.view.LayoutInflater 6import android.view.LayoutInflater
7import android.view.ViewGroup 7import android.view.ViewGroup
8import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding 8import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding
9import org.yuzu.yuzu_emu.model.Addon 9import org.yuzu.yuzu_emu.model.Patch
10import org.yuzu.yuzu_emu.model.AddonViewModel
10import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder 11import org.yuzu.yuzu_emu.viewholder.AbstractViewHolder
11 12
12class AddonAdapter : AbstractDiffAdapter<Addon, AddonAdapter.AddonViewHolder>() { 13class AddonAdapter(val addonViewModel: AddonViewModel) :
14 AbstractDiffAdapter<Patch, AddonAdapter.AddonViewHolder>() {
13 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder { 15 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder {
14 ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false) 16 ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
15 .also { return AddonViewHolder(it) } 17 .also { return AddonViewHolder(it) }
16 } 18 }
17 19
18 inner class AddonViewHolder(val binding: ListItemAddonBinding) : 20 inner class AddonViewHolder(val binding: ListItemAddonBinding) :
19 AbstractViewHolder<Addon>(binding) { 21 AbstractViewHolder<Patch>(binding) {
20 override fun bind(model: Addon) { 22 override fun bind(model: Patch) {
21 binding.root.setOnClickListener { 23 binding.root.setOnClickListener {
22 binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked 24 binding.addonCheckbox.isChecked = !binding.addonCheckbox.isChecked
23 } 25 }
24 binding.title.text = model.title 26 binding.title.text = model.name
25 binding.version.text = model.version 27 binding.version.text = model.version
26 binding.addonSwitch.setOnCheckedChangeListener { _, checked -> 28 binding.addonCheckbox.setOnCheckedChangeListener { _, checked ->
27 model.enabled = checked 29 model.enabled = checked
28 } 30 }
29 binding.addonSwitch.isChecked = model.enabled 31 binding.addonCheckbox.isChecked = model.enabled
32 binding.buttonDelete.setOnClickListener {
33 addonViewModel.setAddonToDelete(model)
34 }
30 } 35 }
31 } 36 }
32} 37}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
index 816336820..adb65812c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
@@ -74,7 +74,7 @@ class AddonsFragment : Fragment() {
74 74
75 binding.listAddons.apply { 75 binding.listAddons.apply {
76 layoutManager = LinearLayoutManager(requireContext()) 76 layoutManager = LinearLayoutManager(requireContext())
77 adapter = AddonAdapter() 77 adapter = AddonAdapter(addonViewModel)
78 } 78 }
79 79
80 viewLifecycleOwner.lifecycleScope.apply { 80 viewLifecycleOwner.lifecycleScope.apply {
@@ -110,6 +110,21 @@ class AddonsFragment : Fragment() {
110 } 110 }
111 } 111 }
112 } 112 }
113 launch {
114 repeatOnLifecycle(Lifecycle.State.STARTED) {
115 addonViewModel.addonToDelete.collect {
116 if (it != null) {
117 MessageDialogFragment.newInstance(
118 requireActivity(),
119 titleId = R.string.confirm_uninstall,
120 descriptionId = R.string.confirm_uninstall_description,
121 positiveAction = { addonViewModel.onDeleteAddon(it) }
122 ).show(parentFragmentManager, MessageDialogFragment.TAG)
123 addonViewModel.setAddonToDelete(null)
124 }
125 }
126 }
127 }
113 } 128 }
114 129
115 binding.buttonInstall.setOnClickListener { 130 binding.buttonInstall.setOnClickListener {
@@ -156,22 +171,22 @@ class AddonsFragment : Fragment() {
156 descriptionId = R.string.invalid_directory_description 171 descriptionId = R.string.invalid_directory_description
157 ) 172 )
158 if (isValid) { 173 if (isValid) {
159 IndeterminateProgressDialogFragment.newInstance( 174 ProgressDialogFragment.newInstance(
160 requireActivity(), 175 requireActivity(),
161 R.string.installing_game_content, 176 R.string.installing_game_content,
162 false 177 false
163 ) { 178 ) { progressCallback, _ ->
164 val parentDirectoryName = externalAddonDirectory.name 179 val parentDirectoryName = externalAddonDirectory.name
165 val internalAddonDirectory = 180 val internalAddonDirectory =
166 File(args.game.addonDir + parentDirectoryName) 181 File(args.game.addonDir + parentDirectoryName)
167 try { 182 try {
168 externalAddonDirectory.copyFilesTo(internalAddonDirectory) 183 externalAddonDirectory.copyFilesTo(internalAddonDirectory, progressCallback)
169 } catch (_: Exception) { 184 } catch (_: Exception) {
170 return@newInstance errorMessage 185 return@newInstance errorMessage
171 } 186 }
172 addonViewModel.refreshAddons() 187 addonViewModel.refreshAddons()
173 return@newInstance getString(R.string.addon_installed_successfully) 188 return@newInstance getString(R.string.addon_installed_successfully)
174 }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) 189 }.show(parentFragmentManager, ProgressDialogFragment.TAG)
175 } else { 190 } else {
176 errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG) 191 errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG)
177 } 192 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
index 9dabb9c41..6c758d80b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt
@@ -173,11 +173,11 @@ class DriverManagerFragment : Fragment() {
173 return@registerForActivityResult 173 return@registerForActivityResult
174 } 174 }
175 175
176 IndeterminateProgressDialogFragment.newInstance( 176 ProgressDialogFragment.newInstance(
177 requireActivity(), 177 requireActivity(),
178 R.string.installing_driver, 178 R.string.installing_driver,
179 false 179 false
180 ) { 180 ) { _, _ ->
181 val driverPath = 181 val driverPath =
182 "${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(result)}" 182 "${GpuDriverHelper.driverStoragePath}${FileUtil.getFilename(result)}"
183 val driverFile = File(driverPath) 183 val driverFile = File(driverPath)
@@ -213,6 +213,6 @@ class DriverManagerFragment : Fragment() {
213 } 213 }
214 } 214 }
215 return@newInstance Any() 215 return@newInstance Any()
216 }.show(childFragmentManager, IndeterminateProgressDialogFragment.TAG) 216 }.show(childFragmentManager, ProgressDialogFragment.TAG)
217 } 217 }
218} 218}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
index b04d1208f..83a845434 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
@@ -44,7 +44,6 @@ import org.yuzu.yuzu_emu.utils.FileUtil
44import org.yuzu.yuzu_emu.utils.GameIconUtils 44import org.yuzu.yuzu_emu.utils.GameIconUtils
45import org.yuzu.yuzu_emu.utils.GpuDriverHelper 45import org.yuzu.yuzu_emu.utils.GpuDriverHelper
46import org.yuzu.yuzu_emu.utils.MemoryUtil 46import org.yuzu.yuzu_emu.utils.MemoryUtil
47import java.io.BufferedInputStream
48import java.io.BufferedOutputStream 47import java.io.BufferedOutputStream
49import java.io.File 48import java.io.File
50 49
@@ -357,27 +356,17 @@ class GamePropertiesFragment : Fragment() {
357 return@registerForActivityResult 356 return@registerForActivityResult
358 } 357 }
359 358
360 val inputZip = requireContext().contentResolver.openInputStream(result)
361 val savesFolder = File(args.game.saveDir) 359 val savesFolder = File(args.game.saveDir)
362 val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") 360 val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
363 cacheSaveDir.mkdir() 361 cacheSaveDir.mkdir()
364 362
365 if (inputZip == null) { 363 ProgressDialogFragment.newInstance(
366 Toast.makeText(
367 YuzuApplication.appContext,
368 getString(R.string.fatal_error),
369 Toast.LENGTH_LONG
370 ).show()
371 return@registerForActivityResult
372 }
373
374 IndeterminateProgressDialogFragment.newInstance(
375 requireActivity(), 364 requireActivity(),
376 R.string.save_files_importing, 365 R.string.save_files_importing,
377 false 366 false
378 ) { 367 ) { _, _ ->
379 try { 368 try {
380 FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) 369 FileUtil.unzipToInternalStorage(result.toString(), cacheSaveDir)
381 val files = cacheSaveDir.listFiles() 370 val files = cacheSaveDir.listFiles()
382 var savesFolderFile: File? = null 371 var savesFolderFile: File? = null
383 if (files != null) { 372 if (files != null) {
@@ -422,7 +411,7 @@ class GamePropertiesFragment : Fragment() {
422 Toast.LENGTH_LONG 411 Toast.LENGTH_LONG
423 ).show() 412 ).show()
424 } 413 }
425 }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) 414 }.show(parentFragmentManager, ProgressDialogFragment.TAG)
426 } 415 }
427 416
428 /** 417 /**
@@ -436,11 +425,11 @@ class GamePropertiesFragment : Fragment() {
436 return@registerForActivityResult 425 return@registerForActivityResult
437 } 426 }
438 427
439 IndeterminateProgressDialogFragment.newInstance( 428 ProgressDialogFragment.newInstance(
440 requireActivity(), 429 requireActivity(),
441 R.string.save_files_exporting, 430 R.string.save_files_exporting,
442 false 431 false
443 ) { 432 ) { _, _ ->
444 val saveLocation = args.game.saveDir 433 val saveLocation = args.game.saveDir
445 val zipResult = FileUtil.zipFromInternalStorage( 434 val zipResult = FileUtil.zipFromInternalStorage(
446 File(saveLocation), 435 File(saveLocation),
@@ -452,6 +441,6 @@ class GamePropertiesFragment : Fragment() {
452 TaskState.Completed -> getString(R.string.export_success) 441 TaskState.Completed -> getString(R.string.export_success)
453 TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) 442 TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
454 } 443 }
455 }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) 444 }.show(parentFragmentManager, ProgressDialogFragment.TAG)
456 } 445 }
457} 446}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
index 5b4bf2c9f..7df8e6bf4 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt
@@ -34,7 +34,6 @@ import org.yuzu.yuzu_emu.model.TaskState
34import org.yuzu.yuzu_emu.ui.main.MainActivity 34import org.yuzu.yuzu_emu.ui.main.MainActivity
35import org.yuzu.yuzu_emu.utils.DirectoryInitialization 35import org.yuzu.yuzu_emu.utils.DirectoryInitialization
36import org.yuzu.yuzu_emu.utils.FileUtil 36import org.yuzu.yuzu_emu.utils.FileUtil
37import java.io.BufferedInputStream
38import java.io.BufferedOutputStream 37import java.io.BufferedOutputStream
39import java.io.File 38import java.io.File
40import java.math.BigInteger 39import java.math.BigInteger
@@ -195,26 +194,20 @@ class InstallableFragment : Fragment() {
195 return@registerForActivityResult 194 return@registerForActivityResult
196 } 195 }
197 196
198 val inputZip = requireContext().contentResolver.openInputStream(result)
199 val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") 197 val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
200 cacheSaveDir.mkdir() 198 cacheSaveDir.mkdir()
201 199
202 if (inputZip == null) { 200 ProgressDialogFragment.newInstance(
203 Toast.makeText(
204 YuzuApplication.appContext,
205 getString(R.string.fatal_error),
206 Toast.LENGTH_LONG
207 ).show()
208 return@registerForActivityResult
209 }
210
211 IndeterminateProgressDialogFragment.newInstance(
212 requireActivity(), 201 requireActivity(),
213 R.string.save_files_importing, 202 R.string.save_files_importing,
214 false 203 false
215 ) { 204 ) { progressCallback, _ ->
216 try { 205 try {
217 FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) 206 FileUtil.unzipToInternalStorage(
207 result.toString(),
208 cacheSaveDir,
209 progressCallback
210 )
218 val files = cacheSaveDir.listFiles() 211 val files = cacheSaveDir.listFiles()
219 var successfulImports = 0 212 var successfulImports = 0
220 var failedImports = 0 213 var failedImports = 0
@@ -287,7 +280,7 @@ class InstallableFragment : Fragment() {
287 Toast.LENGTH_LONG 280 Toast.LENGTH_LONG
288 ).show() 281 ).show()
289 } 282 }
290 }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) 283 }.show(parentFragmentManager, ProgressDialogFragment.TAG)
291 } 284 }
292 285
293 private val exportSaves = registerForActivityResult( 286 private val exportSaves = registerForActivityResult(
@@ -297,11 +290,11 @@ class InstallableFragment : Fragment() {
297 return@registerForActivityResult 290 return@registerForActivityResult
298 } 291 }
299 292
300 IndeterminateProgressDialogFragment.newInstance( 293 ProgressDialogFragment.newInstance(
301 requireActivity(), 294 requireActivity(),
302 R.string.save_files_exporting, 295 R.string.save_files_exporting,
303 false 296 false
304 ) { 297 ) { _, _ ->
305 val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") 298 val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
306 cacheSaveDir.mkdir() 299 cacheSaveDir.mkdir()
307 300
@@ -338,6 +331,6 @@ class InstallableFragment : Fragment() {
338 TaskState.Completed -> getString(R.string.export_success) 331 TaskState.Completed -> getString(R.string.export_success)
339 TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) 332 TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
340 } 333 }
341 }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) 334 }.show(parentFragmentManager, ProgressDialogFragment.TAG)
342 } 335 }
343} 336}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt
index 8847e5531..d201cb80c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ProgressDialogFragment.kt
@@ -23,11 +23,13 @@ import org.yuzu.yuzu_emu.R
23import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding 23import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding
24import org.yuzu.yuzu_emu.model.TaskViewModel 24import org.yuzu.yuzu_emu.model.TaskViewModel
25 25
26class IndeterminateProgressDialogFragment : DialogFragment() { 26class ProgressDialogFragment : DialogFragment() {
27 private val taskViewModel: TaskViewModel by activityViewModels() 27 private val taskViewModel: TaskViewModel by activityViewModels()
28 28
29 private lateinit var binding: DialogProgressBarBinding 29 private lateinit var binding: DialogProgressBarBinding
30 30
31 private val PROGRESS_BAR_RESOLUTION = 1000
32
31 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 33 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
32 val titleId = requireArguments().getInt(TITLE) 34 val titleId = requireArguments().getInt(TITLE)
33 val cancellable = requireArguments().getBoolean(CANCELLABLE) 35 val cancellable = requireArguments().getBoolean(CANCELLABLE)
@@ -61,6 +63,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
61 63
62 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 64 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
63 super.onViewCreated(view, savedInstanceState) 65 super.onViewCreated(view, savedInstanceState)
66 binding.message.isSelected = true
64 viewLifecycleOwner.lifecycleScope.apply { 67 viewLifecycleOwner.lifecycleScope.apply {
65 launch { 68 launch {
66 repeatOnLifecycle(Lifecycle.State.CREATED) { 69 repeatOnLifecycle(Lifecycle.State.CREATED) {
@@ -97,6 +100,35 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
97 } 100 }
98 } 101 }
99 } 102 }
103 launch {
104 repeatOnLifecycle(Lifecycle.State.CREATED) {
105 taskViewModel.progress.collect {
106 if (it != 0.0) {
107 binding.progressBar.apply {
108 isIndeterminate = false
109 progress = (
110 (it / taskViewModel.maxProgress.value) *
111 PROGRESS_BAR_RESOLUTION
112 ).toInt()
113 min = 0
114 max = PROGRESS_BAR_RESOLUTION
115 }
116 }
117 }
118 }
119 }
120 launch {
121 repeatOnLifecycle(Lifecycle.State.CREATED) {
122 taskViewModel.message.collect {
123 if (it.isEmpty()) {
124 binding.message.visibility = View.GONE
125 } else {
126 binding.message.visibility = View.VISIBLE
127 binding.message.text = it
128 }
129 }
130 }
131 }
100 } 132 }
101 } 133 }
102 134
@@ -108,6 +140,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
108 val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE) 140 val negativeButton = alertDialog.getButton(Dialog.BUTTON_NEGATIVE)
109 negativeButton.setOnClickListener { 141 negativeButton.setOnClickListener {
110 alertDialog.setTitle(getString(R.string.cancelling)) 142 alertDialog.setTitle(getString(R.string.cancelling))
143 binding.progressBar.isIndeterminate = true
111 taskViewModel.setCancelled(true) 144 taskViewModel.setCancelled(true)
112 } 145 }
113 } 146 }
@@ -122,9 +155,12 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
122 activity: FragmentActivity, 155 activity: FragmentActivity,
123 titleId: Int, 156 titleId: Int,
124 cancellable: Boolean = false, 157 cancellable: Boolean = false,
125 task: suspend () -> Any 158 task: suspend (
126 ): IndeterminateProgressDialogFragment { 159 progressCallback: (max: Long, progress: Long) -> Boolean,
127 val dialog = IndeterminateProgressDialogFragment() 160 messageCallback: (message: String) -> Unit
161 ) -> Any
162 ): ProgressDialogFragment {
163 val dialog = ProgressDialogFragment()
128 val args = Bundle() 164 val args = Bundle()
129 ViewModelProvider(activity)[TaskViewModel::class.java].task = task 165 ViewModelProvider(activity)[TaskViewModel::class.java].task = task
130 args.putInt(TITLE, titleId) 166 args.putInt(TITLE, titleId)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt
deleted file mode 100644
index ed79a8b02..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt
+++ /dev/null
@@ -1,10 +0,0 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6data class Addon(
7 var enabled: Boolean,
8 val title: String,
9 val version: String
10)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt
index 075252f5b..b9c8e49ca 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt
@@ -15,8 +15,8 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
15import java.util.concurrent.atomic.AtomicBoolean 15import java.util.concurrent.atomic.AtomicBoolean
16 16
17class AddonViewModel : ViewModel() { 17class AddonViewModel : ViewModel() {
18 private val _addonList = MutableStateFlow(mutableListOf<Addon>()) 18 private val _patchList = MutableStateFlow(mutableListOf<Patch>())
19 val addonList get() = _addonList.asStateFlow() 19 val addonList get() = _patchList.asStateFlow()
20 20
21 private val _showModInstallPicker = MutableStateFlow(false) 21 private val _showModInstallPicker = MutableStateFlow(false)
22 val showModInstallPicker get() = _showModInstallPicker.asStateFlow() 22 val showModInstallPicker get() = _showModInstallPicker.asStateFlow()
@@ -24,6 +24,9 @@ class AddonViewModel : ViewModel() {
24 private val _showModNoticeDialog = MutableStateFlow(false) 24 private val _showModNoticeDialog = MutableStateFlow(false)
25 val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow() 25 val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow()
26 26
27 private val _addonToDelete = MutableStateFlow<Patch?>(null)
28 val addonToDelete = _addonToDelete.asStateFlow()
29
27 var game: Game? = null 30 var game: Game? = null
28 31
29 private val isRefreshing = AtomicBoolean(false) 32 private val isRefreshing = AtomicBoolean(false)
@@ -40,36 +43,47 @@ class AddonViewModel : ViewModel() {
40 isRefreshing.set(true) 43 isRefreshing.set(true)
41 viewModelScope.launch { 44 viewModelScope.launch {
42 withContext(Dispatchers.IO) { 45 withContext(Dispatchers.IO) {
43 val addonList = mutableListOf<Addon>() 46 val patchList = (
44 val disabledAddons = NativeConfig.getDisabledAddons(game!!.programId) 47 NativeLibrary.getPatchesForFile(game!!.path, game!!.programId)
45 NativeLibrary.getAddonsForFile(game!!.path, game!!.programId)?.forEach { 48 ?: emptyArray()
46 val name = it.first.replace("[D] ", "") 49 ).toMutableList()
47 addonList.add(Addon(!disabledAddons.contains(name), name, it.second)) 50 patchList.sortBy { it.name }
48 } 51 _patchList.value = patchList
49 addonList.sortBy { it.title }
50 _addonList.value = addonList
51 isRefreshing.set(false) 52 isRefreshing.set(false)
52 } 53 }
53 } 54 }
54 } 55 }
55 56
57 fun setAddonToDelete(patch: Patch?) {
58 _addonToDelete.value = patch
59 }
60
61 fun onDeleteAddon(patch: Patch) {
62 when (PatchType.from(patch.type)) {
63 PatchType.Update -> NativeLibrary.removeUpdate(patch.programId)
64 PatchType.DLC -> NativeLibrary.removeDLC(patch.programId)
65 PatchType.Mod -> NativeLibrary.removeMod(patch.programId, patch.name)
66 }
67 refreshAddons()
68 }
69
56 fun onCloseAddons() { 70 fun onCloseAddons() {
57 if (_addonList.value.isEmpty()) { 71 if (_patchList.value.isEmpty()) {
58 return 72 return
59 } 73 }
60 74
61 NativeConfig.setDisabledAddons( 75 NativeConfig.setDisabledAddons(
62 game!!.programId, 76 game!!.programId,
63 _addonList.value.mapNotNull { 77 _patchList.value.mapNotNull {
64 if (it.enabled) { 78 if (it.enabled) {
65 null 79 null
66 } else { 80 } else {
67 it.title 81 it.name
68 } 82 }
69 }.toTypedArray() 83 }.toTypedArray()
70 ) 84 )
71 NativeConfig.saveGlobalConfig() 85 NativeConfig.saveGlobalConfig()
72 _addonList.value.clear() 86 _patchList.value.clear()
73 game = null 87 game = null
74 } 88 }
75 89
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/InstallResult.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/InstallResult.kt
new file mode 100644
index 000000000..0c3cd0521
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/InstallResult.kt
@@ -0,0 +1,15 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6enum class InstallResult(val int: Int) {
7 Success(0),
8 Overwrite(1),
9 Failure(2),
10 BaseInstallAttempted(3);
11
12 companion object {
13 fun from(int: Int): InstallResult = entries.firstOrNull { it.int == int } ?: Success
14 }
15}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Patch.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Patch.kt
new file mode 100644
index 000000000..25cb9e365
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Patch.kt
@@ -0,0 +1,16 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6import androidx.annotation.Keep
7
8@Keep
9data class Patch(
10 var enabled: Boolean,
11 val name: String,
12 val version: String,
13 val type: Int,
14 val programId: String,
15 val titleId: String
16)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/PatchType.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/PatchType.kt
new file mode 100644
index 000000000..e9a54162b
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/PatchType.kt
@@ -0,0 +1,14 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.model
5
6enum class PatchType(val int: Int) {
7 Update(0),
8 DLC(1),
9 Mod(2);
10
11 companion object {
12 fun from(int: Int): PatchType = entries.firstOrNull { it.int == int } ?: Update
13 }
14}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
index e59c95733..4361eb972 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt
@@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
8import kotlinx.coroutines.Dispatchers 8import kotlinx.coroutines.Dispatchers
9import kotlinx.coroutines.flow.MutableStateFlow 9import kotlinx.coroutines.flow.MutableStateFlow
10import kotlinx.coroutines.flow.StateFlow 10import kotlinx.coroutines.flow.StateFlow
11import kotlinx.coroutines.flow.asStateFlow
11import kotlinx.coroutines.launch 12import kotlinx.coroutines.launch
12 13
13class TaskViewModel : ViewModel() { 14class TaskViewModel : ViewModel() {
@@ -23,13 +24,28 @@ class TaskViewModel : ViewModel() {
23 val cancelled: StateFlow<Boolean> get() = _cancelled 24 val cancelled: StateFlow<Boolean> get() = _cancelled
24 private val _cancelled = MutableStateFlow(false) 25 private val _cancelled = MutableStateFlow(false)
25 26
26 lateinit var task: suspend () -> Any 27 private val _progress = MutableStateFlow(0.0)
28 val progress = _progress.asStateFlow()
29
30 private val _maxProgress = MutableStateFlow(0.0)
31 val maxProgress = _maxProgress.asStateFlow()
32
33 private val _message = MutableStateFlow("")
34 val message = _message.asStateFlow()
35
36 lateinit var task: suspend (
37 progressCallback: (max: Long, progress: Long) -> Boolean,
38 messageCallback: (message: String) -> Unit
39 ) -> Any
27 40
28 fun clear() { 41 fun clear() {
29 _result.value = Any() 42 _result.value = Any()
30 _isComplete.value = false 43 _isComplete.value = false
31 _isRunning.value = false 44 _isRunning.value = false
32 _cancelled.value = false 45 _cancelled.value = false
46 _progress.value = 0.0
47 _maxProgress.value = 0.0
48 _message.value = ""
33 } 49 }
34 50
35 fun setCancelled(value: Boolean) { 51 fun setCancelled(value: Boolean) {
@@ -43,7 +59,16 @@ class TaskViewModel : ViewModel() {
43 _isRunning.value = true 59 _isRunning.value = true
44 60
45 viewModelScope.launch(Dispatchers.IO) { 61 viewModelScope.launch(Dispatchers.IO) {
46 val res = task() 62 val res = task(
63 { max, progress ->
64 _maxProgress.value = max.toDouble()
65 _progress.value = progress.toDouble()
66 return@task cancelled.value
67 },
68 { message ->
69 _message.value = message
70 }
71 )
47 _result.value = res 72 _result.value = res
48 _isComplete.value = true 73 _isComplete.value = true
49 _isRunning.value = false 74 _isRunning.value = false
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
index 644289e25..c2cc29961 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt
@@ -38,12 +38,13 @@ import org.yuzu.yuzu_emu.activities.EmulationActivity
38import org.yuzu.yuzu_emu.databinding.ActivityMainBinding 38import org.yuzu.yuzu_emu.databinding.ActivityMainBinding
39import org.yuzu.yuzu_emu.features.settings.model.Settings 39import org.yuzu.yuzu_emu.features.settings.model.Settings
40import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment 40import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
41import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment 41import org.yuzu.yuzu_emu.fragments.ProgressDialogFragment
42import org.yuzu.yuzu_emu.fragments.MessageDialogFragment 42import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
43import org.yuzu.yuzu_emu.model.AddonViewModel 43import org.yuzu.yuzu_emu.model.AddonViewModel
44import org.yuzu.yuzu_emu.model.DriverViewModel 44import org.yuzu.yuzu_emu.model.DriverViewModel
45import org.yuzu.yuzu_emu.model.GamesViewModel 45import org.yuzu.yuzu_emu.model.GamesViewModel
46import org.yuzu.yuzu_emu.model.HomeViewModel 46import org.yuzu.yuzu_emu.model.HomeViewModel
47import org.yuzu.yuzu_emu.model.InstallResult
47import org.yuzu.yuzu_emu.model.TaskState 48import org.yuzu.yuzu_emu.model.TaskState
48import org.yuzu.yuzu_emu.model.TaskViewModel 49import org.yuzu.yuzu_emu.model.TaskViewModel
49import org.yuzu.yuzu_emu.utils.* 50import org.yuzu.yuzu_emu.utils.*
@@ -369,26 +370,23 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
369 return@registerForActivityResult 370 return@registerForActivityResult
370 } 371 }
371 372
372 val inputZip = contentResolver.openInputStream(result)
373 if (inputZip == null) {
374 Toast.makeText(
375 applicationContext,
376 getString(R.string.fatal_error),
377 Toast.LENGTH_LONG
378 ).show()
379 return@registerForActivityResult
380 }
381
382 val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") } 373 val filterNCA = FilenameFilter { _, dirName -> dirName.endsWith(".nca") }
383 374
384 val firmwarePath = 375 val firmwarePath =
385 File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/") 376 File(DirectoryInitialization.userDirectory + "/nand/system/Contents/registered/")
386 val cacheFirmwareDir = File("${cacheDir.path}/registered/") 377 val cacheFirmwareDir = File("${cacheDir.path}/registered/")
387 378
388 val task: () -> Any = { 379 ProgressDialogFragment.newInstance(
380 this,
381 R.string.firmware_installing
382 ) { progressCallback, _ ->
389 var messageToShow: Any 383 var messageToShow: Any
390 try { 384 try {
391 FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheFirmwareDir) 385 FileUtil.unzipToInternalStorage(
386 result.toString(),
387 cacheFirmwareDir,
388 progressCallback
389 )
392 val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1 390 val unfilteredNumOfFiles = cacheFirmwareDir.list()?.size ?: -1
393 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2 391 val filteredNumOfFiles = cacheFirmwareDir.list(filterNCA)?.size ?: -2
394 messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) { 392 messageToShow = if (unfilteredNumOfFiles != filteredNumOfFiles) {
@@ -404,18 +402,13 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
404 getString(R.string.save_file_imported_success) 402 getString(R.string.save_file_imported_success)
405 } 403 }
406 } catch (e: Exception) { 404 } catch (e: Exception) {
405 Log.error("[MainActivity] Firmware install failed - ${e.message}")
407 messageToShow = getString(R.string.fatal_error) 406 messageToShow = getString(R.string.fatal_error)
408 } finally { 407 } finally {
409 cacheFirmwareDir.deleteRecursively() 408 cacheFirmwareDir.deleteRecursively()
410 } 409 }
411 messageToShow 410 messageToShow
412 } 411 }.show(supportFragmentManager, ProgressDialogFragment.TAG)
413
414 IndeterminateProgressDialogFragment.newInstance(
415 this,
416 R.string.firmware_installing,
417 task = task
418 ).show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
419 } 412 }
420 413
421 val getAmiiboKey = 414 val getAmiiboKey =
@@ -474,11 +467,11 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
474 return@registerForActivityResult 467 return@registerForActivityResult
475 } 468 }
476 469
477 IndeterminateProgressDialogFragment.newInstance( 470 ProgressDialogFragment.newInstance(
478 this@MainActivity, 471 this@MainActivity,
479 R.string.verifying_content, 472 R.string.verifying_content,
480 false 473 false
481 ) { 474 ) { _, _ ->
482 var updatesMatchProgram = true 475 var updatesMatchProgram = true
483 for (document in documents) { 476 for (document in documents) {
484 val valid = NativeLibrary.doesUpdateMatchProgram( 477 val valid = NativeLibrary.doesUpdateMatchProgram(
@@ -501,44 +494,42 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
501 positiveAction = { homeViewModel.setContentToInstall(documents) } 494 positiveAction = { homeViewModel.setContentToInstall(documents) }
502 ) 495 )
503 } 496 }
504 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) 497 }.show(supportFragmentManager, ProgressDialogFragment.TAG)
505 } 498 }
506 499
507 private fun installContent(documents: List<Uri>) { 500 private fun installContent(documents: List<Uri>) {
508 IndeterminateProgressDialogFragment.newInstance( 501 ProgressDialogFragment.newInstance(
509 this@MainActivity, 502 this@MainActivity,
510 R.string.installing_game_content 503 R.string.installing_game_content
511 ) { 504 ) { progressCallback, messageCallback ->
512 var installSuccess = 0 505 var installSuccess = 0
513 var installOverwrite = 0 506 var installOverwrite = 0
514 var errorBaseGame = 0 507 var errorBaseGame = 0
515 var errorExtension = 0 508 var error = 0
516 var errorOther = 0
517 documents.forEach { 509 documents.forEach {
510 messageCallback.invoke(FileUtil.getFilename(it))
518 when ( 511 when (
519 NativeLibrary.installFileToNand( 512 InstallResult.from(
520 it.toString(), 513 NativeLibrary.installFileToNand(
521 FileUtil.getExtension(it) 514 it.toString(),
515 progressCallback
516 )
522 ) 517 )
523 ) { 518 ) {
524 NativeLibrary.InstallFileToNandResult.Success -> { 519 InstallResult.Success -> {
525 installSuccess += 1 520 installSuccess += 1
526 } 521 }
527 522
528 NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> { 523 InstallResult.Overwrite -> {
529 installOverwrite += 1 524 installOverwrite += 1
530 } 525 }
531 526
532 NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> { 527 InstallResult.BaseInstallAttempted -> {
533 errorBaseGame += 1 528 errorBaseGame += 1
534 } 529 }
535 530
536 NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> { 531 InstallResult.Failure -> {
537 errorExtension += 1 532 error += 1
538 }
539
540 else -> {
541 errorOther += 1
542 } 533 }
543 } 534 }
544 } 535 }
@@ -565,7 +556,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
565 ) 556 )
566 installResult.append(separator) 557 installResult.append(separator)
567 } 558 }
568 val errorTotal: Int = errorBaseGame + errorExtension + errorOther 559 val errorTotal: Int = errorBaseGame + error
569 if (errorTotal > 0) { 560 if (errorTotal > 0) {
570 installResult.append(separator) 561 installResult.append(separator)
571 installResult.append( 562 installResult.append(
@@ -582,14 +573,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
582 ) 573 )
583 installResult.append(separator) 574 installResult.append(separator)
584 } 575 }
585 if (errorExtension > 0) { 576 if (error > 0) {
586 installResult.append(separator)
587 installResult.append(
588 getString(R.string.install_game_content_failure_file_extension)
589 )
590 installResult.append(separator)
591 }
592 if (errorOther > 0) {
593 installResult.append( 577 installResult.append(
594 getString(R.string.install_game_content_failure_description) 578 getString(R.string.install_game_content_failure_description)
595 ) 579 )
@@ -608,7 +592,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
608 descriptionString = installResult.toString().trim() 592 descriptionString = installResult.toString().trim()
609 ) 593 )
610 } 594 }
611 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) 595 }.show(supportFragmentManager, ProgressDialogFragment.TAG)
612 } 596 }
613 597
614 val exportUserData = registerForActivityResult( 598 val exportUserData = registerForActivityResult(
@@ -618,16 +602,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
618 return@registerForActivityResult 602 return@registerForActivityResult
619 } 603 }
620 604
621 IndeterminateProgressDialogFragment.newInstance( 605 ProgressDialogFragment.newInstance(
622 this, 606 this,
623 R.string.exporting_user_data, 607 R.string.exporting_user_data,
624 true 608 true
625 ) { 609 ) { progressCallback, _ ->
626 val zipResult = FileUtil.zipFromInternalStorage( 610 val zipResult = FileUtil.zipFromInternalStorage(
627 File(DirectoryInitialization.userDirectory!!), 611 File(DirectoryInitialization.userDirectory!!),
628 DirectoryInitialization.userDirectory!!, 612 DirectoryInitialization.userDirectory!!,
629 BufferedOutputStream(contentResolver.openOutputStream(result)), 613 BufferedOutputStream(contentResolver.openOutputStream(result)),
630 taskViewModel.cancelled, 614 progressCallback,
631 compression = false 615 compression = false
632 ) 616 )
633 return@newInstance when (zipResult) { 617 return@newInstance when (zipResult) {
@@ -635,7 +619,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
635 TaskState.Failed -> R.string.export_failed 619 TaskState.Failed -> R.string.export_failed
636 TaskState.Cancelled -> R.string.user_data_export_cancelled 620 TaskState.Cancelled -> R.string.user_data_export_cancelled
637 } 621 }
638 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) 622 }.show(supportFragmentManager, ProgressDialogFragment.TAG)
639 } 623 }
640 624
641 val importUserData = 625 val importUserData =
@@ -644,10 +628,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
644 return@registerForActivityResult 628 return@registerForActivityResult
645 } 629 }
646 630
647 IndeterminateProgressDialogFragment.newInstance( 631 ProgressDialogFragment.newInstance(
648 this, 632 this,
649 R.string.importing_user_data 633 R.string.importing_user_data
650 ) { 634 ) { progressCallback, _ ->
651 val checkStream = 635 val checkStream =
652 ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result))) 636 ZipInputStream(BufferedInputStream(contentResolver.openInputStream(result)))
653 var isYuzuBackup = false 637 var isYuzuBackup = false
@@ -676,8 +660,9 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
676 // Copy archive to internal storage 660 // Copy archive to internal storage
677 try { 661 try {
678 FileUtil.unzipToInternalStorage( 662 FileUtil.unzipToInternalStorage(
679 BufferedInputStream(contentResolver.openInputStream(result)), 663 result.toString(),
680 File(DirectoryInitialization.userDirectory!!) 664 File(DirectoryInitialization.userDirectory!!),
665 progressCallback
681 ) 666 )
682 } catch (e: Exception) { 667 } catch (e: Exception) {
683 return@newInstance MessageDialogFragment.newInstance( 668 return@newInstance MessageDialogFragment.newInstance(
@@ -694,6 +679,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
694 driverViewModel.reloadDriverData() 679 driverViewModel.reloadDriverData()
695 680
696 return@newInstance getString(R.string.user_data_import_success) 681 return@newInstance getString(R.string.user_data_import_success)
697 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) 682 }.show(supportFragmentManager, ProgressDialogFragment.TAG)
698 } 683 }
699} 684}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
index b54a19c65..fc2339f5a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt
@@ -7,7 +7,6 @@ import android.database.Cursor
7import android.net.Uri 7import android.net.Uri
8import android.provider.DocumentsContract 8import android.provider.DocumentsContract
9import androidx.documentfile.provider.DocumentFile 9import androidx.documentfile.provider.DocumentFile
10import kotlinx.coroutines.flow.StateFlow
11import java.io.BufferedInputStream 10import java.io.BufferedInputStream
12import java.io.File 11import java.io.File
13import java.io.IOException 12import java.io.IOException
@@ -19,6 +18,7 @@ import org.yuzu.yuzu_emu.YuzuApplication
19import org.yuzu.yuzu_emu.model.MinimalDocumentFile 18import org.yuzu.yuzu_emu.model.MinimalDocumentFile
20import org.yuzu.yuzu_emu.model.TaskState 19import org.yuzu.yuzu_emu.model.TaskState
21import java.io.BufferedOutputStream 20import java.io.BufferedOutputStream
21import java.io.OutputStream
22import java.lang.NullPointerException 22import java.lang.NullPointerException
23import java.nio.charset.StandardCharsets 23import java.nio.charset.StandardCharsets
24import java.util.zip.Deflater 24import java.util.zip.Deflater
@@ -283,12 +283,34 @@ object FileUtil {
283 283
284 /** 284 /**
285 * Extracts the given zip file into the given directory. 285 * Extracts the given zip file into the given directory.
286 * @param path String representation of a [Uri] or a typical path delimited by '/'
287 * @param destDir Location to unzip the contents of [path] into
288 * @param progressCallback Lambda that is called with the total number of files and the current
289 * progress through the process. Stops execution as soon as possible if this returns true.
286 */ 290 */
287 @Throws(SecurityException::class) 291 @Throws(SecurityException::class)
288 fun unzipToInternalStorage(zipStream: BufferedInputStream, destDir: File) { 292 fun unzipToInternalStorage(
289 ZipInputStream(zipStream).use { zis -> 293 path: String,
294 destDir: File,
295 progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }
296 ) {
297 var totalEntries = 0L
298 ZipInputStream(getInputStream(path)).use { zis ->
299 var tempEntry = zis.nextEntry
300 while (tempEntry != null) {
301 tempEntry = zis.nextEntry
302 totalEntries++
303 }
304 }
305
306 var progress = 0L
307 ZipInputStream(getInputStream(path)).use { zis ->
290 var entry: ZipEntry? = zis.nextEntry 308 var entry: ZipEntry? = zis.nextEntry
291 while (entry != null) { 309 while (entry != null) {
310 if (progressCallback.invoke(totalEntries, progress)) {
311 return@use
312 }
313
292 val newFile = File(destDir, entry.name) 314 val newFile = File(destDir, entry.name)
293 val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile 315 val destinationDirectory = if (entry.isDirectory) newFile else newFile.parentFile
294 316
@@ -304,6 +326,7 @@ object FileUtil {
304 newFile.outputStream().use { fos -> zis.copyTo(fos) } 326 newFile.outputStream().use { fos -> zis.copyTo(fos) }
305 } 327 }
306 entry = zis.nextEntry 328 entry = zis.nextEntry
329 progress++
307 } 330 }
308 } 331 }
309 } 332 }
@@ -313,14 +336,15 @@ object FileUtil {
313 * @param inputFile File representation of the item that will be zipped 336 * @param inputFile File representation of the item that will be zipped
314 * @param rootDir Directory containing the inputFile 337 * @param rootDir Directory containing the inputFile
315 * @param outputStream Stream where the zip file will be output 338 * @param outputStream Stream where the zip file will be output
316 * @param cancelled [StateFlow] that reports whether this process has been cancelled 339 * @param progressCallback Lambda that is called with the total number of files and the current
340 * progress through the process. Stops execution as soon as possible if this returns true.
317 * @param compression Disables compression if true 341 * @param compression Disables compression if true
318 */ 342 */
319 fun zipFromInternalStorage( 343 fun zipFromInternalStorage(
320 inputFile: File, 344 inputFile: File,
321 rootDir: String, 345 rootDir: String,
322 outputStream: BufferedOutputStream, 346 outputStream: BufferedOutputStream,
323 cancelled: StateFlow<Boolean>? = null, 347 progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false },
324 compression: Boolean = true 348 compression: Boolean = true
325 ): TaskState { 349 ): TaskState {
326 try { 350 try {
@@ -330,8 +354,10 @@ object FileUtil {
330 zos.setLevel(Deflater.NO_COMPRESSION) 354 zos.setLevel(Deflater.NO_COMPRESSION)
331 } 355 }
332 356
357 var count = 0L
358 val totalFiles = inputFile.walkTopDown().count().toLong()
333 inputFile.walkTopDown().forEach { file -> 359 inputFile.walkTopDown().forEach { file ->
334 if (cancelled?.value == true) { 360 if (progressCallback.invoke(totalFiles, count)) {
335 return TaskState.Cancelled 361 return TaskState.Cancelled
336 } 362 }
337 363
@@ -343,6 +369,7 @@ object FileUtil {
343 if (file.isFile) { 369 if (file.isFile) {
344 file.inputStream().use { fis -> fis.copyTo(zos) } 370 file.inputStream().use { fis -> fis.copyTo(zos) }
345 } 371 }
372 count++
346 } 373 }
347 } 374 }
348 } 375 }
@@ -356,9 +383,14 @@ object FileUtil {
356 /** 383 /**
357 * Helper function that copies the contents of a DocumentFile folder into a [File] 384 * Helper function that copies the contents of a DocumentFile folder into a [File]
358 * @param file [File] representation of the folder to copy into 385 * @param file [File] representation of the folder to copy into
386 * @param progressCallback Lambda that is called with the total number of files and the current
387 * progress through the process. Stops execution as soon as possible if this returns true.
359 * @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa 388 * @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa
360 */ 389 */
361 fun DocumentFile.copyFilesTo(file: File) { 390 fun DocumentFile.copyFilesTo(
391 file: File,
392 progressCallback: (max: Long, progress: Long) -> Boolean = { _, _ -> false }
393 ) {
362 file.mkdirs() 394 file.mkdirs()
363 if (!this.isDirectory || !file.isDirectory) { 395 if (!this.isDirectory || !file.isDirectory) {
364 throw IllegalStateException( 396 throw IllegalStateException(
@@ -366,7 +398,13 @@ object FileUtil {
366 ) 398 )
367 } 399 }
368 400
401 var count = 0L
402 val totalFiles = this.listFiles().size.toLong()
369 this.listFiles().forEach { 403 this.listFiles().forEach {
404 if (progressCallback.invoke(totalFiles, count)) {
405 return
406 }
407
370 val newFile = File(file, it.name!!) 408 val newFile = File(file, it.name!!)
371 if (it.isDirectory) { 409 if (it.isDirectory) {
372 newFile.mkdirs() 410 newFile.mkdirs()
@@ -381,6 +419,7 @@ object FileUtil {
381 newFile.outputStream().use { os -> bos.copyTo(os) } 419 newFile.outputStream().use { os -> bos.copyTo(os) }
382 } 420 }
383 } 421 }
422 count++
384 } 423 }
385 } 424 }
386 425
@@ -427,6 +466,18 @@ object FileUtil {
427 } 466 }
428 } 467 }
429 468
469 fun getInputStream(path: String) = if (path.contains("content://")) {
470 Uri.parse(path).inputStream()
471 } else {
472 File(path).inputStream()
473 }
474
475 fun getOutputStream(path: String) = if (path.contains("content://")) {
476 Uri.parse(path).outputStream()
477 } else {
478 File(path).outputStream()
479 }
480
430 @Throws(IOException::class) 481 @Throws(IOException::class)
431 fun getStringFromFile(file: File): String = 482 fun getStringFromFile(file: File): String =
432 String(file.readBytes(), StandardCharsets.UTF_8) 483 String(file.readBytes(), StandardCharsets.UTF_8)
@@ -434,4 +485,19 @@ object FileUtil {
434 @Throws(IOException::class) 485 @Throws(IOException::class)
435 fun getStringFromInputStream(stream: InputStream): String = 486 fun getStringFromInputStream(stream: InputStream): String =
436 String(stream.readBytes(), StandardCharsets.UTF_8) 487 String(stream.readBytes(), StandardCharsets.UTF_8)
488
489 fun DocumentFile.inputStream(): InputStream =
490 YuzuApplication.appContext.contentResolver.openInputStream(uri)!!
491
492 fun DocumentFile.outputStream(): OutputStream =
493 YuzuApplication.appContext.contentResolver.openOutputStream(uri)!!
494
495 fun Uri.inputStream(): InputStream =
496 YuzuApplication.appContext.contentResolver.openInputStream(this)!!
497
498 fun Uri.outputStream(): OutputStream =
499 YuzuApplication.appContext.contentResolver.openOutputStream(this)!!
500
501 fun Uri.asDocumentFile(): DocumentFile? =
502 DocumentFile.fromSingleUri(YuzuApplication.appContext, this)
437} 503}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
index a8f9dcc34..81212cbee 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt
@@ -5,7 +5,6 @@ package org.yuzu.yuzu_emu.utils
5 5
6import android.net.Uri 6import android.net.Uri
7import android.os.Build 7import android.os.Build
8import java.io.BufferedInputStream
9import java.io.File 8import java.io.File
10import java.io.IOException 9import java.io.IOException
11import org.yuzu.yuzu_emu.NativeLibrary 10import org.yuzu.yuzu_emu.NativeLibrary
@@ -123,7 +122,7 @@ object GpuDriverHelper {
123 // Unzip the driver. 122 // Unzip the driver.
124 try { 123 try {
125 FileUtil.unzipToInternalStorage( 124 FileUtil.unzipToInternalStorage(
126 BufferedInputStream(copiedFile.inputStream()), 125 copiedFile.path,
127 File(driverInstallationPath!!) 126 File(driverInstallationPath!!)
128 ) 127 )
129 } catch (e: SecurityException) { 128 } catch (e: SecurityException) {
@@ -156,7 +155,7 @@ object GpuDriverHelper {
156 // Unzip the driver to the private installation directory 155 // Unzip the driver to the private installation directory
157 try { 156 try {
158 FileUtil.unzipToInternalStorage( 157 FileUtil.unzipToInternalStorage(
159 BufferedInputStream(driver.inputStream()), 158 driver.path,
160 File(driverInstallationPath!!) 159 File(driverInstallationPath!!)
161 ) 160 )
162 } catch (e: SecurityException) { 161 } catch (e: SecurityException) {
diff --git a/src/android/app/src/main/jni/android_common/android_common.cpp b/src/android/app/src/main/jni/android_common/android_common.cpp
index 1e884ffdd..7018a52af 100644
--- a/src/android/app/src/main/jni/android_common/android_common.cpp
+++ b/src/android/app/src/main/jni/android_common/android_common.cpp
@@ -42,3 +42,19 @@ double GetJDouble(JNIEnv* env, jobject jdouble) {
42jobject ToJDouble(JNIEnv* env, double value) { 42jobject ToJDouble(JNIEnv* env, double value) {
43 return env->NewObject(IDCache::GetDoubleClass(), IDCache::GetDoubleConstructor(), value); 43 return env->NewObject(IDCache::GetDoubleClass(), IDCache::GetDoubleConstructor(), value);
44} 44}
45
46s32 GetJInteger(JNIEnv* env, jobject jinteger) {
47 return env->GetIntField(jinteger, IDCache::GetIntegerValueField());
48}
49
50jobject ToJInteger(JNIEnv* env, s32 value) {
51 return env->NewObject(IDCache::GetIntegerClass(), IDCache::GetIntegerConstructor(), value);
52}
53
54bool GetJBoolean(JNIEnv* env, jobject jboolean) {
55 return env->GetBooleanField(jboolean, IDCache::GetBooleanValueField());
56}
57
58jobject ToJBoolean(JNIEnv* env, bool value) {
59 return env->NewObject(IDCache::GetBooleanClass(), IDCache::GetBooleanConstructor(), value);
60}
diff --git a/src/android/app/src/main/jni/android_common/android_common.h b/src/android/app/src/main/jni/android_common/android_common.h
index 8eb803e1b..29a338c0a 100644
--- a/src/android/app/src/main/jni/android_common/android_common.h
+++ b/src/android/app/src/main/jni/android_common/android_common.h
@@ -6,6 +6,7 @@
6#include <string> 6#include <string>
7 7
8#include <jni.h> 8#include <jni.h>
9#include "common/common_types.h"
9 10
10std::string GetJString(JNIEnv* env, jstring jstr); 11std::string GetJString(JNIEnv* env, jstring jstr);
11jstring ToJString(JNIEnv* env, std::string_view str); 12jstring ToJString(JNIEnv* env, std::string_view str);
@@ -13,3 +14,9 @@ jstring ToJString(JNIEnv* env, std::u16string_view str);
13 14
14double GetJDouble(JNIEnv* env, jobject jdouble); 15double GetJDouble(JNIEnv* env, jobject jdouble);
15jobject ToJDouble(JNIEnv* env, double value); 16jobject ToJDouble(JNIEnv* env, double value);
17
18s32 GetJInteger(JNIEnv* env, jobject jinteger);
19jobject ToJInteger(JNIEnv* env, s32 value);
20
21bool GetJBoolean(JNIEnv* env, jobject jboolean);
22jobject ToJBoolean(JNIEnv* env, bool value);
diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp
index c79ad7d76..96f2ad3d4 100644
--- a/src/android/app/src/main/jni/id_cache.cpp
+++ b/src/android/app/src/main/jni/id_cache.cpp
@@ -43,10 +43,27 @@ static jfieldID s_overlay_control_data_landscape_position_field;
43static jfieldID s_overlay_control_data_portrait_position_field; 43static jfieldID s_overlay_control_data_portrait_position_field;
44static jfieldID s_overlay_control_data_foldable_position_field; 44static jfieldID s_overlay_control_data_foldable_position_field;
45 45
46static jclass s_patch_class;
47static jmethodID s_patch_constructor;
48static jfieldID s_patch_enabled_field;
49static jfieldID s_patch_name_field;
50static jfieldID s_patch_version_field;
51static jfieldID s_patch_type_field;
52static jfieldID s_patch_program_id_field;
53static jfieldID s_patch_title_id_field;
54
46static jclass s_double_class; 55static jclass s_double_class;
47static jmethodID s_double_constructor; 56static jmethodID s_double_constructor;
48static jfieldID s_double_value_field; 57static jfieldID s_double_value_field;
49 58
59static jclass s_integer_class;
60static jmethodID s_integer_constructor;
61static jfieldID s_integer_value_field;
62
63static jclass s_boolean_class;
64static jmethodID s_boolean_constructor;
65static jfieldID s_boolean_value_field;
66
50static constexpr jint JNI_VERSION = JNI_VERSION_1_6; 67static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
51 68
52namespace IDCache { 69namespace IDCache {
@@ -186,6 +203,38 @@ jfieldID GetOverlayControlDataFoldablePositionField() {
186 return s_overlay_control_data_foldable_position_field; 203 return s_overlay_control_data_foldable_position_field;
187} 204}
188 205
206jclass GetPatchClass() {
207 return s_patch_class;
208}
209
210jmethodID GetPatchConstructor() {
211 return s_patch_constructor;
212}
213
214jfieldID GetPatchEnabledField() {
215 return s_patch_enabled_field;
216}
217
218jfieldID GetPatchNameField() {
219 return s_patch_name_field;
220}
221
222jfieldID GetPatchVersionField() {
223 return s_patch_version_field;
224}
225
226jfieldID GetPatchTypeField() {
227 return s_patch_type_field;
228}
229
230jfieldID GetPatchProgramIdField() {
231 return s_patch_program_id_field;
232}
233
234jfieldID GetPatchTitleIdField() {
235 return s_patch_title_id_field;
236}
237
189jclass GetDoubleClass() { 238jclass GetDoubleClass() {
190 return s_double_class; 239 return s_double_class;
191} 240}
@@ -198,6 +247,30 @@ jfieldID GetDoubleValueField() {
198 return s_double_value_field; 247 return s_double_value_field;
199} 248}
200 249
250jclass GetIntegerClass() {
251 return s_integer_class;
252}
253
254jmethodID GetIntegerConstructor() {
255 return s_integer_constructor;
256}
257
258jfieldID GetIntegerValueField() {
259 return s_integer_value_field;
260}
261
262jclass GetBooleanClass() {
263 return s_boolean_class;
264}
265
266jmethodID GetBooleanConstructor() {
267 return s_boolean_constructor;
268}
269
270jfieldID GetBooleanValueField() {
271 return s_boolean_value_field;
272}
273
201} // namespace IDCache 274} // namespace IDCache
202 275
203#ifdef __cplusplus 276#ifdef __cplusplus
@@ -278,12 +351,37 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
278 env->GetFieldID(overlay_control_data_class, "foldablePosition", "Lkotlin/Pair;"); 351 env->GetFieldID(overlay_control_data_class, "foldablePosition", "Lkotlin/Pair;");
279 env->DeleteLocalRef(overlay_control_data_class); 352 env->DeleteLocalRef(overlay_control_data_class);
280 353
354 const jclass patch_class = env->FindClass("org/yuzu/yuzu_emu/model/Patch");
355 s_patch_class = reinterpret_cast<jclass>(env->NewGlobalRef(patch_class));
356 s_patch_constructor = env->GetMethodID(
357 patch_class, "<init>",
358 "(ZLjava/lang/String;Ljava/lang/String;ILjava/lang/String;Ljava/lang/String;)V");
359 s_patch_enabled_field = env->GetFieldID(patch_class, "enabled", "Z");
360 s_patch_name_field = env->GetFieldID(patch_class, "name", "Ljava/lang/String;");
361 s_patch_version_field = env->GetFieldID(patch_class, "version", "Ljava/lang/String;");
362 s_patch_type_field = env->GetFieldID(patch_class, "type", "I");
363 s_patch_program_id_field = env->GetFieldID(patch_class, "programId", "Ljava/lang/String;");
364 s_patch_title_id_field = env->GetFieldID(patch_class, "titleId", "Ljava/lang/String;");
365 env->DeleteLocalRef(patch_class);
366
281 const jclass double_class = env->FindClass("java/lang/Double"); 367 const jclass double_class = env->FindClass("java/lang/Double");
282 s_double_class = reinterpret_cast<jclass>(env->NewGlobalRef(double_class)); 368 s_double_class = reinterpret_cast<jclass>(env->NewGlobalRef(double_class));
283 s_double_constructor = env->GetMethodID(double_class, "<init>", "(D)V"); 369 s_double_constructor = env->GetMethodID(double_class, "<init>", "(D)V");
284 s_double_value_field = env->GetFieldID(double_class, "value", "D"); 370 s_double_value_field = env->GetFieldID(double_class, "value", "D");
285 env->DeleteLocalRef(double_class); 371 env->DeleteLocalRef(double_class);
286 372
373 const jclass int_class = env->FindClass("java/lang/Integer");
374 s_integer_class = reinterpret_cast<jclass>(env->NewGlobalRef(int_class));
375 s_integer_constructor = env->GetMethodID(int_class, "<init>", "(I)V");
376 s_integer_value_field = env->GetFieldID(int_class, "value", "I");
377 env->DeleteLocalRef(int_class);
378
379 const jclass boolean_class = env->FindClass("java/lang/Boolean");
380 s_boolean_class = reinterpret_cast<jclass>(env->NewGlobalRef(boolean_class));
381 s_boolean_constructor = env->GetMethodID(boolean_class, "<init>", "(Z)V");
382 s_boolean_value_field = env->GetFieldID(boolean_class, "value", "Z");
383 env->DeleteLocalRef(boolean_class);
384
287 // Initialize Android Storage 385 // Initialize Android Storage
288 Common::FS::Android::RegisterCallbacks(env, s_native_library_class); 386 Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
289 387
@@ -309,7 +407,10 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
309 env->DeleteGlobalRef(s_string_class); 407 env->DeleteGlobalRef(s_string_class);
310 env->DeleteGlobalRef(s_pair_class); 408 env->DeleteGlobalRef(s_pair_class);
311 env->DeleteGlobalRef(s_overlay_control_data_class); 409 env->DeleteGlobalRef(s_overlay_control_data_class);
410 env->DeleteGlobalRef(s_patch_class);
312 env->DeleteGlobalRef(s_double_class); 411 env->DeleteGlobalRef(s_double_class);
412 env->DeleteGlobalRef(s_integer_class);
413 env->DeleteGlobalRef(s_boolean_class);
313 414
314 // UnInitialize applets 415 // UnInitialize applets
315 SoftwareKeyboard::CleanupJNI(env); 416 SoftwareKeyboard::CleanupJNI(env);
diff --git a/src/android/app/src/main/jni/id_cache.h b/src/android/app/src/main/jni/id_cache.h
index 784d1412f..a002e705d 100644
--- a/src/android/app/src/main/jni/id_cache.h
+++ b/src/android/app/src/main/jni/id_cache.h
@@ -43,8 +43,25 @@ jfieldID GetOverlayControlDataLandscapePositionField();
43jfieldID GetOverlayControlDataPortraitPositionField(); 43jfieldID GetOverlayControlDataPortraitPositionField();
44jfieldID GetOverlayControlDataFoldablePositionField(); 44jfieldID GetOverlayControlDataFoldablePositionField();
45 45
46jclass GetPatchClass();
47jmethodID GetPatchConstructor();
48jfieldID GetPatchEnabledField();
49jfieldID GetPatchNameField();
50jfieldID GetPatchVersionField();
51jfieldID GetPatchTypeField();
52jfieldID GetPatchProgramIdField();
53jfieldID GetPatchTitleIdField();
54
46jclass GetDoubleClass(); 55jclass GetDoubleClass();
47jmethodID GetDoubleConstructor(); 56jmethodID GetDoubleConstructor();
48jfieldID GetDoubleValueField(); 57jfieldID GetDoubleValueField();
49 58
59jclass GetIntegerClass();
60jmethodID GetIntegerConstructor();
61jfieldID GetIntegerValueField();
62
63jclass GetBooleanClass();
64jmethodID GetBooleanConstructor();
65jfieldID GetBooleanValueField();
66
50} // namespace IDCache 67} // namespace IDCache
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index ed3b1353a..be0a723b1 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -17,6 +17,7 @@
17#include <core/file_sys/patch_manager.h> 17#include <core/file_sys/patch_manager.h>
18#include <core/file_sys/savedata_factory.h> 18#include <core/file_sys/savedata_factory.h>
19#include <core/loader/nro.h> 19#include <core/loader/nro.h>
20#include <frontend_common/content_manager.h>
20#include <jni.h> 21#include <jni.h>
21 22
22#include "common/detached_tasks.h" 23#include "common/detached_tasks.h"
@@ -100,67 +101,6 @@ void EmulationSession::SetNativeWindow(ANativeWindow* native_window) {
100 m_native_window = native_window; 101 m_native_window = native_window;
101} 102}
102 103
103int EmulationSession::InstallFileToNand(std::string filename, std::string file_extension) {
104 jconst copy_func = [](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
105 std::size_t block_size) {
106 if (src == nullptr || dest == nullptr) {
107 return false;
108 }
109 if (!dest->Resize(src->GetSize())) {
110 return false;
111 }
112
113 using namespace Common::Literals;
114 [[maybe_unused]] std::vector<u8> buffer(1_MiB);
115
116 for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
117 jconst read = src->Read(buffer.data(), buffer.size(), i);
118 dest->Write(buffer.data(), read, i);
119 }
120 return true;
121 };
122
123 enum InstallResult {
124 Success = 0,
125 SuccessFileOverwritten = 1,
126 InstallError = 2,
127 ErrorBaseGame = 3,
128 ErrorFilenameExtension = 4,
129 };
130
131 [[maybe_unused]] std::shared_ptr<FileSys::NSP> nsp;
132 if (file_extension == "nsp") {
133 nsp = std::make_shared<FileSys::NSP>(m_vfs->OpenFile(filename, FileSys::Mode::Read));
134 if (nsp->IsExtractedType()) {
135 return InstallError;
136 }
137 } else {
138 return ErrorFilenameExtension;
139 }
140
141 if (!nsp) {
142 return InstallError;
143 }
144
145 if (nsp->GetStatus() != Loader::ResultStatus::Success) {
146 return InstallError;
147 }
148
149 jconst res = m_system.GetFileSystemController().GetUserNANDContents()->InstallEntry(*nsp, true,
150 copy_func);
151
152 switch (res) {
153 case FileSys::InstallResult::Success:
154 return Success;
155 case FileSys::InstallResult::OverwriteExisting:
156 return SuccessFileOverwritten;
157 case FileSys::InstallResult::ErrorBaseInstall:
158 return ErrorBaseGame;
159 default:
160 return InstallError;
161 }
162}
163
164void EmulationSession::InitializeGpuDriver(const std::string& hook_lib_dir, 104void EmulationSession::InitializeGpuDriver(const std::string& hook_lib_dir,
165 const std::string& custom_driver_dir, 105 const std::string& custom_driver_dir,
166 const std::string& custom_driver_name, 106 const std::string& custom_driver_name,
@@ -512,10 +452,20 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_setAppDirectory(JNIEnv* env, jobject
512} 452}
513 453
514int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject instance, 454int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject instance,
515 jstring j_file, 455 jstring j_file, jobject jcallback) {
516 jstring j_file_extension) { 456 auto jlambdaClass = env->GetObjectClass(jcallback);
517 return EmulationSession::GetInstance().InstallFileToNand(GetJString(env, j_file), 457 auto jlambdaInvokeMethod = env->GetMethodID(
518 GetJString(env, j_file_extension)); 458 jlambdaClass, "invoke", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");
459 const auto callback = [env, jcallback, jlambdaInvokeMethod](size_t max, size_t progress) {
460 auto jwasCancelled = env->CallObjectMethod(jcallback, jlambdaInvokeMethod,
461 ToJDouble(env, max), ToJDouble(env, progress));
462 return GetJBoolean(env, jwasCancelled);
463 };
464
465 return static_cast<int>(
466 ContentManager::InstallNSP(&EmulationSession::GetInstance().System(),
467 EmulationSession::GetInstance().System().GetFilesystem().get(),
468 GetJString(env, j_file), callback));
519} 469}
520 470
521jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj, 471jboolean 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,
824 return true; 774 return true;
825} 775}
826 776
827jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, jobject jobj, 777jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPatchesForFile(JNIEnv* env, jobject jobj,
828 jstring jpath, 778 jstring jpath,
829 jstring jprogramId) { 779 jstring jprogramId) {
830 const auto path = GetJString(env, jpath); 780 const auto path = GetJString(env, jpath);
831 const auto vFile = 781 const auto vFile =
832 Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path); 782 Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path);
@@ -843,20 +793,40 @@ jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env,
843 FileSys::VirtualFile update_raw; 793 FileSys::VirtualFile update_raw;
844 loader->ReadUpdateRaw(update_raw); 794 loader->ReadUpdateRaw(update_raw);
845 795
846 auto addons = pm.GetPatchVersionNames(update_raw); 796 auto patches = pm.GetPatches(update_raw);
847 auto jemptyString = ToJString(env, ""); 797 jobjectArray jpatchArray =
848 auto jemptyStringPair = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(), 798 env->NewObjectArray(patches.size(), IDCache::GetPatchClass(), nullptr);
849 jemptyString, jemptyString);
850 jobjectArray jaddonsArray =
851 env->NewObjectArray(addons.size(), IDCache::GetPairClass(), jemptyStringPair);
852 int i = 0; 799 int i = 0;
853 for (const auto& addon : addons) { 800 for (const auto& patch : patches) {
854 jobject jaddon = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(), 801 jobject jpatch = env->NewObject(
855 ToJString(env, addon.first), ToJString(env, addon.second)); 802 IDCache::GetPatchClass(), IDCache::GetPatchConstructor(), patch.enabled,
856 env->SetObjectArrayElement(jaddonsArray, i, jaddon); 803 ToJString(env, patch.name), ToJString(env, patch.version),
804 static_cast<jint>(patch.type), ToJString(env, std::to_string(patch.program_id)),
805 ToJString(env, std::to_string(patch.title_id)));
806 env->SetObjectArrayElement(jpatchArray, i, jpatch);
857 ++i; 807 ++i;
858 } 808 }
859 return jaddonsArray; 809 return jpatchArray;
810}
811
812void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeUpdate(JNIEnv* env, jobject jobj,
813 jstring jprogramId) {
814 auto program_id = EmulationSession::GetProgramId(env, jprogramId);
815 ContentManager::RemoveUpdate(EmulationSession::GetInstance().System().GetFileSystemController(),
816 program_id);
817}
818
819void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeDLC(JNIEnv* env, jobject jobj,
820 jstring jprogramId) {
821 auto program_id = EmulationSession::GetProgramId(env, jprogramId);
822 ContentManager::RemoveAllDLC(&EmulationSession::GetInstance().System(), program_id);
823}
824
825void Java_org_yuzu_yuzu_1emu_NativeLibrary_removeMod(JNIEnv* env, jobject jobj, jstring jprogramId,
826 jstring jname) {
827 auto program_id = EmulationSession::GetProgramId(env, jprogramId);
828 ContentManager::RemoveMod(EmulationSession::GetInstance().System().GetFileSystemController(),
829 program_id, GetJString(env, jname));
860} 830}
861 831
862jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj, 832jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj,
diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h
index 4a8049578..dadb138ad 100644
--- a/src/android/app/src/main/jni/native.h
+++ b/src/android/app/src/main/jni/native.h
@@ -7,6 +7,7 @@
7#include "core/file_sys/registered_cache.h" 7#include "core/file_sys/registered_cache.h"
8#include "core/hle/service/acc/profile_manager.h" 8#include "core/hle/service/acc/profile_manager.h"
9#include "core/perf_stats.h" 9#include "core/perf_stats.h"
10#include "frontend_common/content_manager.h"
10#include "jni/applets/software_keyboard.h" 11#include "jni/applets/software_keyboard.h"
11#include "jni/emu_window/emu_window.h" 12#include "jni/emu_window/emu_window.h"
12#include "video_core/rasterizer_interface.h" 13#include "video_core/rasterizer_interface.h"
@@ -29,7 +30,6 @@ public:
29 void SetNativeWindow(ANativeWindow* native_window); 30 void SetNativeWindow(ANativeWindow* native_window);
30 void SurfaceChanged(); 31 void SurfaceChanged();
31 32
32 int InstallFileToNand(std::string filename, std::string file_extension);
33 void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir, 33 void InitializeGpuDriver(const std::string& hook_lib_dir, const std::string& custom_driver_dir,
34 const std::string& custom_driver_name, 34 const std::string& custom_driver_name,
35 const std::string& file_redirect_dir); 35 const std::string& file_redirect_dir);
diff --git a/src/android/app/src/main/res/layout/dialog_progress_bar.xml b/src/android/app/src/main/res/layout/dialog_progress_bar.xml
index 0209ea082..e61aa5294 100644
--- a/src/android/app/src/main/res/layout/dialog_progress_bar.xml
+++ b/src/android/app/src/main/res/layout/dialog_progress_bar.xml
@@ -1,8 +1,30 @@
1<?xml version="1.0" encoding="utf-8"?> 1<?xml version="1.0" encoding="utf-8"?>
2<com.google.android.material.progressindicator.LinearProgressIndicator xmlns:android="http://schemas.android.com/apk/res/android" 2<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto" 3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 android:id="@+id/progress_bar"
5 android:layout_width="match_parent" 4 android:layout_width="match_parent"
6 android:layout_height="wrap_content" 5 android:layout_height="wrap_content"
7 android:padding="24dp" 6 android:orientation="vertical">
8 app:trackCornerRadius="4dp" /> 7
8 <com.google.android.material.textview.MaterialTextView
9 android:id="@+id/message"
10 style="@style/TextAppearance.Material3.BodyMedium"
11 android:layout_width="match_parent"
12 android:layout_height="wrap_content"
13 android:layout_marginHorizontal="24dp"
14 android:layout_marginTop="12dp"
15 android:layout_marginBottom="6dp"
16 android:ellipsize="marquee"
17 android:marqueeRepeatLimit="marquee_forever"
18 android:requiresFadingEdge="horizontal"
19 android:singleLine="true"
20 android:textAlignment="viewStart"
21 android:visibility="gone" />
22
23 <com.google.android.material.progressindicator.LinearProgressIndicator
24 android:id="@+id/progress_bar"
25 android:layout_width="match_parent"
26 android:layout_height="wrap_content"
27 android:padding="24dp"
28 app:trackCornerRadius="4dp" />
29
30</LinearLayout>
diff --git a/src/android/app/src/main/res/layout/list_item_addon.xml b/src/android/app/src/main/res/layout/list_item_addon.xml
index 74ca04ef1..3a1382fe2 100644
--- a/src/android/app/src/main/res/layout/list_item_addon.xml
+++ b/src/android/app/src/main/res/layout/list_item_addon.xml
@@ -14,12 +14,11 @@
14 android:id="@+id/text_container" 14 android:id="@+id/text_container"
15 android:layout_width="0dp" 15 android:layout_width="0dp"
16 android:layout_height="wrap_content" 16 android:layout_height="wrap_content"
17 android:layout_marginEnd="16dp"
18 android:orientation="vertical" 17 android:orientation="vertical"
19 app:layout_constraintBottom_toBottomOf="@+id/addon_switch" 18 android:layout_marginEnd="16dp"
20 app:layout_constraintEnd_toStartOf="@+id/addon_switch" 19 app:layout_constraintEnd_toStartOf="@+id/addon_checkbox"
21 app:layout_constraintStart_toStartOf="parent" 20 app:layout_constraintStart_toStartOf="parent"
22 app:layout_constraintTop_toTopOf="@+id/addon_switch"> 21 app:layout_constraintTop_toTopOf="parent">
23 22
24 <com.google.android.material.textview.MaterialTextView 23 <com.google.android.material.textview.MaterialTextView
25 android:id="@+id/title" 24 android:id="@+id/title"
@@ -42,16 +41,29 @@
42 41
43 </LinearLayout> 42 </LinearLayout>
44 43
45 <com.google.android.material.materialswitch.MaterialSwitch 44 <com.google.android.material.checkbox.MaterialCheckBox
46 android:id="@+id/addon_switch" 45 android:id="@+id/addon_checkbox"
47 android:layout_width="wrap_content" 46 android:layout_width="wrap_content"
48 android:layout_height="wrap_content" 47 android:layout_height="wrap_content"
49 android:focusable="true" 48 android:focusable="true"
50 android:gravity="center" 49 android:gravity="center"
51 android:nextFocusLeft="@id/addon_container" 50 android:layout_marginEnd="8dp"
52 app:layout_constraintBottom_toBottomOf="parent" 51 app:layout_constraintTop_toTopOf="@+id/text_container"
52 app:layout_constraintBottom_toBottomOf="@+id/text_container"
53 app:layout_constraintEnd_toStartOf="@+id/button_delete" />
54
55 <Button
56 android:id="@+id/button_delete"
57 style="@style/Widget.Material3.Button.IconButton"
58 android:layout_width="wrap_content"
59 android:layout_height="wrap_content"
60 android:layout_gravity="center_vertical"
61 android:contentDescription="@string/delete"
62 android:tooltipText="@string/delete"
63 app:icon="@drawable/ic_delete"
64 app:iconTint="?attr/colorControlNormal"
53 app:layout_constraintEnd_toEndOf="parent" 65 app:layout_constraintEnd_toEndOf="parent"
54 app:layout_constraintStart_toEndOf="@id/text_container" 66 app:layout_constraintTop_toTopOf="@+id/addon_checkbox"
55 app:layout_constraintTop_toTopOf="parent" /> 67 app:layout_constraintBottom_toBottomOf="@+id/addon_checkbox" />
56 68
57</androidx.constraintlayout.widget.ConstraintLayout> 69</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index 547752bda..db5b27d38 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -286,6 +286,7 @@
286 <string name="custom">Custom</string> 286 <string name="custom">Custom</string>
287 <string name="notice">Notice</string> 287 <string name="notice">Notice</string>
288 <string name="import_complete">Import complete</string> 288 <string name="import_complete">Import complete</string>
289 <string name="more_options">More options</string>
289 290
290 <!-- GPU driver installation --> 291 <!-- GPU driver installation -->
291 <string name="select_gpu_driver">Select GPU driver</string> 292 <string name="select_gpu_driver">Select GPU driver</string>
@@ -348,6 +349,8 @@
348 <string name="verifying_content">Verifying content…</string> 349 <string name="verifying_content">Verifying content…</string>
349 <string name="content_install_notice">Content install notice</string> 350 <string name="content_install_notice">Content install notice</string>
350 <string name="content_install_notice_description">The content that you selected does not match this game.\nInstall anyway?</string> 351 <string name="content_install_notice_description">The content that you selected does not match this game.\nInstall anyway?</string>
352 <string name="confirm_uninstall">Confirm uninstall</string>
353 <string name="confirm_uninstall_description">Are you sure you want to uninstall this addon?</string>
351 354
352 <!-- ROM loading errors --> 355 <!-- ROM loading errors -->
353 <string name="loader_error_encrypted">Your ROM is encrypted</string> 356 <string name="loader_error_encrypted">Your ROM is encrypted</string>
diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp
index 4a3dbc6a3..612122224 100644
--- a/src/core/file_sys/patch_manager.cpp
+++ b/src/core/file_sys/patch_manager.cpp
@@ -466,12 +466,12 @@ VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs
466 return romfs; 466 return romfs;
467} 467}
468 468
469PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile update_raw) const { 469std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
470 if (title_id == 0) { 470 if (title_id == 0) {
471 return {}; 471 return {};
472 } 472 }
473 473
474 std::map<std::string, std::string, std::less<>> out; 474 std::vector<Patch> out;
475 const auto& disabled = Settings::values.disabled_addons[title_id]; 475 const auto& disabled = Settings::values.disabled_addons[title_id];
476 476
477 // Game Updates 477 // Game Updates
@@ -482,20 +482,28 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u
482 482
483 const auto update_disabled = 483 const auto update_disabled =
484 std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend(); 484 std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
485 const auto update_label = update_disabled ? "[D] Update" : "Update"; 485 Patch update_patch = {.enabled = !update_disabled,
486 .name = "Update",
487 .version = "",
488 .type = PatchType::Update,
489 .program_id = title_id,
490 .title_id = title_id};
486 491
487 if (nacp != nullptr) { 492 if (nacp != nullptr) {
488 out.insert_or_assign(update_label, nacp->GetVersionString()); 493 update_patch.version = nacp->GetVersionString();
494 out.push_back(update_patch);
489 } else { 495 } else {
490 if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) { 496 if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
491 const auto meta_ver = content_provider.GetEntryVersion(update_tid); 497 const auto meta_ver = content_provider.GetEntryVersion(update_tid);
492 if (meta_ver.value_or(0) == 0) { 498 if (meta_ver.value_or(0) == 0) {
493 out.insert_or_assign(update_label, ""); 499 out.push_back(update_patch);
494 } else { 500 } else {
495 out.insert_or_assign(update_label, FormatTitleVersion(*meta_ver)); 501 update_patch.version = FormatTitleVersion(*meta_ver);
502 out.push_back(update_patch);
496 } 503 }
497 } else if (update_raw != nullptr) { 504 } else if (update_raw != nullptr) {
498 out.insert_or_assign(update_label, "PACKED"); 505 update_patch.version = "PACKED";
506 out.push_back(update_patch);
499 } 507 }
500 } 508 }
501 509
@@ -539,7 +547,12 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u
539 547
540 const auto mod_disabled = 548 const auto mod_disabled =
541 std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end(); 549 std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end();
542 out.insert_or_assign(mod_disabled ? "[D] " + mod->GetName() : mod->GetName(), types); 550 out.push_back({.enabled = !mod_disabled,
551 .name = mod->GetName(),
552 .version = types,
553 .type = PatchType::Mod,
554 .program_id = title_id,
555 .title_id = title_id});
543 } 556 }
544 } 557 }
545 558
@@ -557,7 +570,12 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u
557 if (!types.empty()) { 570 if (!types.empty()) {
558 const auto mod_disabled = 571 const auto mod_disabled =
559 std::find(disabled.begin(), disabled.end(), "SDMC") != disabled.end(); 572 std::find(disabled.begin(), disabled.end(), "SDMC") != disabled.end();
560 out.insert_or_assign(mod_disabled ? "[D] SDMC" : "SDMC", types); 573 out.push_back({.enabled = !mod_disabled,
574 .name = "SDMC",
575 .version = types,
576 .type = PatchType::Mod,
577 .program_id = title_id,
578 .title_id = title_id});
561 } 579 }
562 } 580 }
563 581
@@ -584,7 +602,12 @@ PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile u
584 602
585 const auto dlc_disabled = 603 const auto dlc_disabled =
586 std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end(); 604 std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end();
587 out.insert_or_assign(dlc_disabled ? "[D] DLC" : "DLC", std::move(list)); 605 out.push_back({.enabled = !dlc_disabled,
606 .name = "DLC",
607 .version = std::move(list),
608 .type = PatchType::DLC,
609 .program_id = title_id,
610 .title_id = dlc_match.back().title_id});
588 } 611 }
589 612
590 return out; 613 return out;
diff --git a/src/core/file_sys/patch_manager.h b/src/core/file_sys/patch_manager.h
index 03e9c7301..2601b8217 100644
--- a/src/core/file_sys/patch_manager.h
+++ b/src/core/file_sys/patch_manager.h
@@ -26,12 +26,22 @@ class ContentProvider;
26class NCA; 26class NCA;
27class NACP; 27class NACP;
28 28
29enum class PatchType { Update, DLC, Mod };
30
31struct Patch {
32 bool enabled;
33 std::string name;
34 std::string version;
35 PatchType type;
36 u64 program_id;
37 u64 title_id;
38};
39
29// A centralized class to manage patches to games. 40// A centralized class to manage patches to games.
30class PatchManager { 41class PatchManager {
31public: 42public:
32 using BuildID = std::array<u8, 0x20>; 43 using BuildID = std::array<u8, 0x20>;
33 using Metadata = std::pair<std::unique_ptr<NACP>, VirtualFile>; 44 using Metadata = std::pair<std::unique_ptr<NACP>, VirtualFile>;
34 using PatchVersionNames = std::map<std::string, std::string, std::less<>>;
35 45
36 explicit PatchManager(u64 title_id_, 46 explicit PatchManager(u64 title_id_,
37 const Service::FileSystem::FileSystemController& fs_controller_, 47 const Service::FileSystem::FileSystemController& fs_controller_,
@@ -66,9 +76,8 @@ public:
66 VirtualFile packed_update_raw = nullptr, 76 VirtualFile packed_update_raw = nullptr,
67 bool apply_layeredfs = true) const; 77 bool apply_layeredfs = true) const;
68 78
69 // Returns a vector of pairs between patch names and patch versions. 79 // Returns a vector of patches
70 // i.e. Update 3.2.2 will return {"Update", "3.2.2"} 80 [[nodiscard]] std::vector<Patch> GetPatches(VirtualFile update_raw = nullptr) const;
71 [[nodiscard]] PatchVersionNames GetPatchVersionNames(VirtualFile update_raw = nullptr) const;
72 81
73 // If the game update exists, returns the u32 version field in its Meta-type NCA. If that fails, 82 // If the game update exists, returns the u32 version field in its Meta-type NCA. If that fails,
74 // it will fallback to the Meta-type NCA of the base game. If that fails, the result will be 83 // it will fallback to the Meta-type NCA of the base game. If that fails, the result will be
diff --git a/src/frontend_common/CMakeLists.txt b/src/frontend_common/CMakeLists.txt
index 22e9337c4..94d8cc4c3 100644
--- a/src/frontend_common/CMakeLists.txt
+++ b/src/frontend_common/CMakeLists.txt
@@ -4,6 +4,7 @@
4add_library(frontend_common STATIC 4add_library(frontend_common STATIC
5 config.cpp 5 config.cpp
6 config.h 6 config.h
7 content_manager.h
7) 8)
8 9
9create_target_directory_groups(frontend_common) 10create_target_directory_groups(frontend_common)
diff --git a/src/frontend_common/content_manager.h b/src/frontend_common/content_manager.h
new file mode 100644
index 000000000..23f2979db
--- /dev/null
+++ b/src/frontend_common/content_manager.h
@@ -0,0 +1,238 @@
1// SPDX-FileCopyrightText: 2024 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4#pragma once
5
6#include <boost/algorithm/string.hpp>
7#include "common/common_types.h"
8#include "common/literals.h"
9#include "core/core.h"
10#include "core/file_sys/common_funcs.h"
11#include "core/file_sys/content_archive.h"
12#include "core/file_sys/mode.h"
13#include "core/file_sys/nca_metadata.h"
14#include "core/file_sys/registered_cache.h"
15#include "core/file_sys/submission_package.h"
16#include "core/hle/service/filesystem/filesystem.h"
17#include "core/loader/loader.h"
18
19namespace ContentManager {
20
21enum class InstallResult {
22 Success,
23 Overwrite,
24 Failure,
25 BaseInstallAttempted,
26};
27
28/**
29 * \brief Removes a single installed DLC
30 * \param fs_controller [FileSystemController] reference from the Core::System instance
31 * \param title_id Unique title ID representing the DLC which will be removed
32 * \return 'true' if successful
33 */
34inline bool RemoveDLC(const Service::FileSystem::FileSystemController& fs_controller,
35 const u64 title_id) {
36 return fs_controller.GetUserNANDContents()->RemoveExistingEntry(title_id) ||
37 fs_controller.GetSDMCContents()->RemoveExistingEntry(title_id);
38}
39
40/**
41 * \brief Removes all DLC for a game
42 * \param system Raw pointer to the system instance
43 * \param program_id Program ID for the game that will have all of its DLC removed
44 * \return Number of DLC removed
45 */
46inline size_t RemoveAllDLC(Core::System* system, const u64 program_id) {
47 size_t count{};
48 const auto& fs_controller = system->GetFileSystemController();
49 const auto dlc_entries = system->GetContentProvider().ListEntriesFilter(
50 FileSys::TitleType::AOC, FileSys::ContentRecordType::Data);
51 std::vector<u64> program_dlc_entries;
52
53 for (const auto& entry : dlc_entries) {
54 if (FileSys::GetBaseTitleID(entry.title_id) == program_id) {
55 program_dlc_entries.push_back(entry.title_id);
56 }
57 }
58
59 for (const auto& entry : program_dlc_entries) {
60 if (RemoveDLC(fs_controller, entry)) {
61 ++count;
62 }
63 }
64 return count;
65}
66
67/**
68 * \brief Removes the installed update for a game
69 * \param fs_controller [FileSystemController] reference from the Core::System instance
70 * \param program_id Program ID for the game that will have its installed update removed
71 * \return 'true' if successful
72 */
73inline bool RemoveUpdate(const Service::FileSystem::FileSystemController& fs_controller,
74 const u64 program_id) {
75 const auto update_id = program_id | 0x800;
76 return fs_controller.GetUserNANDContents()->RemoveExistingEntry(update_id) ||
77 fs_controller.GetSDMCContents()->RemoveExistingEntry(update_id);
78}
79
80/**
81 * \brief Removes the base content for a game
82 * \param fs_controller [FileSystemController] reference from the Core::System instance
83 * \param program_id Program ID for the game that will have its base content removed
84 * \return 'true' if successful
85 */
86inline bool RemoveBaseContent(const Service::FileSystem::FileSystemController& fs_controller,
87 const u64 program_id) {
88 return fs_controller.GetUserNANDContents()->RemoveExistingEntry(program_id) ||
89 fs_controller.GetSDMCContents()->RemoveExistingEntry(program_id);
90}
91
92/**
93 * \brief Removes a mod for a game
94 * \param fs_controller [FileSystemController] reference from the Core::System instance
95 * \param program_id Program ID for the game where [mod_name] will be removed
96 * \param mod_name The name of a mod as given by FileSys::PatchManager::GetPatches. This corresponds
97 * with the name of the mod's directory in a game's load folder.
98 * \return 'true' if successful
99 */
100inline bool RemoveMod(const Service::FileSystem::FileSystemController& fs_controller,
101 const u64 program_id, const std::string& mod_name) {
102 // Check general Mods (LayeredFS and IPS)
103 const auto mod_dir = fs_controller.GetModificationLoadRoot(program_id);
104 if (mod_dir != nullptr) {
105 return mod_dir->DeleteSubdirectoryRecursive(mod_name);
106 }
107
108 // Check SDMC mod directory (RomFS LayeredFS)
109 const auto sdmc_mod_dir = fs_controller.GetSDMCModificationLoadRoot(program_id);
110 if (sdmc_mod_dir != nullptr) {
111 return sdmc_mod_dir->DeleteSubdirectoryRecursive(mod_name);
112 }
113
114 return false;
115}
116
117/**
118 * \brief Installs an NSP
119 * \param system Raw pointer to the system instance
120 * \param vfs Raw pointer to the VfsFilesystem instance in Core::System
121 * \param filename Path to the NSP file
122 * \param callback Optional callback to report the progress of the installation. The first size_t
123 * parameter is the total size of the virtual file and the second is the current progress. If you
124 * return false to the callback, it will cancel the installation as soon as possible.
125 * \return [InstallResult] representing how the installation finished
126 */
127inline InstallResult InstallNSP(
128 Core::System* system, FileSys::VfsFilesystem* vfs, const std::string& filename,
129 const std::function<bool(size_t, size_t)>& callback = std::function<bool(size_t, size_t)>()) {
130 const auto copy = [callback](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
131 std::size_t block_size) {
132 if (src == nullptr || dest == nullptr) {
133 return false;
134 }
135 if (!dest->Resize(src->GetSize())) {
136 return false;
137 }
138
139 using namespace Common::Literals;
140 std::vector<u8> buffer(1_MiB);
141
142 for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
143 if (callback(src->GetSize(), i)) {
144 dest->Resize(0);
145 return false;
146 }
147 const auto read = src->Read(buffer.data(), buffer.size(), i);
148 dest->Write(buffer.data(), read, i);
149 }
150 return true;
151 };
152
153 std::shared_ptr<FileSys::NSP> nsp;
154 FileSys::VirtualFile file = vfs->OpenFile(filename, FileSys::Mode::Read);
155 if (boost::to_lower_copy(file->GetName()).ends_with(std::string("nsp"))) {
156 nsp = std::make_shared<FileSys::NSP>(file);
157 if (nsp->IsExtractedType()) {
158 return InstallResult::Failure;
159 }
160 } else {
161 return InstallResult::Failure;
162 }
163
164 if (nsp->GetStatus() != Loader::ResultStatus::Success) {
165 return InstallResult::Failure;
166 }
167 const auto res =
168 system->GetFileSystemController().GetUserNANDContents()->InstallEntry(*nsp, true, copy);
169 switch (res) {
170 case FileSys::InstallResult::Success:
171 return InstallResult::Success;
172 case FileSys::InstallResult::OverwriteExisting:
173 return InstallResult::Overwrite;
174 case FileSys::InstallResult::ErrorBaseInstall:
175 return InstallResult::BaseInstallAttempted;
176 default:
177 return InstallResult::Failure;
178 }
179}
180
181/**
182 * \brief Installs an NCA
183 * \param vfs Raw pointer to the VfsFilesystem instance in Core::System
184 * \param filename Path to the NCA file
185 * \param registered_cache Raw pointer to the registered cache that the NCA will be installed to
186 * \param title_type Type of NCA package to install
187 * \param callback Optional callback to report the progress of the installation. The first size_t
188 * parameter is the total size of the virtual file and the second is the current progress. If you
189 * return false to the callback, it will cancel the installation as soon as possible.
190 * \return [InstallResult] representing how the installation finished
191 */
192inline InstallResult InstallNCA(
193 FileSys::VfsFilesystem* vfs, const std::string& filename,
194 FileSys::RegisteredCache* registered_cache, const FileSys::TitleType title_type,
195 const std::function<bool(size_t, size_t)>& callback = std::function<bool(size_t, size_t)>()) {
196 const auto copy = [callback](const FileSys::VirtualFile& src, const FileSys::VirtualFile& dest,
197 std::size_t block_size) {
198 if (src == nullptr || dest == nullptr) {
199 return false;
200 }
201 if (!dest->Resize(src->GetSize())) {
202 return false;
203 }
204
205 using namespace Common::Literals;
206 std::vector<u8> buffer(1_MiB);
207
208 for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
209 if (callback(src->GetSize(), i)) {
210 dest->Resize(0);
211 return false;
212 }
213 const auto read = src->Read(buffer.data(), buffer.size(), i);
214 dest->Write(buffer.data(), read, i);
215 }
216 return true;
217 };
218
219 const auto nca = std::make_shared<FileSys::NCA>(vfs->OpenFile(filename, FileSys::Mode::Read));
220 const auto id = nca->GetStatus();
221
222 // Game updates necessary are missing base RomFS
223 if (id != Loader::ResultStatus::Success &&
224 id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
225 return InstallResult::Failure;
226 }
227
228 const auto res = registered_cache->InstallEntry(*nca, title_type, true, copy);
229 if (res == FileSys::InstallResult::Success) {
230 return InstallResult::Success;
231 } else if (res == FileSys::InstallResult::OverwriteExisting) {
232 return InstallResult::Overwrite;
233 } else {
234 return InstallResult::Failure;
235 }
236}
237
238} // namespace ContentManager
diff --git a/src/yuzu/configuration/configure_per_game_addons.cpp b/src/yuzu/configuration/configure_per_game_addons.cpp
index 140a7fe5d..568775027 100644
--- a/src/yuzu/configuration/configure_per_game_addons.cpp
+++ b/src/yuzu/configuration/configure_per_game_addons.cpp
@@ -122,9 +122,8 @@ void ConfigurePerGameAddons::LoadConfiguration() {
122 122
123 const auto& disabled = Settings::values.disabled_addons[title_id]; 123 const auto& disabled = Settings::values.disabled_addons[title_id];
124 124
125 for (const auto& patch : pm.GetPatchVersionNames(update_raw)) { 125 for (const auto& patch : pm.GetPatches(update_raw)) {
126 const auto name = 126 const auto name = QString::fromStdString(patch.name);
127 QString::fromStdString(patch.first).replace(QStringLiteral("[D] "), QString{});
128 127
129 auto* const first_item = new QStandardItem; 128 auto* const first_item = new QStandardItem;
130 first_item->setText(name); 129 first_item->setText(name);
@@ -136,7 +135,7 @@ void ConfigurePerGameAddons::LoadConfiguration() {
136 first_item->setCheckState(patch_disabled ? Qt::Unchecked : Qt::Checked); 135 first_item->setCheckState(patch_disabled ? Qt::Unchecked : Qt::Checked);
137 136
138 list_items.push_back(QList<QStandardItem*>{ 137 list_items.push_back(QList<QStandardItem*>{
139 first_item, new QStandardItem{QString::fromStdString(patch.second)}}); 138 first_item, new QStandardItem{QString::fromStdString(patch.version)}});
140 item_model->appendRow(list_items.back()); 139 item_model->appendRow(list_items.back());
141 } 140 }
142 141
diff --git a/src/yuzu/game_list_worker.cpp b/src/yuzu/game_list_worker.cpp
index dc006832e..9747e3fb3 100644
--- a/src/yuzu/game_list_worker.cpp
+++ b/src/yuzu/game_list_worker.cpp
@@ -164,18 +164,19 @@ QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager,
164 QString out; 164 QString out;
165 FileSys::VirtualFile update_raw; 165 FileSys::VirtualFile update_raw;
166 loader.ReadUpdateRaw(update_raw); 166 loader.ReadUpdateRaw(update_raw);
167 for (const auto& kv : patch_manager.GetPatchVersionNames(update_raw)) { 167 for (const auto& patch : patch_manager.GetPatches(update_raw)) {
168 const bool is_update = kv.first == "Update" || kv.first == "[D] Update"; 168 const bool is_update = patch.name == "Update";
169 if (!updatable && is_update) { 169 if (!updatable && is_update) {
170 continue; 170 continue;
171 } 171 }
172 172
173 const QString type = QString::fromStdString(kv.first); 173 const QString type =
174 QString::fromStdString(patch.enabled ? patch.name : "[D] " + patch.name);
174 175
175 if (kv.second.empty()) { 176 if (patch.version.empty()) {
176 out.append(QStringLiteral("%1\n").arg(type)); 177 out.append(QStringLiteral("%1\n").arg(type));
177 } else { 178 } else {
178 auto ver = kv.second; 179 auto ver = patch.version;
179 180
180 // Display container name for packed updates 181 // Display container name for packed updates
181 if (is_update && ver == "PACKED") { 182 if (is_update && ver == "PACKED") {
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index 3c562e3b2..05bd4174c 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -47,6 +47,7 @@
47#include "core/hle/service/am/applet_oe.h" 47#include "core/hle/service/am/applet_oe.h"
48#include "core/hle/service/am/applets/applets.h" 48#include "core/hle/service/am/applets/applets.h"
49#include "core/hle/service/set/system_settings_server.h" 49#include "core/hle/service/set/system_settings_server.h"
50#include "frontend_common/content_manager.h"
50#include "hid_core/frontend/emulated_controller.h" 51#include "hid_core/frontend/emulated_controller.h"
51#include "hid_core/hid_core.h" 52#include "hid_core/hid_core.h"
52#include "yuzu/multiplayer/state.h" 53#include "yuzu/multiplayer/state.h"
@@ -2476,10 +2477,8 @@ void GMainWindow::OnGameListRemoveInstalledEntry(u64 program_id, InstalledEntryT
2476} 2477}
2477 2478
2478void GMainWindow::RemoveBaseContent(u64 program_id, InstalledEntryType type) { 2479void GMainWindow::RemoveBaseContent(u64 program_id, InstalledEntryType type) {
2479 const auto& fs_controller = system->GetFileSystemController(); 2480 const auto res =
2480 const auto res = fs_controller.GetUserNANDContents()->RemoveExistingEntry(program_id) || 2481 ContentManager::RemoveBaseContent(system->GetFileSystemController(), program_id);
2481 fs_controller.GetSDMCContents()->RemoveExistingEntry(program_id);
2482
2483 if (res) { 2482 if (res) {
2484 QMessageBox::information(this, tr("Successfully Removed"), 2483 QMessageBox::information(this, tr("Successfully Removed"),
2485 tr("Successfully removed the installed base game.")); 2484 tr("Successfully removed the installed base game."));
@@ -2491,11 +2490,7 @@ void GMainWindow::RemoveBaseContent(u64 program_id, InstalledEntryType type) {
2491} 2490}
2492 2491
2493void GMainWindow::RemoveUpdateContent(u64 program_id, InstalledEntryType type) { 2492void GMainWindow::RemoveUpdateContent(u64 program_id, InstalledEntryType type) {
2494 const auto update_id = program_id | 0x800; 2493 const auto res = ContentManager::RemoveUpdate(system->GetFileSystemController(), program_id);
2495 const auto& fs_controller = system->GetFileSystemController();
2496 const auto res = fs_controller.GetUserNANDContents()->RemoveExistingEntry(update_id) ||
2497 fs_controller.GetSDMCContents()->RemoveExistingEntry(update_id);
2498
2499 if (res) { 2494 if (res) {
2500 QMessageBox::information(this, tr("Successfully Removed"), 2495 QMessageBox::information(this, tr("Successfully Removed"),
2501 tr("Successfully removed the installed update.")); 2496 tr("Successfully removed the installed update."));
@@ -2506,22 +2501,7 @@ void GMainWindow::RemoveUpdateContent(u64 program_id, InstalledEntryType type) {
2506} 2501}
2507 2502
2508void GMainWindow::RemoveAddOnContent(u64 program_id, InstalledEntryType type) { 2503void GMainWindow::RemoveAddOnContent(u64 program_id, InstalledEntryType type) {
2509 u32 count{}; 2504 const size_t count = ContentManager::RemoveAllDLC(system.get(), program_id);
2510 const auto& fs_controller = system->GetFileSystemController();
2511 const auto dlc_entries = system->GetContentProvider().ListEntriesFilter(
2512 FileSys::TitleType::AOC, FileSys::ContentRecordType::Data);
2513
2514 for (const auto& entry : dlc_entries) {
2515 if (FileSys::GetBaseTitleID(entry.title_id) == program_id) {
2516 const auto res =
2517 fs_controller.GetUserNANDContents()->RemoveExistingEntry(entry.title_id) ||
2518 fs_controller.GetSDMCContents()->RemoveExistingEntry(entry.title_id);
2519 if (res) {
2520 ++count;
2521 }
2522 }
2523 }
2524
2525 if (count == 0) { 2505 if (count == 0) {
2526 QMessageBox::warning(this, GetGameListErrorRemoving(type), 2506 QMessageBox::warning(this, GetGameListErrorRemoving(type),
2527 tr("There are no DLC installed for this title.")); 2507 tr("There are no DLC installed for this title."));
@@ -3290,12 +3270,21 @@ void GMainWindow::OnMenuInstallToNAND() {
3290 install_progress->setLabelText( 3270 install_progress->setLabelText(
3291 tr("Installing file \"%1\"...").arg(QFileInfo(file).fileName())); 3271 tr("Installing file \"%1\"...").arg(QFileInfo(file).fileName()));
3292 3272
3293 QFuture<InstallResult> future; 3273 QFuture<ContentManager::InstallResult> future;
3294 InstallResult result; 3274 ContentManager::InstallResult result;
3295 3275
3296 if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) { 3276 if (file.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
3297 3277 const auto progress_callback = [this](size_t size, size_t progress) {
3298 future = QtConcurrent::run([this, &file] { return InstallNSP(file); }); 3278 emit UpdateInstallProgress();
3279 if (install_progress->wasCanceled()) {
3280 return true;
3281 }
3282 return false;
3283 };
3284 future = QtConcurrent::run([this, &file, progress_callback] {
3285 return ContentManager::InstallNSP(system.get(), vfs.get(), file.toStdString(),
3286 progress_callback);
3287 });
3299 3288
3300 while (!future.isFinished()) { 3289 while (!future.isFinished()) {
3301 QCoreApplication::processEvents(); 3290 QCoreApplication::processEvents();
@@ -3311,16 +3300,16 @@ void GMainWindow::OnMenuInstallToNAND() {
3311 std::this_thread::sleep_for(std::chrono::milliseconds(10)); 3300 std::this_thread::sleep_for(std::chrono::milliseconds(10));
3312 3301
3313 switch (result) { 3302 switch (result) {
3314 case InstallResult::Success: 3303 case ContentManager::InstallResult::Success:
3315 new_files.append(QFileInfo(file).fileName()); 3304 new_files.append(QFileInfo(file).fileName());
3316 break; 3305 break;
3317 case InstallResult::Overwrite: 3306 case ContentManager::InstallResult::Overwrite:
3318 overwritten_files.append(QFileInfo(file).fileName()); 3307 overwritten_files.append(QFileInfo(file).fileName());
3319 break; 3308 break;
3320 case InstallResult::Failure: 3309 case ContentManager::InstallResult::Failure:
3321 failed_files.append(QFileInfo(file).fileName()); 3310 failed_files.append(QFileInfo(file).fileName());
3322 break; 3311 break;
3323 case InstallResult::BaseInstallAttempted: 3312 case ContentManager::InstallResult::BaseInstallAttempted:
3324 failed_files.append(QFileInfo(file).fileName()); 3313 failed_files.append(QFileInfo(file).fileName());
3325 detected_base_install = true; 3314 detected_base_install = true;
3326 break; 3315 break;
@@ -3354,96 +3343,7 @@ void GMainWindow::OnMenuInstallToNAND() {
3354 ui->action_Install_File_NAND->setEnabled(true); 3343 ui->action_Install_File_NAND->setEnabled(true);
3355} 3344}
3356 3345
3357InstallResult GMainWindow::InstallNSP(const QString& filename) { 3346ContentManager::InstallResult GMainWindow::InstallNCA(const QString& filename) {
3358 const auto qt_raw_copy = [this](const FileSys::VirtualFile& src,
3359 const FileSys::VirtualFile& dest, std::size_t block_size) {
3360 if (src == nullptr || dest == nullptr) {
3361 return false;
3362 }
3363 if (!dest->Resize(src->GetSize())) {
3364 return false;
3365 }
3366
3367 std::vector<u8> buffer(CopyBufferSize);
3368
3369 for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
3370 if (install_progress->wasCanceled()) {
3371 dest->Resize(0);
3372 return false;
3373 }
3374
3375 emit UpdateInstallProgress();
3376
3377 const auto read = src->Read(buffer.data(), buffer.size(), i);
3378 dest->Write(buffer.data(), read, i);
3379 }
3380 return true;
3381 };
3382
3383 std::shared_ptr<FileSys::NSP> nsp;
3384 if (filename.endsWith(QStringLiteral("nsp"), Qt::CaseInsensitive)) {
3385 nsp = std::make_shared<FileSys::NSP>(
3386 vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
3387 if (nsp->IsExtractedType()) {
3388 return InstallResult::Failure;
3389 }
3390 } else {
3391 return InstallResult::Failure;
3392 }
3393
3394 if (nsp->GetStatus() != Loader::ResultStatus::Success) {
3395 return InstallResult::Failure;
3396 }
3397 const auto res = system->GetFileSystemController().GetUserNANDContents()->InstallEntry(
3398 *nsp, true, qt_raw_copy);
3399 switch (res) {
3400 case FileSys::InstallResult::Success:
3401 return InstallResult::Success;
3402 case FileSys::InstallResult::OverwriteExisting:
3403 return InstallResult::Overwrite;
3404 case FileSys::InstallResult::ErrorBaseInstall:
3405 return InstallResult::BaseInstallAttempted;
3406 default:
3407 return InstallResult::Failure;
3408 }
3409}
3410
3411InstallResult GMainWindow::InstallNCA(const QString& filename) {
3412 const auto qt_raw_copy = [this](const FileSys::VirtualFile& src,
3413 const FileSys::VirtualFile& dest, std::size_t block_size) {
3414 if (src == nullptr || dest == nullptr) {
3415 return false;
3416 }
3417 if (!dest->Resize(src->GetSize())) {
3418 return false;
3419 }
3420
3421 std::vector<u8> buffer(CopyBufferSize);
3422
3423 for (std::size_t i = 0; i < src->GetSize(); i += buffer.size()) {
3424 if (install_progress->wasCanceled()) {
3425 dest->Resize(0);
3426 return false;
3427 }
3428
3429 emit UpdateInstallProgress();
3430
3431 const auto read = src->Read(buffer.data(), buffer.size(), i);
3432 dest->Write(buffer.data(), read, i);
3433 }
3434 return true;
3435 };
3436
3437 const auto nca =
3438 std::make_shared<FileSys::NCA>(vfs->OpenFile(filename.toStdString(), FileSys::Mode::Read));
3439 const auto id = nca->GetStatus();
3440
3441 // Game updates necessary are missing base RomFS
3442 if (id != Loader::ResultStatus::Success &&
3443 id != Loader::ResultStatus::ErrorMissingBKTRBaseRomFS) {
3444 return InstallResult::Failure;
3445 }
3446
3447 const QStringList tt_options{tr("System Application"), 3347 const QStringList tt_options{tr("System Application"),
3448 tr("System Archive"), 3348 tr("System Archive"),
3449 tr("System Application Update"), 3349 tr("System Application Update"),
@@ -3464,7 +3364,7 @@ InstallResult GMainWindow::InstallNCA(const QString& filename) {
3464 if (!ok || index == -1) { 3364 if (!ok || index == -1) {
3465 QMessageBox::warning(this, tr("Failed to Install"), 3365 QMessageBox::warning(this, tr("Failed to Install"),
3466 tr("The title type you selected for the NCA is invalid.")); 3366 tr("The title type you selected for the NCA is invalid."));
3467 return InstallResult::Failure; 3367 return ContentManager::InstallResult::Failure;
3468 } 3368 }
3469 3369
3470 // If index is equal to or past Game, add the jump in TitleType. 3370 // If index is equal to or past Game, add the jump in TitleType.
@@ -3478,15 +3378,15 @@ InstallResult GMainWindow::InstallNCA(const QString& filename) {
3478 auto* registered_cache = is_application ? fs_controller.GetUserNANDContents() 3378 auto* registered_cache = is_application ? fs_controller.GetUserNANDContents()
3479 : fs_controller.GetSystemNANDContents(); 3379 : fs_controller.GetSystemNANDContents();
3480 3380
3481 const auto res = registered_cache->InstallEntry(*nca, static_cast<FileSys::TitleType>(index), 3381 const auto progress_callback = [this](size_t size, size_t progress) {
3482 true, qt_raw_copy); 3382 emit UpdateInstallProgress();
3483 if (res == FileSys::InstallResult::Success) { 3383 if (install_progress->wasCanceled()) {
3484 return InstallResult::Success; 3384 return true;
3485 } else if (res == FileSys::InstallResult::OverwriteExisting) { 3385 }
3486 return InstallResult::Overwrite; 3386 return false;
3487 } else { 3387 };
3488 return InstallResult::Failure; 3388 return ContentManager::InstallNCA(vfs.get(), filename.toStdString(), registered_cache,
3489 } 3389 static_cast<FileSys::TitleType>(index), progress_callback);
3490} 3390}
3491 3391
3492void GMainWindow::OnMenuRecentFile() { 3392void GMainWindow::OnMenuRecentFile() {
diff --git a/src/yuzu/main.h b/src/yuzu/main.h
index f3276da64..280fae5c3 100644
--- a/src/yuzu/main.h
+++ b/src/yuzu/main.h
@@ -16,6 +16,7 @@
16#include "common/announce_multiplayer_room.h" 16#include "common/announce_multiplayer_room.h"
17#include "common/common_types.h" 17#include "common/common_types.h"
18#include "configuration/qt_config.h" 18#include "configuration/qt_config.h"
19#include "frontend_common/content_manager.h"
19#include "input_common/drivers/tas_input.h" 20#include "input_common/drivers/tas_input.h"
20#include "yuzu/compatibility_list.h" 21#include "yuzu/compatibility_list.h"
21#include "yuzu/hotkeys.h" 22#include "yuzu/hotkeys.h"
@@ -124,13 +125,6 @@ enum class EmulatedDirectoryTarget {
124 SDMC, 125 SDMC,
125}; 126};
126 127
127enum class InstallResult {
128 Success,
129 Overwrite,
130 Failure,
131 BaseInstallAttempted,
132};
133
134enum class ReinitializeKeyBehavior { 128enum class ReinitializeKeyBehavior {
135 NoWarning, 129 NoWarning,
136 Warning, 130 Warning,
@@ -427,8 +421,7 @@ private:
427 void RemoveCacheStorage(u64 program_id); 421 void RemoveCacheStorage(u64 program_id);
428 bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id, 422 bool SelectRomFSDumpTarget(const FileSys::ContentProvider&, u64 program_id,
429 u64* selected_title_id, u8* selected_content_record_type); 423 u64* selected_title_id, u8* selected_content_record_type);
430 InstallResult InstallNSP(const QString& filename); 424 ContentManager::InstallResult InstallNCA(const QString& filename);
431 InstallResult InstallNCA(const QString& filename);
432 void MigrateConfigFiles(); 425 void MigrateConfigFiles();
433 void UpdateWindowTitle(std::string_view title_name = {}, std::string_view title_version = {}, 426 void UpdateWindowTitle(std::string_view title_name = {}, std::string_view title_version = {},
434 std::string_view gpu_vendor = {}); 427 std::string_view gpu_vendor = {});