summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt47
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt20
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt52
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt6
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt18
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt140
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt17
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt54
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt22
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt22
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt53
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt22
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt51
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt22
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt23
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt5
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt44
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt17
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt30
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt7
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt24
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt42
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt17
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt40
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt1
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt16
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt1
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt16
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt16
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt214
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ContentTypeSelectionDialogFragment.kt68
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriverManagerFragment.kt14
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt18
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt51
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt6
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt148
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt456
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt17
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/InstallableFragment.kt24
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt61
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt37
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt7
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt12
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt5
-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.kt83
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt175
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt43
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt36
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt75
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt22
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt13
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt341
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddonUtil.kt8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt32
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt22
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameMetadata.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GpuDriverHelper.kt10
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt4
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt110
-rw-r--r--src/android/app/src/main/jni/android_config.cpp19
-rw-r--r--src/android/app/src/main/jni/android_config.h2
-rw-r--r--src/android/app/src/main/jni/android_settings.h3
-rw-r--r--src/android/app/src/main/jni/game_metadata.cpp39
-rw-r--r--src/android/app/src/main/jni/id_cache.cpp95
-rw-r--r--src/android/app/src/main/jni/id_cache.h15
-rw-r--r--src/android/app/src/main/jni/native.cpp117
-rw-r--r--src/android/app/src/main/jni/native.h3
-rw-r--r--src/android/app/src/main/jni/native_config.cpp217
-rw-r--r--src/android/app/src/main/res/drawable/ic_save.xml9
-rw-r--r--src/android/app/src/main/res/layout-w1000dp/card_installable_icon.xml82
-rw-r--r--src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml99
-rw-r--r--src/android/app/src/main/res/layout/card_installable.xml3
-rw-r--r--src/android/app/src/main/res/layout/card_installable_icon.xml89
-rw-r--r--src/android/app/src/main/res/layout/card_simple_outlined.xml (renamed from src/android/app/src/main/res/layout/card_applet_option.xml)20
-rw-r--r--src/android/app/src/main/res/layout/fragment_addons.xml47
-rw-r--r--src/android/app/src/main/res/layout/fragment_game_info.xml125
-rw-r--r--src/android/app/src/main/res/layout/fragment_game_properties.xml86
-rw-r--r--src/android/app/src/main/res/layout/list_item_addon.xml57
-rw-r--r--src/android/app/src/main/res/layout/list_item_setting.xml10
-rw-r--r--src/android/app/src/main/res/layout/list_item_setting_switch.xml75
-rw-r--r--src/android/app/src/main/res/menu/menu_in_game.xml5
-rw-r--r--src/android/app/src/main/res/navigation/emulation_navigation.xml4
-rw-r--r--src/android/app/src/main/res/navigation/home_navigation.xml48
-rw-r--r--src/android/app/src/main/res/values/dimens.xml2
-rw-r--r--src/android/app/src/main/res/values/strings.xml47
-rw-r--r--src/common/settings.cpp2
-rw-r--r--src/common/settings.h10
-rw-r--r--src/common/settings_common.h1
-rw-r--r--src/common/settings_setting.h5
-rw-r--r--src/core/hle/service/nvnflinger/fb_share_buffer_manager.cpp3
-rw-r--r--src/core/hle/service/nvnflinger/nvnflinger.cpp42
-rw-r--r--src/core/hle/service/nvnflinger/nvnflinger.h11
-rw-r--r--src/core/hle/service/vi/display/vi_display.cpp25
-rw-r--r--src/core/hle/service/vi/display/vi_display.h15
-rw-r--r--src/core/hle/service/vi/layer/vi_layer.cpp4
-rw-r--r--src/core/hle/service/vi/layer/vi_layer.h13
-rw-r--r--src/core/hle/service/vi/vi.cpp3
-rw-r--r--src/frontend_common/config.cpp1
-rw-r--r--src/video_core/renderer_opengl/gl_buffer_cache.cpp2
-rw-r--r--src/video_core/renderer_vulkan/vk_present_manager.cpp13
-rw-r--r--src/video_core/renderer_vulkan/vk_present_manager.h2
119 files changed, 3598 insertions, 918 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 e0f01127c..010c44951 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
@@ -230,8 +230,6 @@ object NativeLibrary {
230 */ 230 */
231 external fun onTouchReleased(finger_id: Int) 231 external fun onTouchReleased(finger_id: Int)
232 232
233 external fun initGameIni(gameID: String?)
234
235 external fun setAppDirectory(directory: String) 233 external fun setAppDirectory(directory: String)
236 234
237 /** 235 /**
@@ -241,6 +239,8 @@ object NativeLibrary {
241 */ 239 */
242 external fun installFileToNand(filename: String, extension: String): Int 240 external fun installFileToNand(filename: String, extension: String): Int
243 241
242 external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean
243
244 external fun initializeGpuDriver( 244 external fun initializeGpuDriver(
245 hookLibDir: String?, 245 hookLibDir: String?,
246 customDriverDir: String?, 246 customDriverDir: String?,
@@ -252,18 +252,11 @@ object NativeLibrary {
252 252
253 external fun initializeSystem(reload: Boolean) 253 external fun initializeSystem(reload: Boolean)
254 254
255 external fun defaultCPUCore(): Int
256
257 /** 255 /**
258 * Begins emulation. 256 * Begins emulation.
259 */ 257 */
260 external fun run(path: String?) 258 external fun run(path: String?)
261 259
262 /**
263 * Begins emulation from the specified savestate.
264 */
265 external fun run(path: String?, savestatePath: String?, deleteSavestate: Boolean)
266
267 // Surface Handling 260 // Surface Handling
268 external fun surfaceChanged(surf: Surface?) 261 external fun surfaceChanged(surf: Surface?)
269 262
@@ -304,10 +297,9 @@ object NativeLibrary {
304 */ 297 */
305 external fun getCpuBackend(): String 298 external fun getCpuBackend(): String
306 299
307 /** 300 external fun applySettings()
308 * Notifies the core emulation that the orientation has changed. 301
309 */ 302 external fun logSettings()
310 external fun notifyOrientationChange(layout_option: Int, rotation: Int)
311 303
312 enum class CoreError { 304 enum class CoreError {
313 ErrorSystemFiles, 305 ErrorSystemFiles,
@@ -539,6 +531,35 @@ object NativeLibrary {
539 external fun isFirmwareAvailable(): Boolean 531 external fun isFirmwareAvailable(): Boolean
540 532
541 /** 533 /**
534 * Checks the PatchManager for any addons that are available
535 *
536 * @param path Path to game file. Can be a [Uri].
537 * @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
539 */
540 external fun getAddonsForFile(path: String, programId: String): Array<Pair<String, String>>?
541
542 /**
543 * Gets the save location for a specific game
544 *
545 * @param programId String representation of a game's program ID
546 * @return Save data path that may not exist yet
547 */
548 external fun getSavePath(programId: String): String
549
550 /**
551 * Adds a file to the manual filesystem provider in our EmulationSession instance
552 * @param path Path to the file we're adding. Can be a string representation of a [Uri] or
553 * a normal path
554 */
555 external fun addFileToFilesystemProvider(path: String)
556
557 /**
558 * Clears all files added to the manual filesystem provider in our EmulationSession instance
559 */
560 external fun clearFilesystemProvider()
561
562 /**
542 * Button type for use in onTouchEvent 563 * Button type for use in onTouchEvent
543 */ 564 */
544 object ButtonType { 565 object ButtonType {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
index f41d7bdbf..9b08f008d 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
@@ -172,7 +172,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
172 172
173 override fun onUserLeaveHint() { 173 override fun onUserLeaveHint() {
174 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { 174 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
175 if (BooleanSetting.PICTURE_IN_PICTURE.boolean && !isInPictureInPictureMode) { 175 if (BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && !isInPictureInPictureMode) {
176 val pictureInPictureParamsBuilder = PictureInPictureParams.Builder() 176 val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
177 .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder() 177 .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
178 enterPictureInPictureMode(pictureInPictureParamsBuilder.build()) 178 enterPictureInPictureMode(pictureInPictureParamsBuilder.build())
@@ -284,7 +284,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
284 284
285 private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder(): 285 private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder():
286 PictureInPictureParams.Builder { 286 PictureInPictureParams.Builder {
287 val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.int) { 287 val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.getInt()) {
288 0 -> Rational(16, 9) 288 0 -> Rational(16, 9)
289 1 -> Rational(4, 3) 289 1 -> Rational(4, 3)
290 2 -> Rational(21, 9) 290 2 -> Rational(21, 9)
@@ -331,7 +331,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
331 pictureInPictureActions.add(pauseRemoteAction) 331 pictureInPictureActions.add(pauseRemoteAction)
332 } 332 }
333 333
334 if (BooleanSetting.AUDIO_MUTED.boolean) { 334 if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
335 val unmuteIcon = Icon.createWithResource( 335 val unmuteIcon = Icon.createWithResource(
336 this@EmulationActivity, 336 this@EmulationActivity,
337 R.drawable.ic_pip_unmute 337 R.drawable.ic_pip_unmute
@@ -376,7 +376,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
376 val isEmulationActive = emulationViewModel.emulationStarted.value && 376 val isEmulationActive = emulationViewModel.emulationStarted.value &&
377 !emulationViewModel.isEmulationStopping.value 377 !emulationViewModel.isEmulationStopping.value
378 pictureInPictureParamsBuilder.setAutoEnterEnabled( 378 pictureInPictureParamsBuilder.setAutoEnterEnabled(
379 BooleanSetting.PICTURE_IN_PICTURE.boolean && isEmulationActive 379 BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && isEmulationActive
380 ) 380 )
381 } 381 }
382 setPictureInPictureParams(pictureInPictureParamsBuilder.build()) 382 setPictureInPictureParams(pictureInPictureParamsBuilder.build())
@@ -390,9 +390,13 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
390 if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation() 390 if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation()
391 } 391 }
392 if (intent.action == actionUnmute) { 392 if (intent.action == actionUnmute) {
393 if (BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(false) 393 if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
394 BooleanSetting.AUDIO_MUTED.setBoolean(false)
395 }
394 } else if (intent.action == actionMute) { 396 } else if (intent.action == actionMute) {
395 if (!BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(true) 397 if (!BooleanSetting.AUDIO_MUTED.getBoolean()) {
398 BooleanSetting.AUDIO_MUTED.setBoolean(true)
399 }
396 } 400 }
397 buildPictureInPictureParams() 401 buildPictureInPictureParams()
398 } 402 }
@@ -423,7 +427,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
423 } catch (ignored: Exception) { 427 } catch (ignored: Exception) {
424 } 428 }
425 // Always resume audio, since there is no UI button 429 // Always resume audio, since there is no UI button
426 if (BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(false) 430 if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
431 BooleanSetting.AUDIO_MUTED.setBoolean(false)
432 }
427 } 433 }
428 } 434 }
429 435
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
new file mode 100644
index 000000000..15c7ca3c9
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt
@@ -0,0 +1,52 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.adapters
5
6import android.view.LayoutInflater
7import android.view.ViewGroup
8import androidx.recyclerview.widget.AsyncDifferConfig
9import androidx.recyclerview.widget.DiffUtil
10import androidx.recyclerview.widget.ListAdapter
11import androidx.recyclerview.widget.RecyclerView
12import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding
13import org.yuzu.yuzu_emu.model.Addon
14
15class AddonAdapter : ListAdapter<Addon, AddonAdapter.AddonViewHolder>(
16 AsyncDifferConfig.Builder(DiffCallback()).build()
17) {
18 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder {
19 ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
20 .also { return AddonViewHolder(it) }
21 }
22
23 override fun getItemCount(): Int = currentList.size
24
25 override fun onBindViewHolder(holder: AddonViewHolder, position: Int) =
26 holder.bind(currentList[position])
27
28 inner class AddonViewHolder(val binding: ListItemAddonBinding) :
29 RecyclerView.ViewHolder(binding.root) {
30 fun bind(addon: Addon) {
31 binding.root.setOnClickListener {
32 binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked
33 }
34 binding.title.text = addon.title
35 binding.version.text = addon.version
36 binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
37 addon.enabled = checked
38 }
39 binding.addonSwitch.isChecked = addon.enabled
40 }
41 }
42
43 private class DiffCallback : DiffUtil.ItemCallback<Addon>() {
44 override fun areItemsTheSame(oldItem: Addon, newItem: Addon): Boolean {
45 return oldItem == newItem
46 }
47
48 override fun areContentsTheSame(oldItem: Addon, newItem: Addon): Boolean {
49 return oldItem == newItem
50 }
51 }
52}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt
index a21a705c1..4a05c5be9 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt
@@ -15,7 +15,7 @@ import org.yuzu.yuzu_emu.HomeNavigationDirections
15import org.yuzu.yuzu_emu.NativeLibrary 15import org.yuzu.yuzu_emu.NativeLibrary
16import org.yuzu.yuzu_emu.R 16import org.yuzu.yuzu_emu.R
17import org.yuzu.yuzu_emu.YuzuApplication 17import org.yuzu.yuzu_emu.YuzuApplication
18import org.yuzu.yuzu_emu.databinding.CardAppletOptionBinding 18import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
19import org.yuzu.yuzu_emu.model.Applet 19import org.yuzu.yuzu_emu.model.Applet
20import org.yuzu.yuzu_emu.model.AppletInfo 20import org.yuzu.yuzu_emu.model.AppletInfo
21import org.yuzu.yuzu_emu.model.Game 21import org.yuzu.yuzu_emu.model.Game
@@ -28,7 +28,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) :
28 parent: ViewGroup, 28 parent: ViewGroup,
29 viewType: Int 29 viewType: Int
30 ): AppletAdapter.AppletViewHolder { 30 ): AppletAdapter.AppletViewHolder {
31 CardAppletOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) 31 CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false)
32 .apply { root.setOnClickListener(this@AppletAdapter) } 32 .apply { root.setOnClickListener(this@AppletAdapter) }
33 .also { return AppletViewHolder(it) } 33 .also { return AppletViewHolder(it) }
34 } 34 }
@@ -65,7 +65,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) :
65 view.findNavController().navigate(action) 65 view.findNavController().navigate(action)
66 } 66 }
67 67
68 inner class AppletViewHolder(val binding: CardAppletOptionBinding) : 68 inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) :
69 RecyclerView.ViewHolder(binding.root) { 69 RecyclerView.ViewHolder(binding.root) {
70 lateinit var applet: Applet 70 lateinit var applet: Applet
71 71
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
index 0e818cab9..d290a656c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/DriverAdapter.kt
@@ -42,7 +42,7 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) :
42 if (driverViewModel.selectedDriver > position) { 42 if (driverViewModel.selectedDriver > position) {
43 driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1) 43 driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1)
44 } 44 }
45 if (GpuDriverHelper.customDriverData == driverData.second) { 45 if (GpuDriverHelper.customDriverSettingData == driverData.second) {
46 driverViewModel.setSelectedDriverIndex(0) 46 driverViewModel.setSelectedDriverIndex(0)
47 } 47 }
48 driverViewModel.driversToDelete.add(driverData.first) 48 driverViewModel.driversToDelete.add(driverData.first)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
index 2ef638559..a578f0de8 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
@@ -44,19 +44,20 @@ import org.yuzu.yuzu_emu.utils.GameIconUtils
44 44
45class GameAdapter(private val activity: AppCompatActivity) : 45class GameAdapter(private val activity: AppCompatActivity) :
46 ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()), 46 ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
47 View.OnClickListener { 47 View.OnClickListener,
48 View.OnLongClickListener {
48 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { 49 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
49 // Create a new view. 50 // Create a new view.
50 val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) 51 val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
51 binding.cardGame.setOnClickListener(this) 52 binding.cardGame.setOnClickListener(this)
53 binding.cardGame.setOnLongClickListener(this)
52 54
53 // Use that view to create a ViewHolder. 55 // Use that view to create a ViewHolder.
54 return GameViewHolder(binding) 56 return GameViewHolder(binding)
55 } 57 }
56 58
57 override fun onBindViewHolder(holder: GameViewHolder, position: Int) { 59 override fun onBindViewHolder(holder: GameViewHolder, position: Int) =
58 holder.bind(currentList[position]) 60 holder.bind(currentList[position])
59 }
60 61
61 override fun getItemCount(): Int = currentList.size 62 override fun getItemCount(): Int = currentList.size
62 63
@@ -125,8 +126,15 @@ class GameAdapter(private val activity: AppCompatActivity) :
125 } 126 }
126 } 127 }
127 128
128 val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game) 129 val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game, true)
130 view.findNavController().navigate(action)
131 }
132
133 override fun onLongClick(view: View): Boolean {
134 val holder = view.tag as GameViewHolder
135 val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(holder.game)
129 view.findNavController().navigate(action) 136 view.findNavController().navigate(action)
137 return true
130 } 138 }
131 139
132 inner class GameViewHolder(val binding: CardGameBinding) : 140 inner class GameViewHolder(val binding: CardGameBinding) :
@@ -157,7 +165,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
157 165
158 private class DiffCallback : DiffUtil.ItemCallback<Game>() { 166 private class DiffCallback : DiffUtil.ItemCallback<Game>() {
159 override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean { 167 override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
160 return oldItem.programId == newItem.programId 168 return oldItem == newItem
161 } 169 }
162 170
163 override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean { 171 override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt
new file mode 100644
index 000000000..95841d786
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt
@@ -0,0 +1,140 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.adapters
5
6import android.text.TextUtils
7import android.view.LayoutInflater
8import android.view.View
9import android.view.ViewGroup
10import androidx.core.content.res.ResourcesCompat
11import androidx.lifecycle.Lifecycle
12import androidx.lifecycle.LifecycleOwner
13import androidx.lifecycle.lifecycleScope
14import androidx.lifecycle.repeatOnLifecycle
15import androidx.recyclerview.widget.RecyclerView
16import kotlinx.coroutines.launch
17import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding
18import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
19import org.yuzu.yuzu_emu.model.GameProperty
20import org.yuzu.yuzu_emu.model.InstallableProperty
21import org.yuzu.yuzu_emu.model.SubmenuProperty
22
23class GamePropertiesAdapter(
24 private val viewLifecycle: LifecycleOwner,
25 private var properties: List<GameProperty>
26) :
27 RecyclerView.Adapter<GamePropertiesAdapter.GamePropertyViewHolder>() {
28 override fun onCreateViewHolder(
29 parent: ViewGroup,
30 viewType: Int
31 ): GamePropertyViewHolder {
32 val inflater = LayoutInflater.from(parent.context)
33 return when (viewType) {
34 PropertyType.Submenu.ordinal -> {
35 SubmenuPropertyViewHolder(
36 CardSimpleOutlinedBinding.inflate(
37 inflater,
38 parent,
39 false
40 )
41 )
42 }
43
44 else -> InstallablePropertyViewHolder(
45 CardInstallableIconBinding.inflate(
46 inflater,
47 parent,
48 false
49 )
50 )
51 }
52 }
53
54 override fun getItemCount(): Int = properties.size
55
56 override fun onBindViewHolder(holder: GamePropertyViewHolder, position: Int) =
57 holder.bind(properties[position])
58
59 override fun getItemViewType(position: Int): Int {
60 return when (properties[position]) {
61 is SubmenuProperty -> PropertyType.Submenu.ordinal
62 else -> PropertyType.Installable.ordinal
63 }
64 }
65
66 sealed class GamePropertyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
67 abstract fun bind(property: GameProperty)
68 }
69
70 inner class SubmenuPropertyViewHolder(val binding: CardSimpleOutlinedBinding) :
71 GamePropertyViewHolder(binding.root) {
72 override fun bind(property: GameProperty) {
73 val submenuProperty = property as SubmenuProperty
74
75 binding.root.setOnClickListener {
76 submenuProperty.action.invoke()
77 }
78
79 binding.title.setText(submenuProperty.titleId)
80 binding.description.setText(submenuProperty.descriptionId)
81 binding.icon.setImageDrawable(
82 ResourcesCompat.getDrawable(
83 binding.icon.context.resources,
84 submenuProperty.iconId,
85 binding.icon.context.theme
86 )
87 )
88
89 binding.details.postDelayed({
90 binding.details.isSelected = true
91 binding.details.ellipsize = TextUtils.TruncateAt.MARQUEE
92 }, 3000)
93
94 if (submenuProperty.details != null) {
95 binding.details.visibility = View.VISIBLE
96 binding.details.text = submenuProperty.details.invoke()
97 } else if (submenuProperty.detailsFlow != null) {
98 binding.details.visibility = View.VISIBLE
99 viewLifecycle.lifecycleScope.launch {
100 viewLifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
101 submenuProperty.detailsFlow.collect { binding.details.text = it }
102 }
103 }
104 } else {
105 binding.details.visibility = View.GONE
106 }
107 }
108 }
109
110 inner class InstallablePropertyViewHolder(val binding: CardInstallableIconBinding) :
111 GamePropertyViewHolder(binding.root) {
112 override fun bind(property: GameProperty) {
113 val installableProperty = property as InstallableProperty
114
115 binding.title.setText(installableProperty.titleId)
116 binding.description.setText(installableProperty.descriptionId)
117 binding.icon.setImageDrawable(
118 ResourcesCompat.getDrawable(
119 binding.icon.context.resources,
120 installableProperty.iconId,
121 binding.icon.context.theme
122 )
123 )
124
125 if (installableProperty.install != null) {
126 binding.buttonInstall.visibility = View.VISIBLE
127 binding.buttonInstall.setOnClickListener { installableProperty.install.invoke() }
128 }
129 if (installableProperty.export != null) {
130 binding.buttonExport.visibility = View.VISIBLE
131 binding.buttonExport.setOnClickListener { installableProperty.export.invoke() }
132 }
133 }
134 }
135
136 enum class PropertyType {
137 Submenu,
138 Installable
139 }
140}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt
index aeda8d222..0ba465356 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt
@@ -4,7 +4,6 @@
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6interface AbstractBooleanSetting : AbstractSetting { 6interface AbstractBooleanSetting : AbstractSetting {
7 val boolean: Boolean 7 fun getBoolean(needsGlobal: Boolean = false): Boolean
8
9 fun setBoolean(value: Boolean) 8 fun setBoolean(value: Boolean)
10} 9}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt
index 606519ad8..cf6300535 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt
@@ -4,7 +4,6 @@
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6interface AbstractByteSetting : AbstractSetting { 6interface AbstractByteSetting : AbstractSetting {
7 val byte: Byte 7 fun getByte(needsGlobal: Boolean = false): Byte
8
9 fun setByte(value: Byte) 8 fun setByte(value: Byte)
10} 9}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt
index 974925eed..c6c0bcf34 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt
@@ -4,7 +4,6 @@
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6interface AbstractFloatSetting : AbstractSetting { 6interface AbstractFloatSetting : AbstractSetting {
7 val float: Float 7 fun getFloat(needsGlobal: Boolean = false): Float
8
9 fun setFloat(value: Float) 8 fun setFloat(value: Float)
10} 9}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt
index 89b285b10..826402c34 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt
@@ -4,7 +4,6 @@
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6interface AbstractIntSetting : AbstractSetting { 6interface AbstractIntSetting : AbstractSetting {
7 val int: Int 7 fun getInt(needsGlobal: Boolean = false): Int
8
9 fun setInt(value: Int) 8 fun setInt(value: Int)
10} 9}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt
index 4873942db..2b62cc06b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt
@@ -4,7 +4,6 @@
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6interface AbstractLongSetting : AbstractSetting { 6interface AbstractLongSetting : AbstractSetting {
7 val long: Long 7 fun getLong(needsGlobal: Boolean = false): Long
8
9 fun setLong(value: Long) 8 fun setLong(value: Long)
10} 9}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt
index 8b6d29fe5..3b78c7cf0 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractSetting.kt
@@ -7,12 +7,7 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
7 7
8interface AbstractSetting { 8interface AbstractSetting {
9 val key: String 9 val key: String
10 val category: Settings.Category
11 val defaultValue: Any 10 val defaultValue: Any
12 val androidDefault: Any?
13 get() = null
14 val valueAsString: String
15 get() = ""
16 11
17 val isRuntimeModifiable: Boolean 12 val isRuntimeModifiable: Boolean
18 get() = NativeConfig.getIsRuntimeModifiable(key) 13 get() = NativeConfig.getIsRuntimeModifiable(key)
@@ -20,5 +15,17 @@ interface AbstractSetting {
20 val pairedSettingKey: String 15 val pairedSettingKey: String
21 get() = NativeConfig.getPairedSettingKey(key) 16 get() = NativeConfig.getPairedSettingKey(key)
22 17
18 val isSwitchable: Boolean
19 get() = NativeConfig.getIsSwitchable(key)
20
21 var global: Boolean
22 get() = NativeConfig.usingGlobal(key)
23 set(value) = NativeConfig.setGlobal(key, value)
24
25 val isSaveable: Boolean
26 get() = NativeConfig.getIsSaveable(key)
27
28 fun getValueAsString(needsGlobal: Boolean = false): String
29
23 fun reset() 30 fun reset()
24} 31}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt
index 91407ccbb..8bfa81e4a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt
@@ -4,7 +4,6 @@
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6interface AbstractShortSetting : AbstractSetting { 6interface AbstractShortSetting : AbstractSetting {
7 val short: Short 7 fun getShort(needsGlobal: Boolean = false): Short
8
9 fun setShort(value: Short) 8 fun setShort(value: Short)
10} 9}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt
index c8935cc48..6ff8fd3f9 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt
@@ -4,7 +4,6 @@
4package org.yuzu.yuzu_emu.features.settings.model 4package org.yuzu.yuzu_emu.features.settings.model
5 5
6interface AbstractStringSetting : AbstractSetting { 6interface AbstractStringSetting : AbstractSetting {
7 val string: String 7 fun getString(needsGlobal: Boolean = false): String
8
9 fun setString(value: String) 8 fun setString(value: String)
10} 9}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
index 8476ce867..16f06cd0a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt
@@ -5,36 +5,34 @@ package org.yuzu.yuzu_emu.features.settings.model
5 5
6import org.yuzu.yuzu_emu.utils.NativeConfig 6import org.yuzu.yuzu_emu.utils.NativeConfig
7 7
8enum class BooleanSetting( 8enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
9 override val key: String, 9 AUDIO_MUTED("audio_muted"),
10 override val category: Settings.Category, 10 CPU_DEBUG_MODE("cpu_debug_mode"),
11 override val androidDefault: Boolean? = null 11 FASTMEM("cpuopt_fastmem"),
12) : AbstractBooleanSetting { 12 FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives"),
13 AUDIO_MUTED("audio_muted", Settings.Category.Audio), 13 RENDERER_USE_SPEED_LIMIT("use_speed_limit"),
14 CPU_DEBUG_MODE("cpu_debug_mode", Settings.Category.Cpu), 14 USE_DOCKED_MODE("use_docked_mode"),
15 FASTMEM("cpuopt_fastmem", Settings.Category.Cpu), 15 RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache"),
16 FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives", Settings.Category.Cpu), 16 RENDERER_FORCE_MAX_CLOCK("force_max_clock"),
17 RENDERER_USE_SPEED_LIMIT("use_speed_limit", Settings.Category.Core), 17 RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
18 USE_DOCKED_MODE("use_docked_mode", Settings.Category.System, false), 18 RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
19 RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache", Settings.Category.Renderer), 19 RENDERER_DEBUG("debug"),
20 RENDERER_FORCE_MAX_CLOCK("force_max_clock", Settings.Category.Renderer), 20 PICTURE_IN_PICTURE("picture_in_picture"),
21 RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders", Settings.Category.Renderer), 21 USE_CUSTOM_RTC("custom_rtc_enabled");
22 RENDERER_REACTIVE_FLUSHING("use_reactive_flushing", Settings.Category.Renderer, false), 22
23 RENDERER_DEBUG("debug", Settings.Category.Renderer), 23 override fun getBoolean(needsGlobal: Boolean): Boolean =
24 PICTURE_IN_PICTURE("picture_in_picture", Settings.Category.Android), 24 NativeConfig.getBoolean(key, needsGlobal)
25 USE_CUSTOM_RTC("custom_rtc_enabled", Settings.Category.System); 25
26 26 override fun setBoolean(value: Boolean) {
27 override val boolean: Boolean 27 if (NativeConfig.isPerGameConfigLoaded()) {
28 get() = NativeConfig.getBoolean(key, false) 28 global = false
29 29 }
30 override fun setBoolean(value: Boolean) = NativeConfig.setBoolean(key, value) 30 NativeConfig.setBoolean(key, value)
31
32 override val defaultValue: Boolean by lazy {
33 androidDefault ?: NativeConfig.getBoolean(key, true)
34 } 31 }
35 32
36 override val valueAsString: String 33 override val defaultValue: Boolean by lazy { NativeConfig.getDefaultToString(key).toBoolean() }
37 get() = if (boolean) "1" else "0" 34
35 override fun getValueAsString(needsGlobal: Boolean): String = getBoolean(needsGlobal).toString()
38 36
39 override fun reset() = NativeConfig.setBoolean(key, defaultValue) 37 override fun reset() = NativeConfig.setBoolean(key, defaultValue)
40} 38}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt
index 6ec0a765e..7b7fac211 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt
@@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
5 5
6import org.yuzu.yuzu_emu.utils.NativeConfig 6import org.yuzu.yuzu_emu.utils.NativeConfig
7 7
8enum class ByteSetting( 8enum class ByteSetting(override val key: String) : AbstractByteSetting {
9 override val key: String, 9 AUDIO_VOLUME("volume");
10 override val category: Settings.Category
11) : AbstractByteSetting {
12 AUDIO_VOLUME("volume", Settings.Category.Audio);
13 10
14 override val byte: Byte 11 override fun getByte(needsGlobal: Boolean): Byte = NativeConfig.getByte(key, needsGlobal)
15 get() = NativeConfig.getByte(key, false)
16 12
17 override fun setByte(value: Byte) = NativeConfig.setByte(key, value) 13 override fun setByte(value: Byte) {
14 if (NativeConfig.isPerGameConfigLoaded()) {
15 global = false
16 }
17 NativeConfig.setByte(key, value)
18 }
18 19
19 override val defaultValue: Byte by lazy { NativeConfig.getByte(key, true) } 20 override val defaultValue: Byte by lazy { NativeConfig.getDefaultToString(key).toByte() }
20 21
21 override val valueAsString: String 22 override fun getValueAsString(needsGlobal: Boolean): String = getByte(needsGlobal).toString()
22 get() = byte.toString()
23 23
24 override fun reset() = NativeConfig.setByte(key, defaultValue) 24 override fun reset() = NativeConfig.setByte(key, defaultValue)
25} 25}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt
index 0181d06f2..4644824d8 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt
@@ -5,22 +5,22 @@ package org.yuzu.yuzu_emu.features.settings.model
5 5
6import org.yuzu.yuzu_emu.utils.NativeConfig 6import org.yuzu.yuzu_emu.utils.NativeConfig
7 7
8enum class FloatSetting( 8enum class FloatSetting(override val key: String) : AbstractFloatSetting {
9 override val key: String,
10 override val category: Settings.Category
11) : AbstractFloatSetting {
12 // No float settings currently exist 9 // No float settings currently exist
13 EMPTY_SETTING("", Settings.Category.UiGeneral); 10 EMPTY_SETTING("");
14 11
15 override val float: Float 12 override fun getFloat(needsGlobal: Boolean): Float = NativeConfig.getFloat(key, false)
16 get() = NativeConfig.getFloat(key, false)
17 13
18 override fun setFloat(value: Float) = NativeConfig.setFloat(key, value) 14 override fun setFloat(value: Float) {
15 if (NativeConfig.isPerGameConfigLoaded()) {
16 global = false
17 }
18 NativeConfig.setFloat(key, value)
19 }
19 20
20 override val defaultValue: Float by lazy { NativeConfig.getFloat(key, true) } 21 override val defaultValue: Float by lazy { NativeConfig.getDefaultToString(key).toFloat() }
21 22
22 override val valueAsString: String 23 override fun getValueAsString(needsGlobal: Boolean): String = getFloat(needsGlobal).toString()
23 get() = float.toString()
24 24
25 override fun reset() = NativeConfig.setFloat(key, defaultValue) 25 override fun reset() = NativeConfig.setFloat(key, defaultValue)
26} 26}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
index ef10b209f..21e4e1afd 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
@@ -5,36 +5,33 @@ package org.yuzu.yuzu_emu.features.settings.model
5 5
6import org.yuzu.yuzu_emu.utils.NativeConfig 6import org.yuzu.yuzu_emu.utils.NativeConfig
7 7
8enum class IntSetting( 8enum class IntSetting(override val key: String) : AbstractIntSetting {
9 override val key: String, 9 CPU_BACKEND("cpu_backend"),
10 override val category: Settings.Category, 10 CPU_ACCURACY("cpu_accuracy"),
11 override val androidDefault: Int? = null 11 REGION_INDEX("region_index"),
12) : AbstractIntSetting { 12 LANGUAGE_INDEX("language_index"),
13 CPU_BACKEND("cpu_backend", Settings.Category.Cpu), 13 RENDERER_BACKEND("backend"),
14 CPU_ACCURACY("cpu_accuracy", Settings.Category.Cpu), 14 RENDERER_ACCURACY("gpu_accuracy"),
15 REGION_INDEX("region_index", Settings.Category.System), 15 RENDERER_RESOLUTION("resolution_setup"),
16 LANGUAGE_INDEX("language_index", Settings.Category.System), 16 RENDERER_VSYNC("use_vsync"),
17 RENDERER_BACKEND("backend", Settings.Category.Renderer), 17 RENDERER_SCALING_FILTER("scaling_filter"),
18 RENDERER_ACCURACY("gpu_accuracy", Settings.Category.Renderer, 0), 18 RENDERER_ANTI_ALIASING("anti_aliasing"),
19 RENDERER_RESOLUTION("resolution_setup", Settings.Category.Renderer), 19 RENDERER_SCREEN_LAYOUT("screen_layout"),
20 RENDERER_VSYNC("use_vsync", Settings.Category.Renderer), 20 RENDERER_ASPECT_RATIO("aspect_ratio"),
21 RENDERER_SCALING_FILTER("scaling_filter", Settings.Category.Renderer), 21 AUDIO_OUTPUT_ENGINE("output_engine");
22 RENDERER_ANTI_ALIASING("anti_aliasing", Settings.Category.Renderer), 22
23 RENDERER_SCREEN_LAYOUT("screen_layout", Settings.Category.Android), 23 override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal)
24 RENDERER_ASPECT_RATIO("aspect_ratio", Settings.Category.Renderer), 24
25 AUDIO_OUTPUT_ENGINE("output_engine", Settings.Category.Audio); 25 override fun setInt(value: Int) {
26 26 if (NativeConfig.isPerGameConfigLoaded()) {
27 override val int: Int 27 global = false
28 get() = NativeConfig.getInt(key, false) 28 }
29 29 NativeConfig.setInt(key, value)
30 override fun setInt(value: Int) = NativeConfig.setInt(key, value)
31
32 override val defaultValue: Int by lazy {
33 androidDefault ?: NativeConfig.getInt(key, true)
34 } 30 }
35 31
36 override val valueAsString: String 32 override val defaultValue: Int by lazy { NativeConfig.getDefaultToString(key).toInt() }
37 get() = int.toString() 33
34 override fun getValueAsString(needsGlobal: Boolean): String = getInt(needsGlobal).toString()
38 35
39 override fun reset() = NativeConfig.setInt(key, defaultValue) 36 override fun reset() = NativeConfig.setInt(key, defaultValue)
40} 37}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt
index c526fc4cf..e3efd516c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt
@@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
5 5
6import org.yuzu.yuzu_emu.utils.NativeConfig 6import org.yuzu.yuzu_emu.utils.NativeConfig
7 7
8enum class LongSetting( 8enum class LongSetting(override val key: String) : AbstractLongSetting {
9 override val key: String, 9 CUSTOM_RTC("custom_rtc");
10 override val category: Settings.Category
11) : AbstractLongSetting {
12 CUSTOM_RTC("custom_rtc", Settings.Category.System);
13 10
14 override val long: Long 11 override fun getLong(needsGlobal: Boolean): Long = NativeConfig.getLong(key, needsGlobal)
15 get() = NativeConfig.getLong(key, false)
16 12
17 override fun setLong(value: Long) = NativeConfig.setLong(key, value) 13 override fun setLong(value: Long) {
14 if (NativeConfig.isPerGameConfigLoaded()) {
15 global = false
16 }
17 NativeConfig.setLong(key, value)
18 }
18 19
19 override val defaultValue: Long by lazy { NativeConfig.getLong(key, true) } 20 override val defaultValue: Long by lazy { NativeConfig.getDefaultToString(key).toLong() }
20 21
21 override val valueAsString: String 22 override fun getValueAsString(needsGlobal: Boolean): String = getLong(needsGlobal).toString()
22 get() = long.toString()
23 23
24 override fun reset() = NativeConfig.setLong(key, defaultValue) 24 override fun reset() = NativeConfig.setLong(key, defaultValue)
25} 25}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
index e3cd66185..9551fc05e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt
@@ -6,62 +6,11 @@ package org.yuzu.yuzu_emu.features.settings.model
6import org.yuzu.yuzu_emu.R 6import org.yuzu.yuzu_emu.R
7 7
8object Settings { 8object Settings {
9 enum class Category {
10 Android,
11 Audio,
12 Core,
13 Cpu,
14 CpuDebug,
15 CpuUnsafe,
16 Renderer,
17 RendererAdvanced,
18 RendererDebug,
19 System,
20 SystemAudio,
21 DataStorage,
22 Debugging,
23 DebuggingGraphics,
24 Miscellaneous,
25 Network,
26 WebService,
27 AddOns,
28 Controls,
29 Ui,
30 UiGeneral,
31 UiLayout,
32 UiGameList,
33 Screenshots,
34 Shortcuts,
35 Multiplayer,
36 Services,
37 Paths,
38 MaxEnum
39 }
40
41 val settingsList = listOf<AbstractSetting>(
42 *BooleanSetting.values(),
43 *ByteSetting.values(),
44 *ShortSetting.values(),
45 *IntSetting.values(),
46 *FloatSetting.values(),
47 *LongSetting.values(),
48 *StringSetting.values()
49 )
50
51 const val SECTION_GENERAL = "General"
52 const val SECTION_SYSTEM = "System"
53 const val SECTION_RENDERER = "Renderer"
54 const val SECTION_AUDIO = "Audio"
55 const val SECTION_CPU = "Cpu"
56 const val SECTION_THEME = "Theme"
57 const val SECTION_DEBUG = "Debug"
58
59 enum class MenuTag(val titleId: Int) { 9 enum class MenuTag(val titleId: Int) {
60 SECTION_ROOT(R.string.advanced_settings), 10 SECTION_ROOT(R.string.advanced_settings),
61 SECTION_SYSTEM(R.string.preferences_system), 11 SECTION_SYSTEM(R.string.preferences_system),
62 SECTION_RENDERER(R.string.preferences_graphics), 12 SECTION_RENDERER(R.string.preferences_graphics),
63 SECTION_AUDIO(R.string.preferences_audio), 13 SECTION_AUDIO(R.string.preferences_audio),
64 SECTION_CPU(R.string.cpu),
65 SECTION_THEME(R.string.preferences_theme), 14 SECTION_THEME(R.string.preferences_theme),
66 SECTION_DEBUG(R.string.preferences_debug); 15 SECTION_DEBUG(R.string.preferences_debug);
67 } 16 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt
index c9a0c664c..16eb4ffdd 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt
@@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
5 5
6import org.yuzu.yuzu_emu.utils.NativeConfig 6import org.yuzu.yuzu_emu.utils.NativeConfig
7 7
8enum class ShortSetting( 8enum class ShortSetting(override val key: String) : AbstractShortSetting {
9 override val key: String, 9 RENDERER_SPEED_LIMIT("speed_limit");
10 override val category: Settings.Category
11) : AbstractShortSetting {
12 RENDERER_SPEED_LIMIT("speed_limit", Settings.Category.Core);
13 10
14 override val short: Short 11 override fun getShort(needsGlobal: Boolean): Short = NativeConfig.getShort(key, needsGlobal)
15 get() = NativeConfig.getShort(key, false)
16 12
17 override fun setShort(value: Short) = NativeConfig.setShort(key, value) 13 override fun setShort(value: Short) {
14 if (NativeConfig.isPerGameConfigLoaded()) {
15 global = false
16 }
17 NativeConfig.setShort(key, value)
18 }
18 19
19 override val defaultValue: Short by lazy { NativeConfig.getShort(key, true) } 20 override val defaultValue: Short by lazy { NativeConfig.getDefaultToString(key).toShort() }
20 21
21 override val valueAsString: String 22 override fun getValueAsString(needsGlobal: Boolean): String = getShort(needsGlobal).toString()
22 get() = short.toString()
23 23
24 override fun reset() = NativeConfig.setShort(key, defaultValue) 24 override fun reset() = NativeConfig.setShort(key, defaultValue)
25} 25}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt
index 9bb3e66d4..a0d8cfede 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt
@@ -5,22 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
5 5
6import org.yuzu.yuzu_emu.utils.NativeConfig 6import org.yuzu.yuzu_emu.utils.NativeConfig
7 7
8enum class StringSetting( 8enum class StringSetting(override val key: String) : AbstractStringSetting {
9 override val key: String, 9 DRIVER_PATH("driver_path");
10 override val category: Settings.Category
11) : AbstractStringSetting {
12 // No string settings currently exist
13 EMPTY_SETTING("", Settings.Category.UiGeneral);
14 10
15 override val string: String 11 override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal)
16 get() = NativeConfig.getString(key, false)
17 12
18 override fun setString(value: String) = NativeConfig.setString(key, value) 13 override fun setString(value: String) {
14 if (NativeConfig.isPerGameConfigLoaded()) {
15 global = false
16 }
17 NativeConfig.setString(key, value)
18 }
19 19
20 override val defaultValue: String by lazy { NativeConfig.getString(key, true) } 20 override val defaultValue: String by lazy { NativeConfig.getDefaultToString(key) }
21 21
22 override val valueAsString: String 22 override fun getValueAsString(needsGlobal: Boolean): String = getString(needsGlobal)
23 get() = string
24 23
25 override fun reset() = NativeConfig.setString(key, defaultValue) 24 override fun reset() = NativeConfig.setString(key, defaultValue)
26} 25}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt
index 8bc164197..1d81f5f2b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt
@@ -12,7 +12,6 @@ class DateTimeSetting(
12) : SettingsItem(longSetting, titleId, descriptionId) { 12) : SettingsItem(longSetting, titleId, descriptionId) {
13 override val type = TYPE_DATETIME_SETTING 13 override val type = TYPE_DATETIME_SETTING
14 14
15 var value: Long 15 fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal)
16 get() = longSetting.long 16 fun setValue(value: Long) = (setting as AbstractLongSetting).setLong(value)
17 set(value) = (setting as AbstractLongSetting).setLong(value)
18} 17}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
index e198b18a0..2e97aee2c 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SettingsItem.kt
@@ -11,8 +11,8 @@ import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
11import org.yuzu.yuzu_emu.features.settings.model.ByteSetting 11import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
12import org.yuzu.yuzu_emu.features.settings.model.IntSetting 12import org.yuzu.yuzu_emu.features.settings.model.IntSetting
13import org.yuzu.yuzu_emu.features.settings.model.LongSetting 13import org.yuzu.yuzu_emu.features.settings.model.LongSetting
14import org.yuzu.yuzu_emu.features.settings.model.Settings
15import org.yuzu.yuzu_emu.features.settings.model.ShortSetting 14import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
15import org.yuzu.yuzu_emu.utils.NativeConfig
16 16
17/** 17/**
18 * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments. 18 * ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
@@ -30,10 +30,26 @@ abstract class SettingsItem(
30 30
31 val isEditable: Boolean 31 val isEditable: Boolean
32 get() { 32 get() {
33 // Can't edit settings that aren't saveable in per-game config even if they are switchable
34 if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {
35 return false
36 }
37
33 if (!NativeLibrary.isRunning()) return true 38 if (!NativeLibrary.isRunning()) return true
39
40 // Prevent editing settings that were modified in per-game config while editing global
41 // config
42 if (!NativeConfig.isPerGameConfigLoaded() && !setting.global) {
43 return false
44 }
45
34 return setting.isRuntimeModifiable 46 return setting.isRuntimeModifiable
35 } 47 }
36 48
49 val needsRuntimeGlobal: Boolean
50 get() = NativeLibrary.isRunning() && !setting.global &&
51 !NativeConfig.isPerGameConfigLoaded()
52
37 companion object { 53 companion object {
38 const val TYPE_HEADER = 0 54 const val TYPE_HEADER = 0
39 const val TYPE_SWITCH = 1 55 const val TYPE_SWITCH = 1
@@ -48,8 +64,9 @@ abstract class SettingsItem(
48 64
49 val emptySetting = object : AbstractSetting { 65 val emptySetting = object : AbstractSetting {
50 override val key: String = "" 66 override val key: String = ""
51 override val category: Settings.Category = Settings.Category.Ui
52 override val defaultValue: Any = false 67 override val defaultValue: Any = false
68 override val isSaveable = true
69 override fun getValueAsString(needsGlobal: Boolean): String = ""
53 override fun reset() {} 70 override fun reset() {}
54 } 71 }
55 72
@@ -270,9 +287,9 @@ abstract class SettingsItem(
270 ) 287 )
271 288
272 val fastmem = object : AbstractBooleanSetting { 289 val fastmem = object : AbstractBooleanSetting {
273 override val boolean: Boolean 290 override fun getBoolean(needsGlobal: Boolean): Boolean =
274 get() = 291 BooleanSetting.FASTMEM.getBoolean() &&
275 BooleanSetting.FASTMEM.boolean && BooleanSetting.FASTMEM_EXCLUSIVES.boolean 292 BooleanSetting.FASTMEM_EXCLUSIVES.getBoolean()
276 293
277 override fun setBoolean(value: Boolean) { 294 override fun setBoolean(value: Boolean) {
278 BooleanSetting.FASTMEM.setBoolean(value) 295 BooleanSetting.FASTMEM.setBoolean(value)
@@ -280,9 +297,24 @@ abstract class SettingsItem(
280 } 297 }
281 298
282 override val key: String = FASTMEM_COMBINED 299 override val key: String = FASTMEM_COMBINED
283 override val category = Settings.Category.Cpu
284 override val isRuntimeModifiable: Boolean = false 300 override val isRuntimeModifiable: Boolean = false
285 override val defaultValue: Boolean = true 301 override val defaultValue: Boolean = true
302 override val isSwitchable: Boolean = true
303 override var global: Boolean
304 get() {
305 return BooleanSetting.FASTMEM.global &&
306 BooleanSetting.FASTMEM_EXCLUSIVES.global
307 }
308 set(value) {
309 BooleanSetting.FASTMEM.global = value
310 BooleanSetting.FASTMEM_EXCLUSIVES.global = value
311 }
312
313 override val isSaveable = true
314
315 override fun getValueAsString(needsGlobal: Boolean): String =
316 getBoolean().toString()
317
286 override fun reset() = setBoolean(defaultValue) 318 override fun reset() = setBoolean(defaultValue)
287 } 319 }
288 put(SwitchSetting(fastmem, R.string.fastmem, 0)) 320 put(SwitchSetting(fastmem, R.string.fastmem, 0))
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt
index 705527a73..97a5a9e59 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt
@@ -15,16 +15,11 @@ class SingleChoiceSetting(
15) : SettingsItem(setting, titleId, descriptionId) { 15) : SettingsItem(setting, titleId, descriptionId) {
16 override val type = TYPE_SINGLE_CHOICE 16 override val type = TYPE_SINGLE_CHOICE
17 17
18 var selectedValue: Int 18 fun getSelectedValue(needsGlobal: Boolean = false) =
19 get() { 19 when (setting) {
20 return when (setting) { 20 is AbstractIntSetting -> setting.getInt(needsGlobal)
21 is AbstractIntSetting -> setting.int 21 else -> -1
22 else -> -1
23 }
24 }
25 set(value) {
26 when (setting) {
27 is AbstractIntSetting -> setting.setInt(value)
28 }
29 } 22 }
23
24 fun setSelectedValue(value: Int) = (setting as AbstractIntSetting).setInt(value)
30} 25}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt
index c3b5df02c..b9b709bf7 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt
@@ -20,22 +20,20 @@ class SliderSetting(
20) : SettingsItem(setting, titleId, descriptionId) { 20) : SettingsItem(setting, titleId, descriptionId) {
21 override val type = TYPE_SLIDER 21 override val type = TYPE_SLIDER
22 22
23 var selectedValue: Int 23 fun getSelectedValue(needsGlobal: Boolean = false) =
24 get() { 24 when (setting) {
25 return when (setting) { 25 is AbstractByteSetting -> setting.getByte(needsGlobal).toInt()
26 is AbstractByteSetting -> setting.byte.toInt() 26 is AbstractShortSetting -> setting.getShort(needsGlobal).toInt()
27 is AbstractShortSetting -> setting.short.toInt() 27 is AbstractIntSetting -> setting.getInt(needsGlobal)
28 is AbstractIntSetting -> setting.int 28 is AbstractFloatSetting -> setting.getFloat(needsGlobal).roundToInt()
29 is AbstractFloatSetting -> setting.float.roundToInt() 29 else -> -1
30 else -> -1
31 }
32 } 30 }
33 set(value) { 31
34 when (setting) { 32 fun setSelectedValue(value: Int) =
35 is AbstractByteSetting -> setting.setByte(value.toByte()) 33 when (setting) {
36 is AbstractShortSetting -> setting.setShort(value.toShort()) 34 is AbstractByteSetting -> setting.setByte(value.toByte())
37 is AbstractIntSetting -> setting.setInt(value) 35 is AbstractShortSetting -> setting.setShort(value.toShort())
38 is AbstractFloatSetting -> setting.setFloat(value.toFloat()) 36 is AbstractFloatSetting -> setting.setFloat(value.toFloat())
39 } 37 else -> (setting as AbstractIntSetting).setInt(value)
40 } 38 }
41} 39}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt
index 871dab4f3..ba7920f50 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt
@@ -17,14 +17,13 @@ class StringSingleChoiceSetting(
17 fun getValueAt(index: Int): String = 17 fun getValueAt(index: Int): String =
18 if (index >= 0 && index < values.size) values[index] else "" 18 if (index >= 0 && index < values.size) values[index] else ""
19 19
20 var selectedValue: String 20 fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal)
21 get() = stringSetting.string 21 fun setSelectedValue(value: String) = stringSetting.setString(value)
22 set(value) = stringSetting.setString(value)
23 22
24 val selectValueIndex: Int 23 val selectValueIndex: Int
25 get() { 24 get() {
26 for (i in values.indices) { 25 for (i in values.indices) {
27 if (values[i] == selectedValue) { 26 if (values[i] == getSelectedValue()) {
28 return i 27 return i
29 } 28 }
30 } 29 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt
index 416967e64..44d47dd69 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt
@@ -14,18 +14,18 @@ class SwitchSetting(
14) : SettingsItem(setting, titleId, descriptionId) { 14) : SettingsItem(setting, titleId, descriptionId) {
15 override val type = TYPE_SWITCH 15 override val type = TYPE_SWITCH
16 16
17 var checked: Boolean 17 fun getIsChecked(needsGlobal: Boolean = false): Boolean {
18 get() { 18 return when (setting) {
19 return when (setting) { 19 is AbstractIntSetting -> setting.getInt(needsGlobal) == 1
20 is AbstractIntSetting -> setting.int == 1 20 is AbstractBooleanSetting -> setting.getBoolean(needsGlobal)
21 is AbstractBooleanSetting -> setting.boolean 21 else -> false
22 else -> false
23 }
24 } 22 }
25 set(value) { 23 }
26 when (setting) { 24
27 is AbstractIntSetting -> setting.setInt(if (value) 1 else 0) 25 fun setChecked(value: Boolean) {
28 is AbstractBooleanSetting -> setting.setBoolean(value) 26 when (setting) {
29 } 27 is AbstractIntSetting -> setting.setInt(if (value) 1 else 0)
28 is AbstractBooleanSetting -> setting.setBoolean(value)
30 } 29 }
30 }
31} 31}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
index 64bfc6dd0..6f072241a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt
@@ -19,10 +19,9 @@ import androidx.lifecycle.repeatOnLifecycle
19import androidx.navigation.fragment.NavHostFragment 19import androidx.navigation.fragment.NavHostFragment
20import androidx.navigation.navArgs 20import androidx.navigation.navArgs
21import com.google.android.material.color.MaterialColors 21import com.google.android.material.color.MaterialColors
22import kotlinx.coroutines.CoroutineScope
23import kotlinx.coroutines.Dispatchers
24import kotlinx.coroutines.flow.collectLatest 22import kotlinx.coroutines.flow.collectLatest
25import kotlinx.coroutines.launch 23import kotlinx.coroutines.launch
24import org.yuzu.yuzu_emu.NativeLibrary
26import java.io.IOException 25import java.io.IOException
27import org.yuzu.yuzu_emu.R 26import org.yuzu.yuzu_emu.R
28import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding 27import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
@@ -46,6 +45,9 @@ class SettingsActivity : AppCompatActivity() {
46 binding = ActivitySettingsBinding.inflate(layoutInflater) 45 binding = ActivitySettingsBinding.inflate(layoutInflater)
47 setContentView(binding.root) 46 setContentView(binding.root)
48 47
48 if (!NativeConfig.isPerGameConfigLoaded() && args.game != null) {
49 SettingsFile.loadCustomConfig(args.game!!)
50 }
49 settingsViewModel.game = args.game 51 settingsViewModel.game = args.game
50 52
51 val navHostFragment = 53 val navHostFragment =
@@ -126,7 +128,6 @@ class SettingsActivity : AppCompatActivity() {
126 128
127 override fun onStart() { 129 override fun onStart() {
128 super.onStart() 130 super.onStart()
129 // TODO: Load custom settings contextually
130 if (!DirectoryInitialization.areDirectoriesReady) { 131 if (!DirectoryInitialization.areDirectoriesReady) {
131 DirectoryInitialization.start() 132 DirectoryInitialization.start()
132 } 133 }
@@ -134,24 +135,35 @@ class SettingsActivity : AppCompatActivity() {
134 135
135 override fun onStop() { 136 override fun onStop() {
136 super.onStop() 137 super.onStop()
137 CoroutineScope(Dispatchers.IO).launch { 138 Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
138 NativeConfig.saveSettings() 139 if (isFinishing) {
140 NativeLibrary.applySettings()
141 if (args.game == null) {
142 NativeConfig.saveGlobalConfig()
143 } else if (NativeConfig.isPerGameConfigLoaded()) {
144 NativeLibrary.logSettings()
145 NativeConfig.savePerGameConfig()
146 NativeConfig.unloadPerGameConfig()
147 }
139 } 148 }
140 } 149 }
141 150
142 override fun onDestroy() {
143 settingsViewModel.clear()
144 super.onDestroy()
145 }
146
147 fun onSettingsReset() { 151 fun onSettingsReset() {
148 // Delete settings file because the user may have changed values that do not exist in the UI 152 // Delete settings file because the user may have changed values that do not exist in the UI
149 NativeConfig.unloadConfig() 153 if (args.game == null) {
150 val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG) 154 NativeConfig.unloadGlobalConfig()
151 if (!settingsFile.delete()) { 155 val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
152 throw IOException("Failed to delete $settingsFile") 156 if (!settingsFile.delete()) {
157 throw IOException("Failed to delete $settingsFile")
158 }
159 NativeConfig.initializeGlobalConfig()
160 } else {
161 NativeConfig.unloadPerGameConfig()
162 val settingsFile = SettingsFile.getCustomSettingsFile(args.game!!)
163 if (!settingsFile.delete()) {
164 throw IOException("Failed to delete $settingsFile")
165 }
153 } 166 }
154 NativeConfig.initializeConfig()
155 167
156 Toast.makeText( 168 Toast.makeText(
157 applicationContext, 169 applicationContext,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
index af2c1e582..be9b3031b 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsAdapter.kt
@@ -102,8 +102,9 @@ class SettingsAdapter(
102 return currentList[position].type 102 return currentList[position].type
103 } 103 }
104 104
105 fun onBooleanClick(item: SwitchSetting, checked: Boolean) { 105 fun onBooleanClick(item: SwitchSetting, checked: Boolean, position: Int) {
106 item.checked = checked 106 item.setChecked(checked)
107 notifyItemChanged(position)
107 settingsViewModel.setShouldReloadSettingsList(true) 108 settingsViewModel.setShouldReloadSettingsList(true)
108 } 109 }
109 110
@@ -126,7 +127,7 @@ class SettingsAdapter(
126 } 127 }
127 128
128 fun onDateTimeClick(item: DateTimeSetting, position: Int) { 129 fun onDateTimeClick(item: DateTimeSetting, position: Int) {
129 val storedTime = item.value * 1000 130 val storedTime = item.getValue() * 1000
130 131
131 // Helper to extract hour and minute from epoch time 132 // Helper to extract hour and minute from epoch time
132 val calendar: Calendar = Calendar.getInstance() 133 val calendar: Calendar = Calendar.getInstance()
@@ -159,9 +160,9 @@ class SettingsAdapter(
159 var epochTime: Long = datePicker.selection!! / 1000 160 var epochTime: Long = datePicker.selection!! / 1000
160 epochTime += timePicker.hour.toLong() * 60 * 60 161 epochTime += timePicker.hour.toLong() * 60 * 60
161 epochTime += timePicker.minute.toLong() * 60 162 epochTime += timePicker.minute.toLong() * 60
162 if (item.value != epochTime) { 163 if (item.getValue() != epochTime) {
163 notifyItemChanged(position) 164 notifyItemChanged(position)
164 item.value = epochTime 165 item.setValue(epochTime)
165 } 166 }
166 } 167 }
167 datePicker.show( 168 datePicker.show(
@@ -195,6 +196,12 @@ class SettingsAdapter(
195 return true 196 return true
196 } 197 }
197 198
199 fun onClearClick(item: SettingsItem, position: Int) {
200 item.setting.global = true
201 notifyItemChanged(position)
202 settingsViewModel.setShouldReloadSettingsList(true)
203 }
204
198 private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() { 205 private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() {
199 override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean { 206 override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
200 return oldItem.setting.key == newItem.setting.key 207 return oldItem.setting.key == newItem.setting.key
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
index 769baf744..d7ab0b5d9 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt
@@ -66,7 +66,13 @@ class SettingsFragment : Fragment() {
66 args.menuTag 66 args.menuTag
67 ) 67 )
68 68
69 binding.toolbarSettingsLayout.title = getString(args.menuTag.titleId) 69 binding.toolbarSettingsLayout.title = if (args.menuTag == Settings.MenuTag.SECTION_ROOT &&
70 args.game != null
71 ) {
72 args.game!!.title
73 } else {
74 getString(args.menuTag.titleId)
75 }
70 binding.listSettings.apply { 76 binding.listSettings.apply {
71 adapter = settingsAdapter 77 adapter = settingsAdapter
72 layoutManager = LinearLayoutManager(requireContext()) 78 layoutManager = LinearLayoutManager(requireContext())
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
index 7425728c6..a7e965589 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt
@@ -7,6 +7,7 @@ import android.content.SharedPreferences
7import android.os.Build 7import android.os.Build
8import android.widget.Toast 8import android.widget.Toast
9import androidx.preference.PreferenceManager 9import androidx.preference.PreferenceManager
10import org.yuzu.yuzu_emu.NativeLibrary
10import org.yuzu.yuzu_emu.R 11import org.yuzu.yuzu_emu.R
11import org.yuzu.yuzu_emu.YuzuApplication 12import org.yuzu.yuzu_emu.YuzuApplication
12import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting 13import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
@@ -31,12 +32,27 @@ class SettingsFragmentPresenter(
31 private val preferences: SharedPreferences 32 private val preferences: SharedPreferences
32 get() = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) 33 get() = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
33 34
34 // Extension for populating settings list based on paired settings 35 // Extension for altering settings list based on each setting's properties
35 fun ArrayList<SettingsItem>.add(key: String) { 36 fun ArrayList<SettingsItem>.add(key: String) {
36 val item = SettingsItem.settingsItems[key]!! 37 val item = SettingsItem.settingsItems[key]!!
38 if (settingsViewModel.game != null && !item.setting.isSwitchable) {
39 return
40 }
41
42 if (!NativeConfig.isPerGameConfigLoaded() && !NativeLibrary.isRunning()) {
43 item.setting.global = true
44 }
45
37 val pairedSettingKey = item.setting.pairedSettingKey 46 val pairedSettingKey = item.setting.pairedSettingKey
38 if (pairedSettingKey.isNotEmpty()) { 47 if (pairedSettingKey.isNotEmpty()) {
39 val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false) 48 val pairedSettingValue = NativeConfig.getBoolean(
49 pairedSettingKey,
50 if (NativeLibrary.isRunning() && !NativeConfig.isPerGameConfigLoaded()) {
51 !NativeConfig.usingGlobal(pairedSettingKey)
52 } else {
53 NativeConfig.usingGlobal(pairedSettingKey)
54 }
55 )
40 if (!pairedSettingValue) return 56 if (!pairedSettingValue) return
41 } 57 }
42 add(item) 58 add(item)
@@ -153,8 +169,8 @@ class SettingsFragmentPresenter(
153 private fun addThemeSettings(sl: ArrayList<SettingsItem>) { 169 private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
154 sl.apply { 170 sl.apply {
155 val theme: AbstractIntSetting = object : AbstractIntSetting { 171 val theme: AbstractIntSetting = object : AbstractIntSetting {
156 override val int: Int 172 override fun getInt(needsGlobal: Boolean): Int =
157 get() = preferences.getInt(Settings.PREF_THEME, 0) 173 preferences.getInt(Settings.PREF_THEME, 0)
158 174
159 override fun setInt(value: Int) { 175 override fun setInt(value: Int) {
160 preferences.edit() 176 preferences.edit()
@@ -164,8 +180,8 @@ class SettingsFragmentPresenter(
164 } 180 }
165 181
166 override val key: String = Settings.PREF_THEME 182 override val key: String = Settings.PREF_THEME
167 override val category = Settings.Category.UiGeneral
168 override val isRuntimeModifiable: Boolean = false 183 override val isRuntimeModifiable: Boolean = false
184 override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
169 override val defaultValue: Int = 0 185 override val defaultValue: Int = 0
170 override fun reset() { 186 override fun reset() {
171 preferences.edit() 187 preferences.edit()
@@ -197,8 +213,8 @@ class SettingsFragmentPresenter(
197 } 213 }
198 214
199 val themeMode: AbstractIntSetting = object : AbstractIntSetting { 215 val themeMode: AbstractIntSetting = object : AbstractIntSetting {
200 override val int: Int 216 override fun getInt(needsGlobal: Boolean): Int =
201 get() = preferences.getInt(Settings.PREF_THEME_MODE, -1) 217 preferences.getInt(Settings.PREF_THEME_MODE, -1)
202 218
203 override fun setInt(value: Int) { 219 override fun setInt(value: Int) {
204 preferences.edit() 220 preferences.edit()
@@ -208,8 +224,8 @@ class SettingsFragmentPresenter(
208 } 224 }
209 225
210 override val key: String = Settings.PREF_THEME_MODE 226 override val key: String = Settings.PREF_THEME_MODE
211 override val category = Settings.Category.UiGeneral
212 override val isRuntimeModifiable: Boolean = false 227 override val isRuntimeModifiable: Boolean = false
228 override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString()
213 override val defaultValue: Int = -1 229 override val defaultValue: Int = -1
214 override fun reset() { 230 override fun reset() {
215 preferences.edit() 231 preferences.edit()
@@ -230,8 +246,8 @@ class SettingsFragmentPresenter(
230 ) 246 )
231 247
232 val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting { 248 val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting {
233 override val boolean: Boolean 249 override fun getBoolean(needsGlobal: Boolean): Boolean =
234 get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) 250 preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
235 251
236 override fun setBoolean(value: Boolean) { 252 override fun setBoolean(value: Boolean) {
237 preferences.edit() 253 preferences.edit()
@@ -241,8 +257,10 @@ class SettingsFragmentPresenter(
241 } 257 }
242 258
243 override val key: String = Settings.PREF_BLACK_BACKGROUNDS 259 override val key: String = Settings.PREF_BLACK_BACKGROUNDS
244 override val category = Settings.Category.UiGeneral
245 override val isRuntimeModifiable: Boolean = false 260 override val isRuntimeModifiable: Boolean = false
261 override fun getValueAsString(needsGlobal: Boolean): String =
262 getBoolean().toString()
263
246 override val defaultValue: Boolean = false 264 override val defaultValue: Boolean = false
247 override fun reset() { 265 override fun reset() {
248 preferences.edit() 266 preferences.edit()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
index 525f013f8..5ad0899dd 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/DateTimeViewHolder.kt
@@ -13,6 +13,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
13import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting 13import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting
14import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem 14import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
15import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter 15import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
16import org.yuzu.yuzu_emu.utils.NativeConfig
16 17
17class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : 18class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
18 SettingViewHolder(binding.root, adapter) { 19 SettingViewHolder(binding.root, adapter) {
@@ -29,12 +30,23 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
29 } 30 }
30 31
31 binding.textSettingValue.visibility = View.VISIBLE 32 binding.textSettingValue.visibility = View.VISIBLE
32 val epochTime = setting.value 33 val epochTime = setting.getValue()
33 val instant = Instant.ofEpochMilli(epochTime * 1000) 34 val instant = Instant.ofEpochMilli(epochTime * 1000)
34 val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")) 35 val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
35 val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) 36 val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
36 binding.textSettingValue.text = dateFormatter.format(zonedTime) 37 binding.textSettingValue.text = dateFormatter.format(zonedTime)
37 38
39 binding.buttonClear.visibility = if (setting.setting.global ||
40 !NativeConfig.isPerGameConfigLoaded()
41 ) {
42 View.GONE
43 } else {
44 View.VISIBLE
45 }
46 binding.buttonClear.setOnClickListener {
47 adapter.onClearClick(setting, bindingAdapterPosition)
48 }
49
38 setStyle(setting.isEditable, binding) 50 setStyle(setting.isEditable, binding)
39 } 51 }
40 52
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt
index 036195624..507184238 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/RunnableViewHolder.kt
@@ -38,6 +38,7 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
38 binding.textSettingDescription.visibility = View.GONE 38 binding.textSettingDescription.visibility = View.GONE
39 } 39 }
40 binding.textSettingValue.visibility = View.GONE 40 binding.textSettingValue.visibility = View.GONE
41 binding.buttonClear.visibility = View.GONE
41 42
42 setStyle(setting.isEditable, binding) 43 setStyle(setting.isEditable, binding)
43 } 44 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt
index 0fd1d2eaa..d26887df8 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SettingViewHolder.kt
@@ -41,6 +41,7 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
41 binding.textSettingName.alpha = opacity 41 binding.textSettingName.alpha = opacity
42 binding.textSettingDescription.alpha = opacity 42 binding.textSettingDescription.alpha = opacity
43 binding.textSettingValue.alpha = opacity 43 binding.textSettingValue.alpha = opacity
44 binding.buttonClear.isEnabled = isEditable
44 } 45 }
45 46
46 fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) { 47 fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) {
@@ -48,5 +49,6 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
48 val opacity = if (isEditable) 1.0f else 0.5f 49 val opacity = if (isEditable) 1.0f else 0.5f
49 binding.textSettingName.alpha = opacity 50 binding.textSettingName.alpha = opacity
50 binding.textSettingDescription.alpha = opacity 51 binding.textSettingDescription.alpha = opacity
52 binding.buttonClear.isEnabled = isEditable
51 } 53 }
52} 54}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
index 80d1b22c1..02dab3785 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SingleChoiceViewHolder.kt
@@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
9import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting 9import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
10import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting 10import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
11import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter 11import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
12import org.yuzu.yuzu_emu.utils.NativeConfig
12 13
13class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : 14class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
14 SettingViewHolder(binding.root, adapter) { 15 SettingViewHolder(binding.root, adapter) {
@@ -29,20 +30,31 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
29 val resMgr = binding.textSettingValue.context.resources 30 val resMgr = binding.textSettingValue.context.resources
30 val values = resMgr.getIntArray(item.valuesId) 31 val values = resMgr.getIntArray(item.valuesId)
31 for (i in values.indices) { 32 for (i in values.indices) {
32 if (values[i] == item.selectedValue) { 33 if (values[i] == item.getSelectedValue()) {
33 binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i] 34 binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i]
34 break 35 break
35 } 36 }
36 } 37 }
37 } else if (item is StringSingleChoiceSetting) { 38 } else if (item is StringSingleChoiceSetting) {
38 for (i in item.values.indices) { 39 for (i in item.values.indices) {
39 if (item.values[i] == item.selectedValue) { 40 if (item.values[i] == item.getSelectedValue()) {
40 binding.textSettingValue.text = item.choices[i] 41 binding.textSettingValue.text = item.choices[i]
41 break 42 break
42 } 43 }
43 } 44 }
44 } 45 }
45 46
47 binding.buttonClear.visibility = if (setting.setting.global ||
48 !NativeConfig.isPerGameConfigLoaded()
49 ) {
50 View.GONE
51 } else {
52 View.VISIBLE
53 }
54 binding.buttonClear.setOnClickListener {
55 adapter.onClearClick(setting, bindingAdapterPosition)
56 }
57
46 setStyle(setting.isEditable, binding) 58 setStyle(setting.isEditable, binding)
47 } 59 }
48 60
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
index b83c90100..596c18012 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SliderViewHolder.kt
@@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
9import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem 9import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
10import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting 10import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
11import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter 11import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
12import org.yuzu.yuzu_emu.utils.NativeConfig
12 13
13class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) : 14class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
14 SettingViewHolder(binding.root, adapter) { 15 SettingViewHolder(binding.root, adapter) {
@@ -26,10 +27,21 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
26 binding.textSettingValue.visibility = View.VISIBLE 27 binding.textSettingValue.visibility = View.VISIBLE
27 binding.textSettingValue.text = String.format( 28 binding.textSettingValue.text = String.format(
28 binding.textSettingValue.context.getString(R.string.value_with_units), 29 binding.textSettingValue.context.getString(R.string.value_with_units),
29 setting.selectedValue, 30 setting.getSelectedValue(),
30 setting.units 31 setting.units
31 ) 32 )
32 33
34 binding.buttonClear.visibility = if (setting.setting.global ||
35 !NativeConfig.isPerGameConfigLoaded()
36 ) {
37 View.GONE
38 } else {
39 View.VISIBLE
40 }
41 binding.buttonClear.setOnClickListener {
42 adapter.onClearClick(setting, bindingAdapterPosition)
43 }
44
33 setStyle(setting.isEditable, binding) 45 setStyle(setting.isEditable, binding)
34 } 46 }
35 47
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt
index 8100c65dd..20d35a17d 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SubmenuViewHolder.kt
@@ -37,6 +37,7 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd
37 binding.textSettingDescription.visibility = View.GONE 37 binding.textSettingDescription.visibility = View.GONE
38 } 38 }
39 binding.textSettingValue.visibility = View.GONE 39 binding.textSettingValue.visibility = View.GONE
40 binding.buttonClear.visibility = View.GONE
40 } 41 }
41 42
42 override fun onClick(clicked: View) { 43 override fun onClick(clicked: View) {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
index 57fdeaa20..d26bf9374 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/viewholder/SwitchSettingViewHolder.kt
@@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
9import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem 9import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
10import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting 10import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting
11import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter 11import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
12import org.yuzu.yuzu_emu.utils.NativeConfig
12 13
13class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) : 14class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) :
14 SettingViewHolder(binding.root, adapter) { 15 SettingViewHolder(binding.root, adapter) {
@@ -27,9 +28,20 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
27 } 28 }
28 29
29 binding.switchWidget.setOnCheckedChangeListener(null) 30 binding.switchWidget.setOnCheckedChangeListener(null)
30 binding.switchWidget.isChecked = setting.checked 31 binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal)
31 binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> 32 binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
32 adapter.onBooleanClick(item, binding.switchWidget.isChecked) 33 adapter.onBooleanClick(item, binding.switchWidget.isChecked, bindingAdapterPosition)
34 }
35
36 binding.buttonClear.visibility = if (setting.setting.global ||
37 !NativeConfig.isPerGameConfigLoaded()
38 ) {
39 View.GONE
40 } else {
41 View.VISIBLE
42 }
43 binding.buttonClear.setOnClickListener {
44 adapter.onClearClick(setting, bindingAdapterPosition)
33 } 45 }
34 46
35 setStyle(setting.isEditable, binding) 47 setStyle(setting.isEditable, binding)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt
index 3ae5b4653..5d523be67 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/utils/SettingsFile.kt
@@ -3,15 +3,27 @@
3 3
4package org.yuzu.yuzu_emu.features.settings.utils 4package org.yuzu.yuzu_emu.features.settings.utils
5 5
6import android.net.Uri
7import org.yuzu.yuzu_emu.model.Game
6import java.io.* 8import java.io.*
7import org.yuzu.yuzu_emu.utils.DirectoryInitialization 9import org.yuzu.yuzu_emu.utils.DirectoryInitialization
10import org.yuzu.yuzu_emu.utils.FileUtil
11import org.yuzu.yuzu_emu.utils.NativeConfig
8 12
9/** 13/**
10 * Contains static methods for interacting with .ini files in which settings are stored. 14 * Contains static methods for interacting with .ini files in which settings are stored.
11 */ 15 */
12object SettingsFile { 16object SettingsFile {
13 const val FILE_NAME_CONFIG = "config" 17 const val FILE_NAME_CONFIG = "config.ini"
14 18
15 fun getSettingsFile(fileName: String): File = 19 fun getSettingsFile(fileName: String): File =
16 File(DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini") 20 File(DirectoryInitialization.userDirectory + "/config/" + fileName)
21
22 fun getCustomSettingsFile(game: Game): File =
23 File(DirectoryInitialization.userDirectory + "/config/custom/" + game.settingsName + ".ini")
24
25 fun loadCustomConfig(game: Game) {
26 val fileName = FileUtil.getFilename(Uri.parse(game.path))
27 NativeConfig.initializePerGameConfig(game.programId, fileName)
28 }
17} 29}
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
new file mode 100644
index 000000000..0dce8ad8d
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt
@@ -0,0 +1,214 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.annotation.SuppressLint
7import android.content.Intent
8import android.os.Bundle
9import android.view.LayoutInflater
10import android.view.View
11import android.view.ViewGroup
12import androidx.activity.result.contract.ActivityResultContracts
13import androidx.core.view.ViewCompat
14import androidx.core.view.WindowInsetsCompat
15import androidx.core.view.updatePadding
16import androidx.documentfile.provider.DocumentFile
17import androidx.fragment.app.Fragment
18import androidx.fragment.app.activityViewModels
19import androidx.lifecycle.Lifecycle
20import androidx.lifecycle.lifecycleScope
21import androidx.lifecycle.repeatOnLifecycle
22import androidx.navigation.findNavController
23import androidx.navigation.fragment.navArgs
24import androidx.recyclerview.widget.LinearLayoutManager
25import com.google.android.material.transition.MaterialSharedAxis
26import kotlinx.coroutines.launch
27import org.yuzu.yuzu_emu.R
28import org.yuzu.yuzu_emu.adapters.AddonAdapter
29import org.yuzu.yuzu_emu.databinding.FragmentAddonsBinding
30import org.yuzu.yuzu_emu.model.AddonViewModel
31import org.yuzu.yuzu_emu.model.HomeViewModel
32import org.yuzu.yuzu_emu.utils.AddonUtil
33import org.yuzu.yuzu_emu.utils.FileUtil.copyFilesTo
34import java.io.File
35
36class AddonsFragment : Fragment() {
37 private var _binding: FragmentAddonsBinding? = null
38 private val binding get() = _binding!!
39
40 private val homeViewModel: HomeViewModel by activityViewModels()
41 private val addonViewModel: AddonViewModel by activityViewModels()
42
43 private val args by navArgs<AddonsFragmentArgs>()
44
45 override fun onCreate(savedInstanceState: Bundle?) {
46 super.onCreate(savedInstanceState)
47 addonViewModel.onOpenAddons(args.game)
48 enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
49 returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
50 reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
51 }
52
53 override fun onCreateView(
54 inflater: LayoutInflater,
55 container: ViewGroup?,
56 savedInstanceState: Bundle?
57 ): View {
58 _binding = FragmentAddonsBinding.inflate(inflater)
59 return binding.root
60 }
61
62 // This is using the correct scope, lint is just acting up
63 @SuppressLint("UnsafeRepeatOnLifecycleDetector")
64 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
65 super.onViewCreated(view, savedInstanceState)
66 homeViewModel.setNavigationVisibility(visible = false, animated = false)
67 homeViewModel.setStatusBarShadeVisibility(false)
68
69 binding.toolbarAddons.setNavigationOnClickListener {
70 binding.root.findNavController().popBackStack()
71 }
72
73 binding.toolbarAddons.title = getString(R.string.addons_game, args.game.title)
74
75 binding.listAddons.apply {
76 layoutManager = LinearLayoutManager(requireContext())
77 adapter = AddonAdapter()
78 }
79
80 viewLifecycleOwner.lifecycleScope.apply {
81 launch {
82 repeatOnLifecycle(Lifecycle.State.STARTED) {
83 addonViewModel.addonList.collect {
84 (binding.listAddons.adapter as AddonAdapter).submitList(it)
85 }
86 }
87 }
88 launch {
89 repeatOnLifecycle(Lifecycle.State.STARTED) {
90 addonViewModel.showModInstallPicker.collect {
91 if (it) {
92 installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
93 addonViewModel.showModInstallPicker(false)
94 }
95 }
96 }
97 }
98 launch {
99 repeatOnLifecycle(Lifecycle.State.STARTED) {
100 addonViewModel.showModNoticeDialog.collect {
101 if (it) {
102 MessageDialogFragment.newInstance(
103 requireActivity(),
104 titleId = R.string.addon_notice,
105 descriptionId = R.string.addon_notice_description,
106 positiveAction = { addonViewModel.showModInstallPicker(true) }
107 ).show(parentFragmentManager, MessageDialogFragment.TAG)
108 addonViewModel.showModNoticeDialog(false)
109 }
110 }
111 }
112 }
113 }
114
115 binding.buttonInstall.setOnClickListener {
116 ContentTypeSelectionDialogFragment().show(
117 parentFragmentManager,
118 ContentTypeSelectionDialogFragment.TAG
119 )
120 }
121
122 setInsets()
123 }
124
125 override fun onResume() {
126 super.onResume()
127 addonViewModel.refreshAddons()
128 }
129
130 override fun onDestroy() {
131 super.onDestroy()
132 addonViewModel.onCloseAddons()
133 }
134
135 val installAddon =
136 registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
137 if (result == null) {
138 return@registerForActivityResult
139 }
140
141 val externalAddonDirectory = DocumentFile.fromTreeUri(requireContext(), result)
142 if (externalAddonDirectory == null) {
143 MessageDialogFragment.newInstance(
144 requireActivity(),
145 titleId = R.string.invalid_directory,
146 descriptionId = R.string.invalid_directory_description
147 ).show(parentFragmentManager, MessageDialogFragment.TAG)
148 return@registerForActivityResult
149 }
150
151 val isValid = externalAddonDirectory.listFiles()
152 .any { AddonUtil.validAddonDirectories.contains(it.name) }
153 val errorMessage = MessageDialogFragment.newInstance(
154 requireActivity(),
155 titleId = R.string.invalid_directory,
156 descriptionId = R.string.invalid_directory_description
157 )
158 if (isValid) {
159 IndeterminateProgressDialogFragment.newInstance(
160 requireActivity(),
161 R.string.installing_game_content,
162 false
163 ) {
164 val parentDirectoryName = externalAddonDirectory.name
165 val internalAddonDirectory =
166 File(args.game.addonDir + parentDirectoryName)
167 try {
168 externalAddonDirectory.copyFilesTo(internalAddonDirectory)
169 } catch (_: Exception) {
170 return@newInstance errorMessage
171 }
172 addonViewModel.refreshAddons()
173 return@newInstance getString(R.string.addon_installed_successfully)
174 }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
175 } else {
176 errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG)
177 }
178 }
179
180 private fun setInsets() =
181 ViewCompat.setOnApplyWindowInsetsListener(
182 binding.root
183 ) { _: View, windowInsets: WindowInsetsCompat ->
184 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
185 val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
186
187 val leftInsets = barInsets.left + cutoutInsets.left
188 val rightInsets = barInsets.right + cutoutInsets.right
189
190 val mlpToolbar = binding.toolbarAddons.layoutParams as ViewGroup.MarginLayoutParams
191 mlpToolbar.leftMargin = leftInsets
192 mlpToolbar.rightMargin = rightInsets
193 binding.toolbarAddons.layoutParams = mlpToolbar
194
195 val mlpAddonsList = binding.listAddons.layoutParams as ViewGroup.MarginLayoutParams
196 mlpAddonsList.leftMargin = leftInsets
197 mlpAddonsList.rightMargin = rightInsets
198 binding.listAddons.layoutParams = mlpAddonsList
199 binding.listAddons.updatePadding(
200 bottom = barInsets.bottom +
201 resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
202 )
203
204 val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
205 val mlpFab =
206 binding.buttonInstall.layoutParams as ViewGroup.MarginLayoutParams
207 mlpFab.leftMargin = leftInsets + fabSpacing
208 mlpFab.rightMargin = rightInsets + fabSpacing
209 mlpFab.bottomMargin = barInsets.bottom + fabSpacing
210 binding.buttonInstall.layoutParams = mlpFab
211
212 windowInsets
213 }
214}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ContentTypeSelectionDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ContentTypeSelectionDialogFragment.kt
new file mode 100644
index 000000000..c1d8b9ea5
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ContentTypeSelectionDialogFragment.kt
@@ -0,0 +1,68 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.app.Dialog
7import android.content.DialogInterface
8import android.os.Bundle
9import androidx.fragment.app.DialogFragment
10import androidx.fragment.app.activityViewModels
11import androidx.preference.PreferenceManager
12import com.google.android.material.dialog.MaterialAlertDialogBuilder
13import org.yuzu.yuzu_emu.R
14import org.yuzu.yuzu_emu.YuzuApplication
15import org.yuzu.yuzu_emu.model.AddonViewModel
16import org.yuzu.yuzu_emu.ui.main.MainActivity
17
18class ContentTypeSelectionDialogFragment : DialogFragment() {
19 private val addonViewModel: AddonViewModel by activityViewModels()
20
21 private val preferences get() =
22 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
23
24 private var selectedItem = 0
25
26 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
27 val launchOptions =
28 arrayOf(getString(R.string.updates_and_dlc), getString(R.string.mods_and_cheats))
29
30 if (savedInstanceState != null) {
31 selectedItem = savedInstanceState.getInt(SELECTED_ITEM)
32 }
33
34 val mainActivity = requireActivity() as MainActivity
35 return MaterialAlertDialogBuilder(requireContext())
36 .setTitle(R.string.select_content_type)
37 .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
38 when (selectedItem) {
39 0 -> mainActivity.installGameUpdate.launch(arrayOf("*/*"))
40 else -> {
41 if (!preferences.getBoolean(MOD_NOTICE_SEEN, false)) {
42 preferences.edit().putBoolean(MOD_NOTICE_SEEN, true).apply()
43 addonViewModel.showModNoticeDialog(true)
44 return@setPositiveButton
45 }
46 addonViewModel.showModInstallPicker(true)
47 }
48 }
49 }
50 .setSingleChoiceItems(launchOptions, 0) { _: DialogInterface, i: Int ->
51 selectedItem = i
52 }
53 .setNegativeButton(android.R.string.cancel, null)
54 .show()
55 }
56
57 override fun onSaveInstanceState(outState: Bundle) {
58 super.onSaveInstanceState(outState)
59 outState.putInt(SELECTED_ITEM, selectedItem)
60 }
61
62 companion object {
63 const val TAG = "ContentTypeSelectionDialogFragment"
64
65 private const val SELECTED_ITEM = "SelectedItem"
66 private const val MOD_NOTICE_SEEN = "ModNoticeSeen"
67 }
68}
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 df21d74b2..cc71254dc 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
@@ -15,6 +15,7 @@ import androidx.fragment.app.Fragment
15import androidx.fragment.app.activityViewModels 15import androidx.fragment.app.activityViewModels
16import androidx.lifecycle.lifecycleScope 16import androidx.lifecycle.lifecycleScope
17import androidx.navigation.findNavController 17import androidx.navigation.findNavController
18import androidx.navigation.fragment.navArgs
18import androidx.recyclerview.widget.GridLayoutManager 19import androidx.recyclerview.widget.GridLayoutManager
19import com.google.android.material.transition.MaterialSharedAxis 20import com.google.android.material.transition.MaterialSharedAxis
20import kotlinx.coroutines.flow.collectLatest 21import kotlinx.coroutines.flow.collectLatest
@@ -36,6 +37,8 @@ class DriverManagerFragment : Fragment() {
36 private val homeViewModel: HomeViewModel by activityViewModels() 37 private val homeViewModel: HomeViewModel by activityViewModels()
37 private val driverViewModel: DriverViewModel by activityViewModels() 38 private val driverViewModel: DriverViewModel by activityViewModels()
38 39
40 private val args by navArgs<DriverManagerFragmentArgs>()
41
39 override fun onCreate(savedInstanceState: Bundle?) { 42 override fun onCreate(savedInstanceState: Bundle?) {
40 super.onCreate(savedInstanceState) 43 super.onCreate(savedInstanceState)
41 enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) 44 enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
@@ -57,7 +60,9 @@ class DriverManagerFragment : Fragment() {
57 homeViewModel.setNavigationVisibility(visible = false, animated = true) 60 homeViewModel.setNavigationVisibility(visible = false, animated = true)
58 homeViewModel.setStatusBarShadeVisibility(visible = false) 61 homeViewModel.setStatusBarShadeVisibility(visible = false)
59 62
60 if (!driverViewModel.isInteractionAllowed) { 63 driverViewModel.onOpenDriverManager(args.game)
64
65 if (!driverViewModel.isInteractionAllowed.value) {
61 DriversLoadingDialogFragment().show( 66 DriversLoadingDialogFragment().show(
62 childFragmentManager, 67 childFragmentManager,
63 DriversLoadingDialogFragment.TAG 68 DriversLoadingDialogFragment.TAG
@@ -102,10 +107,9 @@ class DriverManagerFragment : Fragment() {
102 setInsets() 107 setInsets()
103 } 108 }
104 109
105 // Start installing requested driver 110 override fun onDestroy() {
106 override fun onStop() { 111 super.onDestroy()
107 super.onStop() 112 driverViewModel.onCloseDriverManager(args.game)
108 driverViewModel.onCloseDriverManager()
109 } 113 }
110 114
111 private fun setInsets() = 115 private fun setInsets() =
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt
index f8c34346a..6a47b29f0 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/DriversLoadingDialogFragment.kt
@@ -47,25 +47,9 @@ class DriversLoadingDialogFragment : DialogFragment() {
47 viewLifecycleOwner.lifecycleScope.apply { 47 viewLifecycleOwner.lifecycleScope.apply {
48 launch { 48 launch {
49 repeatOnLifecycle(Lifecycle.State.RESUMED) { 49 repeatOnLifecycle(Lifecycle.State.RESUMED) {
50 driverViewModel.areDriversLoading.collect { checkForDismiss() } 50 driverViewModel.isInteractionAllowed.collect { if (it) dismiss() }
51 } 51 }
52 } 52 }
53 launch {
54 repeatOnLifecycle(Lifecycle.State.RESUMED) {
55 driverViewModel.isDriverReady.collect { checkForDismiss() }
56 }
57 }
58 launch {
59 repeatOnLifecycle(Lifecycle.State.RESUMED) {
60 driverViewModel.isDeletingDrivers.collect { checkForDismiss() }
61 }
62 }
63 }
64 }
65
66 private fun checkForDismiss() {
67 if (driverViewModel.isInteractionAllowed) {
68 dismiss()
69 } 53 }
70 } 54 }
71 55
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
index 734c1d5ca..d7b38f62d 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt
@@ -52,6 +52,7 @@ import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
52import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding 52import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
53import org.yuzu.yuzu_emu.features.settings.model.IntSetting 53import org.yuzu.yuzu_emu.features.settings.model.IntSetting
54import org.yuzu.yuzu_emu.features.settings.model.Settings 54import org.yuzu.yuzu_emu.features.settings.model.Settings
55import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
55import org.yuzu.yuzu_emu.model.DriverViewModel 56import org.yuzu.yuzu_emu.model.DriverViewModel
56import org.yuzu.yuzu_emu.model.Game 57import org.yuzu.yuzu_emu.model.Game
57import org.yuzu.yuzu_emu.model.EmulationViewModel 58import org.yuzu.yuzu_emu.model.EmulationViewModel
@@ -127,6 +128,17 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
127 return 128 return
128 } 129 }
129 130
131 // Always load custom settings when launching a game from an intent
132 if (args.custom || intentGame != null) {
133 SettingsFile.loadCustomConfig(game)
134 NativeConfig.unloadPerGameConfig()
135 } else {
136 NativeConfig.reloadGlobalConfig()
137 }
138
139 // Install the selected driver asynchronously as the game starts
140 driverViewModel.onLaunchGame()
141
130 // So this fragment doesn't restart on configuration changes; i.e. rotation. 142 // So this fragment doesn't restart on configuration changes; i.e. rotation.
131 retainInstance = true 143 retainInstance = true
132 preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) 144 preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
@@ -217,6 +229,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
217 true 229 true
218 } 230 }
219 231
232 R.id.menu_settings_per_game -> {
233 val action = HomeNavigationDirections.actionGlobalSettingsActivity(
234 args.game,
235 Settings.MenuTag.SECTION_ROOT
236 )
237 binding.root.findNavController().navigate(action)
238 true
239 }
240
220 R.id.menu_overlay_controls -> { 241 R.id.menu_overlay_controls -> {
221 showOverlayOptions() 242 showOverlayOptions()
222 true 243 true
@@ -332,15 +353,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
332 } 353 }
333 launch { 354 launch {
334 repeatOnLifecycle(Lifecycle.State.RESUMED) { 355 repeatOnLifecycle(Lifecycle.State.RESUMED) {
335 driverViewModel.isDriverReady.collect { 356 driverViewModel.isInteractionAllowed.collect {
336 if (it && !emulationState.isRunning) { 357 if (it) {
337 if (!DirectoryInitialization.areDirectoriesReady) { 358 onEmulationStart()
338 DirectoryInitialization.start()
339 }
340
341 updateScreenLayout()
342
343 emulationState.run(emulationActivity!!.isActivityRecreated)
344 } 359 }
345 } 360 }
346 } 361 }
@@ -348,6 +363,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
348 } 363 }
349 } 364 }
350 365
366 private fun onEmulationStart() {
367 if (!NativeLibrary.isRunning() && !NativeLibrary.isPaused()) {
368 if (!DirectoryInitialization.areDirectoriesReady) {
369 DirectoryInitialization.start()
370 }
371
372 updateScreenLayout()
373
374 emulationState.run(emulationActivity!!.isActivityRecreated)
375 }
376 }
377
351 override fun onConfigurationChanged(newConfig: Configuration) { 378 override fun onConfigurationChanged(newConfig: Configuration) {
352 super.onConfigurationChanged(newConfig) 379 super.onConfigurationChanged(newConfig)
353 if (_binding == null) { 380 if (_binding == null) {
@@ -435,7 +462,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
435 @SuppressLint("SourceLockedOrientationActivity") 462 @SuppressLint("SourceLockedOrientationActivity")
436 private fun updateOrientation() { 463 private fun updateOrientation() {
437 emulationActivity?.let { 464 emulationActivity?.let {
438 it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) { 465 it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.getInt()) {
439 Settings.LayoutOption_MobileLandscape -> 466 Settings.LayoutOption_MobileLandscape ->
440 ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE 467 ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
441 Settings.LayoutOption_MobilePortrait -> 468 Settings.LayoutOption_MobilePortrait ->
@@ -617,7 +644,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
617 @SuppressLint("SourceLockedOrientationActivity") 644 @SuppressLint("SourceLockedOrientationActivity")
618 private fun startConfiguringControls() { 645 private fun startConfiguringControls() {
619 // Lock the current orientation to prevent editing inconsistencies 646 // Lock the current orientation to prevent editing inconsistencies
620 if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) { 647 if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) {
621 emulationActivity?.let { 648 emulationActivity?.let {
622 it.requestedOrientation = 649 it.requestedOrientation =
623 if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { 650 if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
@@ -635,7 +662,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
635 binding.doneControlConfig.visibility = View.GONE 662 binding.doneControlConfig.visibility = View.GONE
636 binding.surfaceInputOverlay.setIsInEditMode(false) 663 binding.surfaceInputOverlay.setIsInEditMode(false)
637 // Unlock the orientation if it was locked for editing 664 // Unlock the orientation if it was locked for editing
638 if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) { 665 if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) {
639 emulationActivity?.let { 666 emulationActivity?.let {
640 it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED 667 it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
641 } 668 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt
index b6c2e4635..1ea1e036e 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameFolderPropertiesDialogFragment.kt
@@ -13,6 +13,7 @@ import org.yuzu.yuzu_emu.R
13import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding 13import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
14import org.yuzu.yuzu_emu.model.GameDir 14import org.yuzu.yuzu_emu.model.GameDir
15import org.yuzu.yuzu_emu.model.GamesViewModel 15import org.yuzu.yuzu_emu.model.GamesViewModel
16import org.yuzu.yuzu_emu.utils.NativeConfig
16import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable 17import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
17 18
18class GameFolderPropertiesDialogFragment : DialogFragment() { 19class GameFolderPropertiesDialogFragment : DialogFragment() {
@@ -49,6 +50,11 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
49 .show() 50 .show()
50 } 51 }
51 52
53 override fun onStop() {
54 super.onStop()
55 NativeConfig.saveGlobalConfig()
56 }
57
52 override fun onSaveInstanceState(outState: Bundle) { 58 override fun onSaveInstanceState(outState: Bundle) {
53 super.onSaveInstanceState(outState) 59 super.onSaveInstanceState(outState)
54 outState.putBoolean(DEEP_SCAN, deepScan) 60 outState.putBoolean(DEEP_SCAN, deepScan)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt
new file mode 100644
index 000000000..fa2a4c9f9
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt
@@ -0,0 +1,148 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.content.ClipData
7import android.content.ClipboardManager
8import android.content.Context
9import android.net.Uri
10import android.os.Build
11import android.os.Bundle
12import android.view.LayoutInflater
13import android.view.View
14import android.view.ViewGroup
15import android.widget.Toast
16import androidx.core.view.ViewCompat
17import androidx.core.view.WindowInsetsCompat
18import androidx.core.view.updatePadding
19import androidx.fragment.app.Fragment
20import androidx.fragment.app.activityViewModels
21import androidx.navigation.findNavController
22import androidx.navigation.fragment.navArgs
23import com.google.android.material.transition.MaterialSharedAxis
24import org.yuzu.yuzu_emu.R
25import org.yuzu.yuzu_emu.databinding.FragmentGameInfoBinding
26import org.yuzu.yuzu_emu.model.HomeViewModel
27import org.yuzu.yuzu_emu.utils.GameMetadata
28
29class GameInfoFragment : Fragment() {
30 private var _binding: FragmentGameInfoBinding? = null
31 private val binding get() = _binding!!
32
33 private val homeViewModel: HomeViewModel by activityViewModels()
34
35 private val args by navArgs<GameInfoFragmentArgs>()
36
37 override fun onCreate(savedInstanceState: Bundle?) {
38 super.onCreate(savedInstanceState)
39 enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
40 returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
41 reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
42
43 // Check for an up-to-date version string
44 args.game.version = GameMetadata.getVersion(args.game.path, true)
45 }
46
47 override fun onCreateView(
48 inflater: LayoutInflater,
49 container: ViewGroup?,
50 savedInstanceState: Bundle?
51 ): View {
52 _binding = FragmentGameInfoBinding.inflate(inflater)
53 return binding.root
54 }
55
56 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
57 super.onViewCreated(view, savedInstanceState)
58 homeViewModel.setNavigationVisibility(visible = false, animated = false)
59 homeViewModel.setStatusBarShadeVisibility(false)
60
61 binding.apply {
62 toolbarInfo.title = args.game.title
63 toolbarInfo.setNavigationOnClickListener {
64 view.findNavController().popBackStack()
65 }
66
67 val pathString = Uri.parse(args.game.path).path ?: ""
68 path.setHint(R.string.path)
69 pathField.setText(pathString)
70 pathField.setOnClickListener { copyToClipboard(getString(R.string.path), pathString) }
71
72 programId.setHint(R.string.program_id)
73 programIdField.setText(args.game.programIdHex)
74 programIdField.setOnClickListener {
75 copyToClipboard(getString(R.string.program_id), args.game.programIdHex)
76 }
77
78 if (args.game.developer.isNotEmpty()) {
79 developer.setHint(R.string.developer)
80 developerField.setText(args.game.developer)
81 developerField.setOnClickListener {
82 copyToClipboard(getString(R.string.developer), args.game.developer)
83 }
84 } else {
85 developer.visibility = View.GONE
86 }
87
88 version.setHint(R.string.version)
89 versionField.setText(args.game.version)
90 versionField.setOnClickListener {
91 copyToClipboard(getString(R.string.version), args.game.version)
92 }
93
94 buttonCopy.setOnClickListener {
95 val details = """
96 ${args.game.title}
97 ${getString(R.string.path)} - $pathString
98 ${getString(R.string.program_id)} - ${args.game.programIdHex}
99 ${getString(R.string.developer)} - ${args.game.developer}
100 ${getString(R.string.version)} - ${args.game.version}
101 """.trimIndent()
102 copyToClipboard(args.game.title, details)
103 }
104 }
105
106 setInsets()
107 }
108
109 private fun copyToClipboard(label: String, body: String) {
110 val clipBoard =
111 requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
112 val clip = ClipData.newPlainText(label, body)
113 clipBoard.setPrimaryClip(clip)
114
115 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
116 Toast.makeText(
117 requireContext(),
118 R.string.copied_to_clipboard,
119 Toast.LENGTH_SHORT
120 ).show()
121 }
122 }
123
124 private fun setInsets() =
125 ViewCompat.setOnApplyWindowInsetsListener(
126 binding.root
127 ) { _: View, windowInsets: WindowInsetsCompat ->
128 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
129 val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
130
131 val leftInsets = barInsets.left + cutoutInsets.left
132 val rightInsets = barInsets.right + cutoutInsets.right
133
134 val mlpToolbar = binding.toolbarInfo.layoutParams as ViewGroup.MarginLayoutParams
135 mlpToolbar.leftMargin = leftInsets
136 mlpToolbar.rightMargin = rightInsets
137 binding.toolbarInfo.layoutParams = mlpToolbar
138
139 val mlpScrollAbout = binding.scrollInfo.layoutParams as ViewGroup.MarginLayoutParams
140 mlpScrollAbout.leftMargin = leftInsets
141 mlpScrollAbout.rightMargin = rightInsets
142 binding.scrollInfo.layoutParams = mlpScrollAbout
143
144 binding.contentInfo.updatePadding(bottom = barInsets.bottom)
145
146 windowInsets
147 }
148}
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
new file mode 100644
index 000000000..b1d3c0040
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt
@@ -0,0 +1,456 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.annotation.SuppressLint
7import android.os.Bundle
8import android.text.TextUtils
9import android.view.LayoutInflater
10import android.view.View
11import android.view.ViewGroup
12import android.widget.Toast
13import androidx.activity.result.contract.ActivityResultContracts
14import androidx.core.view.ViewCompat
15import androidx.core.view.WindowInsetsCompat
16import androidx.core.view.updatePadding
17import androidx.fragment.app.Fragment
18import androidx.fragment.app.activityViewModels
19import androidx.lifecycle.Lifecycle
20import androidx.lifecycle.lifecycleScope
21import androidx.lifecycle.repeatOnLifecycle
22import androidx.navigation.findNavController
23import androidx.navigation.fragment.navArgs
24import androidx.recyclerview.widget.GridLayoutManager
25import com.google.android.material.transition.MaterialSharedAxis
26import kotlinx.coroutines.Dispatchers
27import kotlinx.coroutines.launch
28import kotlinx.coroutines.withContext
29import org.yuzu.yuzu_emu.HomeNavigationDirections
30import org.yuzu.yuzu_emu.R
31import org.yuzu.yuzu_emu.YuzuApplication
32import org.yuzu.yuzu_emu.adapters.GamePropertiesAdapter
33import org.yuzu.yuzu_emu.databinding.FragmentGamePropertiesBinding
34import org.yuzu.yuzu_emu.features.settings.model.Settings
35import org.yuzu.yuzu_emu.model.DriverViewModel
36import org.yuzu.yuzu_emu.model.GameProperty
37import org.yuzu.yuzu_emu.model.GamesViewModel
38import org.yuzu.yuzu_emu.model.HomeViewModel
39import org.yuzu.yuzu_emu.model.InstallableProperty
40import org.yuzu.yuzu_emu.model.SubmenuProperty
41import org.yuzu.yuzu_emu.model.TaskState
42import org.yuzu.yuzu_emu.utils.DirectoryInitialization
43import org.yuzu.yuzu_emu.utils.FileUtil
44import org.yuzu.yuzu_emu.utils.GameIconUtils
45import org.yuzu.yuzu_emu.utils.GpuDriverHelper
46import org.yuzu.yuzu_emu.utils.MemoryUtil
47import java.io.BufferedInputStream
48import java.io.BufferedOutputStream
49import java.io.File
50
51class GamePropertiesFragment : Fragment() {
52 private var _binding: FragmentGamePropertiesBinding? = null
53 private val binding get() = _binding!!
54
55 private val homeViewModel: HomeViewModel by activityViewModels()
56 private val gamesViewModel: GamesViewModel by activityViewModels()
57 private val driverViewModel: DriverViewModel by activityViewModels()
58
59 private val args by navArgs<GamePropertiesFragmentArgs>()
60
61 override fun onCreate(savedInstanceState: Bundle?) {
62 super.onCreate(savedInstanceState)
63 enterTransition = MaterialSharedAxis(MaterialSharedAxis.Y, true)
64 returnTransition = MaterialSharedAxis(MaterialSharedAxis.Y, false)
65 reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
66 }
67
68 override fun onCreateView(
69 inflater: LayoutInflater,
70 container: ViewGroup?,
71 savedInstanceState: Bundle?
72 ): View {
73 _binding = FragmentGamePropertiesBinding.inflate(layoutInflater)
74 return binding.root
75 }
76
77 // This is using the correct scope, lint is just acting up
78 @SuppressLint("UnsafeRepeatOnLifecycleDetector")
79 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
80 super.onViewCreated(view, savedInstanceState)
81 homeViewModel.setNavigationVisibility(visible = false, animated = true)
82 homeViewModel.setStatusBarShadeVisibility(true)
83
84 binding.buttonBack.setOnClickListener {
85 view.findNavController().popBackStack()
86 }
87
88 GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen)
89 binding.title.text = args.game.title
90 binding.title.postDelayed(
91 {
92 binding.title.ellipsize = TextUtils.TruncateAt.MARQUEE
93 binding.title.isSelected = true
94 },
95 3000
96 )
97
98 binding.buttonStart.setOnClickListener {
99 LaunchGameDialogFragment.newInstance(args.game)
100 .show(childFragmentManager, LaunchGameDialogFragment.TAG)
101 }
102
103 reloadList()
104
105 viewLifecycleOwner.lifecycleScope.apply {
106 launch {
107 repeatOnLifecycle(Lifecycle.State.STARTED) {
108 homeViewModel.openImportSaves.collect {
109 if (it) {
110 importSaves.launch(arrayOf("application/zip"))
111 homeViewModel.setOpenImportSaves(false)
112 }
113 }
114 }
115 }
116 launch {
117 repeatOnLifecycle(Lifecycle.State.STARTED) {
118 homeViewModel.reloadPropertiesList.collect {
119 if (it) {
120 reloadList()
121 homeViewModel.reloadPropertiesList(false)
122 }
123 }
124 }
125 }
126 }
127
128 setInsets()
129 }
130
131 override fun onDestroy() {
132 super.onDestroy()
133 gamesViewModel.reloadGames(true)
134 }
135
136 private fun reloadList() {
137 _binding ?: return
138
139 driverViewModel.updateDriverNameForGame(args.game)
140 val properties = mutableListOf<GameProperty>().apply {
141 add(
142 SubmenuProperty(
143 R.string.info,
144 R.string.info_description,
145 R.drawable.ic_info_outline
146 ) {
147 val action = GamePropertiesFragmentDirections
148 .actionPerGamePropertiesFragmentToGameInfoFragment(args.game)
149 binding.root.findNavController().navigate(action)
150 }
151 )
152 add(
153 SubmenuProperty(
154 R.string.preferences_settings,
155 R.string.per_game_settings_description,
156 R.drawable.ic_settings
157 ) {
158 val action = HomeNavigationDirections.actionGlobalSettingsActivity(
159 args.game,
160 Settings.MenuTag.SECTION_ROOT
161 )
162 binding.root.findNavController().navigate(action)
163 }
164 )
165
166 if (GpuDriverHelper.supportsCustomDriverLoading()) {
167 add(
168 SubmenuProperty(
169 R.string.gpu_driver_manager,
170 R.string.install_gpu_driver_description,
171 R.drawable.ic_build,
172 detailsFlow = driverViewModel.selectedDriverTitle
173 ) {
174 val action = GamePropertiesFragmentDirections
175 .actionPerGamePropertiesFragmentToDriverManagerFragment(args.game)
176 binding.root.findNavController().navigate(action)
177 }
178 )
179 }
180
181 if (!args.game.isHomebrew) {
182 add(
183 SubmenuProperty(
184 R.string.add_ons,
185 R.string.add_ons_description,
186 R.drawable.ic_edit
187 ) {
188 val action = GamePropertiesFragmentDirections
189 .actionPerGamePropertiesFragmentToAddonsFragment(args.game)
190 binding.root.findNavController().navigate(action)
191 }
192 )
193 add(
194 InstallableProperty(
195 R.string.save_data,
196 R.string.save_data_description,
197 R.drawable.ic_save,
198 {
199 MessageDialogFragment.newInstance(
200 requireActivity(),
201 titleId = R.string.import_save_warning,
202 descriptionId = R.string.import_save_warning_description,
203 positiveAction = { homeViewModel.setOpenImportSaves(true) }
204 ).show(parentFragmentManager, MessageDialogFragment.TAG)
205 },
206 if (File(args.game.saveDir).exists()) {
207 { exportSaves.launch(args.game.saveZipName) }
208 } else {
209 null
210 }
211 )
212 )
213
214 val saveDirFile = File(args.game.saveDir)
215 if (saveDirFile.exists()) {
216 add(
217 SubmenuProperty(
218 R.string.delete_save_data,
219 R.string.delete_save_data_description,
220 R.drawable.ic_delete,
221 action = {
222 MessageDialogFragment.newInstance(
223 requireActivity(),
224 titleId = R.string.delete_save_data,
225 descriptionId = R.string.delete_save_data_warning_description,
226 positiveAction = {
227 File(args.game.saveDir).deleteRecursively()
228 Toast.makeText(
229 YuzuApplication.appContext,
230 R.string.save_data_deleted_successfully,
231 Toast.LENGTH_SHORT
232 ).show()
233 homeViewModel.reloadPropertiesList(true)
234 }
235 ).show(parentFragmentManager, MessageDialogFragment.TAG)
236 }
237 )
238 )
239 }
240
241 val shaderCacheDir = File(
242 DirectoryInitialization.userDirectory +
243 "/shader/" + args.game.settingsName.lowercase()
244 )
245 if (shaderCacheDir.exists()) {
246 add(
247 SubmenuProperty(
248 R.string.clear_shader_cache,
249 R.string.clear_shader_cache_description,
250 R.drawable.ic_delete,
251 {
252 if (shaderCacheDir.exists()) {
253 val bytes = shaderCacheDir.walkTopDown().filter { it.isFile }
254 .map { it.length() }.sum()
255 MemoryUtil.bytesToSizeUnit(bytes.toFloat())
256 } else {
257 MemoryUtil.bytesToSizeUnit(0f)
258 }
259 }
260 ) {
261 MessageDialogFragment.newInstance(
262 requireActivity(),
263 titleId = R.string.clear_shader_cache,
264 descriptionId = R.string.clear_shader_cache_warning_description,
265 positiveAction = {
266 shaderCacheDir.deleteRecursively()
267 Toast.makeText(
268 YuzuApplication.appContext,
269 R.string.cleared_shaders_successfully,
270 Toast.LENGTH_SHORT
271 ).show()
272 homeViewModel.reloadPropertiesList(true)
273 }
274 ).show(parentFragmentManager, MessageDialogFragment.TAG)
275 }
276 )
277 }
278 }
279 }
280 binding.listProperties.apply {
281 layoutManager =
282 GridLayoutManager(requireContext(), resources.getInteger(R.integer.grid_columns))
283 adapter = GamePropertiesAdapter(viewLifecycleOwner, properties)
284 }
285 }
286
287 override fun onResume() {
288 super.onResume()
289 driverViewModel.updateDriverNameForGame(args.game)
290 }
291
292 private fun setInsets() =
293 ViewCompat.setOnApplyWindowInsetsListener(
294 binding.root
295 ) { _: View, windowInsets: WindowInsetsCompat ->
296 val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
297 val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
298
299 val leftInsets = barInsets.left + cutoutInsets.left
300 val rightInsets = barInsets.right + cutoutInsets.right
301
302 val smallLayout = resources.getBoolean(R.bool.small_layout)
303 if (smallLayout) {
304 val mlpListAll =
305 binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
306 mlpListAll.leftMargin = leftInsets
307 mlpListAll.rightMargin = rightInsets
308 binding.listAll.layoutParams = mlpListAll
309 } else {
310 if (ViewCompat.getLayoutDirection(binding.root) ==
311 ViewCompat.LAYOUT_DIRECTION_LTR
312 ) {
313 val mlpListAll =
314 binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
315 mlpListAll.rightMargin = rightInsets
316 binding.listAll.layoutParams = mlpListAll
317
318 val mlpIconLayout =
319 binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams
320 mlpIconLayout.topMargin = barInsets.top
321 mlpIconLayout.leftMargin = leftInsets
322 binding.iconLayout!!.layoutParams = mlpIconLayout
323 } else {
324 val mlpListAll =
325 binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
326 mlpListAll.leftMargin = leftInsets
327 binding.listAll.layoutParams = mlpListAll
328
329 val mlpIconLayout =
330 binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams
331 mlpIconLayout.topMargin = barInsets.top
332 mlpIconLayout.rightMargin = rightInsets
333 binding.iconLayout!!.layoutParams = mlpIconLayout
334 }
335 }
336
337 val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
338 val mlpFab =
339 binding.buttonStart.layoutParams as ViewGroup.MarginLayoutParams
340 mlpFab.leftMargin = leftInsets + fabSpacing
341 mlpFab.rightMargin = rightInsets + fabSpacing
342 mlpFab.bottomMargin = barInsets.bottom + fabSpacing
343 binding.buttonStart.layoutParams = mlpFab
344
345 binding.layoutAll.updatePadding(
346 top = barInsets.top,
347 bottom = barInsets.bottom +
348 resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
349 )
350
351 windowInsets
352 }
353
354 private val importSaves =
355 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
356 if (result == null) {
357 return@registerForActivityResult
358 }
359
360 val inputZip = requireContext().contentResolver.openInputStream(result)
361 val savesFolder = File(args.game.saveDir)
362 val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
363 cacheSaveDir.mkdir()
364
365 if (inputZip == null) {
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(),
376 R.string.save_files_importing,
377 false
378 ) {
379 try {
380 FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
381 val files = cacheSaveDir.listFiles()
382 var savesFolderFile: File? = null
383 if (files != null) {
384 val savesFolderName = args.game.programIdHex
385 for (file in files) {
386 if (file.isDirectory && file.name == savesFolderName) {
387 savesFolderFile = file
388 break
389 }
390 }
391 }
392
393 if (savesFolderFile != null) {
394 savesFolder.deleteRecursively()
395 savesFolder.mkdir()
396 savesFolderFile.copyRecursively(savesFolder)
397 savesFolderFile.deleteRecursively()
398 }
399
400 withContext(Dispatchers.Main) {
401 if (savesFolderFile == null) {
402 MessageDialogFragment.newInstance(
403 requireActivity(),
404 titleId = R.string.save_file_invalid_zip_structure,
405 descriptionId = R.string.save_file_invalid_zip_structure_description
406 ).show(parentFragmentManager, MessageDialogFragment.TAG)
407 return@withContext
408 }
409 Toast.makeText(
410 YuzuApplication.appContext,
411 getString(R.string.save_file_imported_success),
412 Toast.LENGTH_LONG
413 ).show()
414 homeViewModel.reloadPropertiesList(true)
415 }
416
417 cacheSaveDir.deleteRecursively()
418 } catch (e: Exception) {
419 Toast.makeText(
420 YuzuApplication.appContext,
421 getString(R.string.fatal_error),
422 Toast.LENGTH_LONG
423 ).show()
424 }
425 }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
426 }
427
428 /**
429 * Exports the save file located in the given folder path by creating a zip file and opening a
430 * file picker to save.
431 */
432 private val exportSaves = registerForActivityResult(
433 ActivityResultContracts.CreateDocument("application/zip")
434 ) { result ->
435 if (result == null) {
436 return@registerForActivityResult
437 }
438
439 IndeterminateProgressDialogFragment.newInstance(
440 requireActivity(),
441 R.string.save_files_exporting,
442 false
443 ) {
444 val saveLocation = args.game.saveDir
445 val zipResult = FileUtil.zipFromInternalStorage(
446 File(saveLocation),
447 saveLocation.replaceAfterLast("/", ""),
448 BufferedOutputStream(requireContext().contentResolver.openOutputStream(result))
449 )
450 return@newInstance when (zipResult) {
451 TaskState.Completed -> getString(R.string.export_success)
452 TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
453 }
454 }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
455 }
456}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
index 3addc2e63..6ddd758e6 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt
@@ -68,6 +68,9 @@ class HomeSettingsFragment : Fragment() {
68 } 68 }
69 69
70 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 70 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
71 super.onViewCreated(view, savedInstanceState)
72 homeViewModel.setNavigationVisibility(visible = true, animated = true)
73 homeViewModel.setStatusBarShadeVisibility(visible = true)
71 mainActivity = requireActivity() as MainActivity 74 mainActivity = requireActivity() as MainActivity
72 75
73 val optionsList: MutableList<HomeSetting> = mutableListOf<HomeSetting>().apply { 76 val optionsList: MutableList<HomeSetting> = mutableListOf<HomeSetting>().apply {
@@ -91,13 +94,14 @@ class HomeSettingsFragment : Fragment() {
91 R.string.install_gpu_driver_description, 94 R.string.install_gpu_driver_description,
92 R.drawable.ic_build, 95 R.drawable.ic_build,
93 { 96 {
94 binding.root.findNavController() 97 val action = HomeSettingsFragmentDirections
95 .navigate(R.id.action_homeSettingsFragment_to_driverManagerFragment) 98 .actionHomeSettingsFragmentToDriverManagerFragment(null)
99 binding.root.findNavController().navigate(action)
96 }, 100 },
97 { GpuDriverHelper.supportsCustomDriverLoading() }, 101 { GpuDriverHelper.supportsCustomDriverLoading() },
98 R.string.custom_driver_not_supported, 102 R.string.custom_driver_not_supported,
99 R.string.custom_driver_not_supported_description, 103 R.string.custom_driver_not_supported_description,
100 driverViewModel.selectedDriverMetadata 104 driverViewModel.selectedDriverTitle
101 ) 105 )
102 ) 106 )
103 add( 107 add(
@@ -212,8 +216,11 @@ class HomeSettingsFragment : Fragment() {
212 override fun onStart() { 216 override fun onStart() {
213 super.onStart() 217 super.onStart()
214 exitTransition = null 218 exitTransition = null
215 homeViewModel.setNavigationVisibility(visible = true, animated = true) 219 }
216 homeViewModel.setStatusBarShadeVisibility(visible = true) 220
221 override fun onResume() {
222 super.onResume()
223 driverViewModel.updateDriverNameForGame(null)
217 } 224 }
218 225
219 override fun onDestroyView() { 226 override fun onDestroyView() {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
index 7e467814d..8847e5531 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt
@@ -122,7 +122,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
122 activity: FragmentActivity, 122 activity: FragmentActivity,
123 titleId: Int, 123 titleId: Int,
124 cancellable: Boolean = false, 124 cancellable: Boolean = false,
125 task: () -> Any 125 task: suspend () -> Any
126 ): IndeterminateProgressDialogFragment { 126 ): IndeterminateProgressDialogFragment {
127 val dialog = IndeterminateProgressDialogFragment() 127 val dialog = IndeterminateProgressDialogFragment()
128 val args = Bundle() 128 val args = Bundle()
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 6940fc757..569727b90 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
@@ -21,8 +21,6 @@ import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding
21import org.yuzu.yuzu_emu.model.HomeViewModel 21import org.yuzu.yuzu_emu.model.HomeViewModel
22import org.yuzu.yuzu_emu.model.Installable 22import org.yuzu.yuzu_emu.model.Installable
23import org.yuzu.yuzu_emu.ui.main.MainActivity 23import org.yuzu.yuzu_emu.ui.main.MainActivity
24import java.time.LocalDateTime
25import java.time.format.DateTimeFormatter
26 24
27class InstallableFragment : Fragment() { 25class InstallableFragment : Fragment() {
28 private var _binding: FragmentInstallablesBinding? = null 26 private var _binding: FragmentInstallablesBinding? = null
@@ -75,28 +73,6 @@ class InstallableFragment : Fragment() {
75 R.string.install_firmware_description, 73 R.string.install_firmware_description,
76 install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) } 74 install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) }
77 ), 75 ),
78 if (mainActivity.savesFolderRoot != "") {
79 Installable(
80 R.string.manage_save_data,
81 R.string.import_export_saves_description,
82 install = { mainActivity.importSaves.launch(arrayOf("application/zip")) },
83 export = {
84 mainActivity.exportSaves.launch(
85 "yuzu saves - ${
86 LocalDateTime.now().format(
87 DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
88 )
89 }.zip"
90 )
91 }
92 )
93 } else {
94 Installable(
95 R.string.manage_save_data,
96 R.string.import_export_saves_description,
97 install = { mainActivity.importSaves.launch(arrayOf("application/zip")) }
98 )
99 },
100 Installable( 76 Installable(
101 R.string.install_prod_keys, 77 R.string.install_prod_keys,
102 R.string.install_prod_keys_description, 78 R.string.install_prod_keys_description,
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt
new file mode 100644
index 000000000..e1ac46c48
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt
@@ -0,0 +1,61 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.fragments
5
6import android.app.Dialog
7import android.content.DialogInterface
8import android.os.Bundle
9import androidx.fragment.app.DialogFragment
10import androidx.navigation.fragment.findNavController
11import com.google.android.material.dialog.MaterialAlertDialogBuilder
12import org.yuzu.yuzu_emu.HomeNavigationDirections
13import org.yuzu.yuzu_emu.R
14import org.yuzu.yuzu_emu.model.Game
15import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
16
17class LaunchGameDialogFragment : DialogFragment() {
18 private var selectedItem = 1
19
20 override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
21 val game = requireArguments().parcelable<Game>(GAME)
22 val launchOptions = arrayOf(getString(R.string.global), getString(R.string.custom))
23
24 if (savedInstanceState != null) {
25 selectedItem = savedInstanceState.getInt(SELECTED_ITEM)
26 }
27
28 return MaterialAlertDialogBuilder(requireContext())
29 .setTitle(R.string.launch_options)
30 .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
31 val action = HomeNavigationDirections
32 .actionGlobalEmulationActivity(game, selectedItem != 0)
33 requireParentFragment().findNavController().navigate(action)
34 }
35 .setSingleChoiceItems(launchOptions, 1) { _: DialogInterface, i: Int ->
36 selectedItem = i
37 }
38 .setNegativeButton(android.R.string.cancel, null)
39 .show()
40 }
41
42 override fun onSaveInstanceState(outState: Bundle) {
43 super.onSaveInstanceState(outState)
44 outState.putInt(SELECTED_ITEM, selectedItem)
45 }
46
47 companion object {
48 const val TAG = "LaunchGameDialogFragment"
49
50 const val GAME = "Game"
51 const val SELECTED_ITEM = "SelectedItem"
52
53 fun newInstance(game: Game): LaunchGameDialogFragment {
54 val args = Bundle()
55 args.putParcelable(GAME, game)
56 val fragment = LaunchGameDialogFragment()
57 fragment.arguments = args
58 return fragment
59 }
60 }
61}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
index a6183d19e..32062b6fe 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt
@@ -27,30 +27,31 @@ class MessageDialogFragment : DialogFragment() {
27 val descriptionString = requireArguments().getString(DESCRIPTION_STRING)!! 27 val descriptionString = requireArguments().getString(DESCRIPTION_STRING)!!
28 val helpLinkId = requireArguments().getInt(HELP_LINK) 28 val helpLinkId = requireArguments().getInt(HELP_LINK)
29 29
30 val dialog = MaterialAlertDialogBuilder(requireContext()) 30 val builder = MaterialAlertDialogBuilder(requireContext())
31 .setPositiveButton(R.string.close, null)
32 31
33 if (titleId != 0) dialog.setTitle(titleId) 32 if (messageDialogViewModel.positiveAction == null) {
34 if (titleString.isNotEmpty()) dialog.setTitle(titleString) 33 builder.setPositiveButton(R.string.close, null)
34 } else {
35 builder.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
36 messageDialogViewModel.positiveAction?.invoke()
37 }.setNegativeButton(android.R.string.cancel, null)
38 }
39
40 if (titleId != 0) builder.setTitle(titleId)
41 if (titleString.isNotEmpty()) builder.setTitle(titleString)
35 42
36 if (descriptionId != 0) { 43 if (descriptionId != 0) {
37 dialog.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY)) 44 builder.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY))
38 } 45 }
39 if (descriptionString.isNotEmpty()) dialog.setMessage(descriptionString) 46 if (descriptionString.isNotEmpty()) builder.setMessage(descriptionString)
40 47
41 if (helpLinkId != 0) { 48 if (helpLinkId != 0) {
42 dialog.setNeutralButton(R.string.learn_more) { _, _ -> 49 builder.setNeutralButton(R.string.learn_more) { _, _ ->
43 openLink(getString(helpLinkId)) 50 openLink(getString(helpLinkId))
44 } 51 }
45 } 52 }
46 53
47 return dialog.show() 54 return builder.show()
48 }
49
50 override fun onDismiss(dialog: DialogInterface) {
51 super.onDismiss(dialog)
52 messageDialogViewModel.dismissAction.invoke()
53 messageDialogViewModel.clear()
54 } 55 }
55 56
56 private fun openLink(link: String) { 57 private fun openLink(link: String) {
@@ -74,7 +75,7 @@ class MessageDialogFragment : DialogFragment() {
74 descriptionId: Int = 0, 75 descriptionId: Int = 0,
75 descriptionString: String = "", 76 descriptionString: String = "",
76 helpLinkId: Int = 0, 77 helpLinkId: Int = 0,
77 dismissAction: () -> Unit = {} 78 positiveAction: (() -> Unit)? = null
78 ): MessageDialogFragment { 79 ): MessageDialogFragment {
79 val dialog = MessageDialogFragment() 80 val dialog = MessageDialogFragment()
80 val bundle = Bundle() 81 val bundle = Bundle()
@@ -85,8 +86,10 @@ class MessageDialogFragment : DialogFragment() {
85 putString(DESCRIPTION_STRING, descriptionString) 86 putString(DESCRIPTION_STRING, descriptionString)
86 putInt(HELP_LINK, helpLinkId) 87 putInt(HELP_LINK, helpLinkId)
87 } 88 }
88 ViewModelProvider(activity)[MessageDialogViewModel::class.java].dismissAction = 89 ViewModelProvider(activity)[MessageDialogViewModel::class.java].apply {
89 dismissAction 90 clear()
91 this.positiveAction = positiveAction
92 }
90 dialog.arguments = bundle 93 dialog.arguments = bundle
91 return dialog 94 return dialog
92 } 95 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
index 2dbca76a5..64b295fbd 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SearchFragment.kt
@@ -24,6 +24,7 @@ import androidx.lifecycle.repeatOnLifecycle
24import androidx.preference.PreferenceManager 24import androidx.preference.PreferenceManager
25import info.debatty.java.stringsimilarity.Jaccard 25import info.debatty.java.stringsimilarity.Jaccard
26import info.debatty.java.stringsimilarity.JaroWinkler 26import info.debatty.java.stringsimilarity.JaroWinkler
27import kotlinx.coroutines.flow.collectLatest
27import kotlinx.coroutines.launch 28import kotlinx.coroutines.launch
28import java.util.Locale 29import java.util.Locale
29import org.yuzu.yuzu_emu.R 30import org.yuzu.yuzu_emu.R
@@ -60,7 +61,9 @@ class SearchFragment : Fragment() {
60 // This is using the correct scope, lint is just acting up 61 // This is using the correct scope, lint is just acting up
61 @SuppressLint("UnsafeRepeatOnLifecycleDetector") 62 @SuppressLint("UnsafeRepeatOnLifecycleDetector")
62 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 63 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
63 homeViewModel.setNavigationVisibility(visible = true, animated = false) 64 super.onViewCreated(view, savedInstanceState)
65 homeViewModel.setNavigationVisibility(visible = true, animated = true)
66 homeViewModel.setStatusBarShadeVisibility(true)
64 preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) 67 preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
65 68
66 if (savedInstanceState != null) { 69 if (savedInstanceState != null) {
@@ -99,7 +102,7 @@ class SearchFragment : Fragment() {
99 } 102 }
100 launch { 103 launch {
101 repeatOnLifecycle(Lifecycle.State.CREATED) { 104 repeatOnLifecycle(Lifecycle.State.CREATED) {
102 gamesViewModel.games.collect { filterAndSearch() } 105 gamesViewModel.games.collectLatest { filterAndSearch() }
103 } 106 }
104 } 107 }
105 launch { 108 launch {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt
index b88d2c038..60e029f34 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt
@@ -70,7 +70,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
70 sliderBinding = DialogSliderBinding.inflate(layoutInflater) 70 sliderBinding = DialogSliderBinding.inflate(layoutInflater)
71 val item = settingsViewModel.clickedItem as SliderSetting 71 val item = settingsViewModel.clickedItem as SliderSetting
72 72
73 settingsViewModel.setSliderTextValue(item.selectedValue.toFloat(), item.units) 73 settingsViewModel.setSliderTextValue(item.getSelectedValue().toFloat(), item.units)
74 sliderBinding.slider.apply { 74 sliderBinding.slider.apply {
75 valueFrom = item.min.toFloat() 75 valueFrom = item.min.toFloat()
76 valueTo = item.max.toFloat() 76 valueTo = item.max.toFloat()
@@ -136,18 +136,18 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
136 is SingleChoiceSetting -> { 136 is SingleChoiceSetting -> {
137 val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting 137 val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
138 val value = getValueForSingleChoiceSelection(scSetting, which) 138 val value = getValueForSingleChoiceSelection(scSetting, which)
139 scSetting.selectedValue = value 139 scSetting.setSelectedValue(value)
140 } 140 }
141 141
142 is StringSingleChoiceSetting -> { 142 is StringSingleChoiceSetting -> {
143 val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting 143 val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
144 val value = scSetting.getValueAt(which) 144 val value = scSetting.getValueAt(which)
145 scSetting.selectedValue = value 145 scSetting.setSelectedValue(value)
146 } 146 }
147 147
148 is SliderSetting -> { 148 is SliderSetting -> {
149 val sliderSetting = settingsViewModel.clickedItem as SliderSetting 149 val sliderSetting = settingsViewModel.clickedItem as SliderSetting
150 sliderSetting.selectedValue = settingsViewModel.sliderProgress.value 150 sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value)
151 } 151 }
152 } 152 }
153 closeDialog() 153 closeDialog()
@@ -171,7 +171,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
171 } 171 }
172 172
173 private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int { 173 private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
174 val value = item.selectedValue 174 val value = item.getSelectedValue()
175 val valuesId = item.valuesId 175 val valuesId = item.valuesId
176 if (valuesId > 0) { 176 if (valuesId > 0) {
177 val valuesArray = requireContext().resources.getIntArray(valuesId) 177 val valuesArray = requireContext().resources.getIntArray(valuesId)
@@ -211,7 +211,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
211 throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!") 211 throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!")
212 212
213 SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress( 213 SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress(
214 (clickedItem as SliderSetting).selectedValue.toFloat() 214 (clickedItem as SliderSetting).getSelectedValue().toFloat()
215 ) 215 )
216 } 216 }
217 settingsViewModel.clickedItem = clickedItem 217 settingsViewModel.clickedItem = clickedItem
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
index eb5edaa10..064342cdd 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt
@@ -304,6 +304,11 @@ class SetupFragment : Fragment() {
304 setInsets() 304 setInsets()
305 } 305 }
306 306
307 override fun onStop() {
308 super.onStop()
309 NativeConfig.saveGlobalConfig()
310 }
311
307 override fun onSaveInstanceState(outState: Bundle) { 312 override fun onSaveInstanceState(outState: Bundle) {
308 super.onSaveInstanceState(outState) 313 super.onSaveInstanceState(outState)
309 if (_binding != null) { 314 if (_binding != null) {
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
new file mode 100644
index 000000000..ed79a8b02
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt
@@ -0,0 +1,10 @@
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
new file mode 100644
index 000000000..075252f5b
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt
@@ -0,0 +1,83 @@
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.lifecycle.ViewModel
7import androidx.lifecycle.viewModelScope
8import kotlinx.coroutines.Dispatchers
9import kotlinx.coroutines.flow.MutableStateFlow
10import kotlinx.coroutines.flow.asStateFlow
11import kotlinx.coroutines.launch
12import kotlinx.coroutines.withContext
13import org.yuzu.yuzu_emu.NativeLibrary
14import org.yuzu.yuzu_emu.utils.NativeConfig
15import java.util.concurrent.atomic.AtomicBoolean
16
17class AddonViewModel : ViewModel() {
18 private val _addonList = MutableStateFlow(mutableListOf<Addon>())
19 val addonList get() = _addonList.asStateFlow()
20
21 private val _showModInstallPicker = MutableStateFlow(false)
22 val showModInstallPicker get() = _showModInstallPicker.asStateFlow()
23
24 private val _showModNoticeDialog = MutableStateFlow(false)
25 val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow()
26
27 var game: Game? = null
28
29 private val isRefreshing = AtomicBoolean(false)
30
31 fun onOpenAddons(game: Game) {
32 this.game = game
33 refreshAddons()
34 }
35
36 fun refreshAddons() {
37 if (isRefreshing.get() || game == null) {
38 return
39 }
40 isRefreshing.set(true)
41 viewModelScope.launch {
42 withContext(Dispatchers.IO) {
43 val addonList = mutableListOf<Addon>()
44 val disabledAddons = NativeConfig.getDisabledAddons(game!!.programId)
45 NativeLibrary.getAddonsForFile(game!!.path, game!!.programId)?.forEach {
46 val name = it.first.replace("[D] ", "")
47 addonList.add(Addon(!disabledAddons.contains(name), name, it.second))
48 }
49 addonList.sortBy { it.title }
50 _addonList.value = addonList
51 isRefreshing.set(false)
52 }
53 }
54 }
55
56 fun onCloseAddons() {
57 if (_addonList.value.isEmpty()) {
58 return
59 }
60
61 NativeConfig.setDisabledAddons(
62 game!!.programId,
63 _addonList.value.mapNotNull {
64 if (it.enabled) {
65 null
66 } else {
67 it.title
68 }
69 }.toTypedArray()
70 )
71 NativeConfig.saveGlobalConfig()
72 _addonList.value.clear()
73 game = null
74 }
75
76 fun showModInstallPicker(install: Boolean) {
77 _showModInstallPicker.value = install
78 }
79
80 fun showModNoticeDialog(show: Boolean) {
81 _showModNoticeDialog.value = show
82 }
83}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt
index 62945ad65..76accf8f3 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/DriverViewModel.kt
@@ -7,81 +7,83 @@ import androidx.lifecycle.ViewModel
7import androidx.lifecycle.viewModelScope 7import 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.SharingStarted
10import kotlinx.coroutines.flow.StateFlow 11import kotlinx.coroutines.flow.StateFlow
12import kotlinx.coroutines.flow.combine
13import kotlinx.coroutines.flow.stateIn
11import kotlinx.coroutines.launch 14import kotlinx.coroutines.launch
12import kotlinx.coroutines.withContext 15import kotlinx.coroutines.withContext
13import org.yuzu.yuzu_emu.R 16import org.yuzu.yuzu_emu.R
14import org.yuzu.yuzu_emu.YuzuApplication 17import org.yuzu.yuzu_emu.YuzuApplication
18import org.yuzu.yuzu_emu.features.settings.model.StringSetting
19import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
15import org.yuzu.yuzu_emu.utils.FileUtil 20import org.yuzu.yuzu_emu.utils.FileUtil
16import org.yuzu.yuzu_emu.utils.GpuDriverHelper 21import org.yuzu.yuzu_emu.utils.GpuDriverHelper
17import org.yuzu.yuzu_emu.utils.GpuDriverMetadata 22import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
23import org.yuzu.yuzu_emu.utils.NativeConfig
18import java.io.BufferedOutputStream 24import java.io.BufferedOutputStream
19import java.io.File 25import java.io.File
20 26
21class DriverViewModel : ViewModel() { 27class DriverViewModel : ViewModel() {
22 private val _areDriversLoading = MutableStateFlow(false) 28 private val _areDriversLoading = MutableStateFlow(false)
23 val areDriversLoading: StateFlow<Boolean> get() = _areDriversLoading
24
25 private val _isDriverReady = MutableStateFlow(true) 29 private val _isDriverReady = MutableStateFlow(true)
26 val isDriverReady: StateFlow<Boolean> get() = _isDriverReady
27
28 private val _isDeletingDrivers = MutableStateFlow(false) 30 private val _isDeletingDrivers = MutableStateFlow(false)
29 val isDeletingDrivers: StateFlow<Boolean> get() = _isDeletingDrivers
30 31
31 private val _driverList = MutableStateFlow(mutableListOf<Pair<String, GpuDriverMetadata>>()) 32 val isInteractionAllowed: StateFlow<Boolean> =
33 combine(
34 _areDriversLoading,
35 _isDriverReady,
36 _isDeletingDrivers
37 ) { loading, ready, deleting ->
38 !loading && ready && !deleting
39 }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = false)
40
41 private val _driverList = MutableStateFlow(GpuDriverHelper.getDrivers())
32 val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList 42 val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList
33 43
34 var previouslySelectedDriver = 0 44 var previouslySelectedDriver = 0
35 var selectedDriver = -1 45 var selectedDriver = -1
36 46
37 private val _selectedDriverMetadata = 47 // Used for showing which driver is currently installed within the driver manager card
38 MutableStateFlow( 48 private val _selectedDriverTitle = MutableStateFlow("")
39 GpuDriverHelper.customDriverData.name 49 val selectedDriverTitle: StateFlow<String> get() = _selectedDriverTitle
40 ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
41 )
42 val selectedDriverMetadata: StateFlow<String> get() = _selectedDriverMetadata
43 50
44 private val _newDriverInstalled = MutableStateFlow(false) 51 private val _newDriverInstalled = MutableStateFlow(false)
45 val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled 52 val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled
46 53
47 val driversToDelete = mutableListOf<String>() 54 val driversToDelete = mutableListOf<String>()
48 55
49 val isInteractionAllowed
50 get() = !areDriversLoading.value && isDriverReady.value && !isDeletingDrivers.value
51
52 init { 56 init {
53 _areDriversLoading.value = true 57 val currentDriverMetadata = GpuDriverHelper.installedCustomDriverData
54 viewModelScope.launch { 58 findSelectedDriver(currentDriverMetadata)
55 withContext(Dispatchers.IO) { 59
56 val drivers = GpuDriverHelper.getDrivers() 60 // If a user had installed a driver before the manager was implemented, this zips
57 val currentDriverMetadata = GpuDriverHelper.customDriverData 61 // the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can
58 for (i in drivers.indices) { 62 // be indexed and exported as expected.
59 if (drivers[i].second == currentDriverMetadata) { 63 if (selectedDriver == -1) {
60 setSelectedDriverIndex(i) 64 val driverToSave =
61 break 65 File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip")
62 } 66 driverToSave.createNewFile()
63 } 67 FileUtil.zipFromInternalStorage(
64 68 File(GpuDriverHelper.driverInstallationPath!!),
65 // If a user had installed a driver before the manager was implemented, this zips 69 GpuDriverHelper.driverInstallationPath!!,
66 // the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can 70 BufferedOutputStream(driverToSave.outputStream())
67 // be indexed and exported as expected. 71 )
68 if (selectedDriver == -1) { 72 _driverList.value.add(Pair(driverToSave.path, currentDriverMetadata))
69 val driverToSave = 73 setSelectedDriverIndex(_driverList.value.size - 1)
70 File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip") 74 }
71 driverToSave.createNewFile()
72 FileUtil.zipFromInternalStorage(
73 File(GpuDriverHelper.driverInstallationPath!!),
74 GpuDriverHelper.driverInstallationPath!!,
75 BufferedOutputStream(driverToSave.outputStream())
76 )
77 drivers.add(Pair(driverToSave.path, currentDriverMetadata))
78 setSelectedDriverIndex(drivers.size - 1)
79 }
80 75
81 _driverList.value = drivers 76 // If a user had installed a driver before the config was reworked to be multiplatform,
82 _areDriversLoading.value = false 77 // we have save the path of the previously selected driver to the new setting.
83 } 78 if (StringSetting.DRIVER_PATH.getString(true).isEmpty() && selectedDriver > 0 &&
79 StringSetting.DRIVER_PATH.global
80 ) {
81 StringSetting.DRIVER_PATH.setString(_driverList.value[selectedDriver].first)
82 NativeConfig.saveGlobalConfig()
83 } else {
84 findSelectedDriver(GpuDriverHelper.customDriverSettingData)
84 } 85 }
86 updateDriverNameForGame(null)
85 } 87 }
86 88
87 fun setSelectedDriverIndex(value: Int) { 89 fun setSelectedDriverIndex(value: Int) {
@@ -98,9 +100,9 @@ class DriverViewModel : ViewModel() {
98 fun addDriver(driverData: Pair<String, GpuDriverMetadata>) { 100 fun addDriver(driverData: Pair<String, GpuDriverMetadata>) {
99 val driverIndex = _driverList.value.indexOfFirst { it == driverData } 101 val driverIndex = _driverList.value.indexOfFirst { it == driverData }
100 if (driverIndex == -1) { 102 if (driverIndex == -1) {
101 setSelectedDriverIndex(_driverList.value.size)
102 _driverList.value.add(driverData) 103 _driverList.value.add(driverData)
103 _selectedDriverMetadata.value = driverData.second.name 104 setSelectedDriverIndex(_driverList.value.size - 1)
105 _selectedDriverTitle.value = driverData.second.name
104 ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) 106 ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
105 } else { 107 } else {
106 setSelectedDriverIndex(driverIndex) 108 setSelectedDriverIndex(driverIndex)
@@ -111,8 +113,31 @@ class DriverViewModel : ViewModel() {
111 _driverList.value.remove(driverData) 113 _driverList.value.remove(driverData)
112 } 114 }
113 115
114 fun onCloseDriverManager() { 116 fun onOpenDriverManager(game: Game?) {
117 if (game != null) {
118 SettingsFile.loadCustomConfig(game)
119 }
120
121 val driverPath = StringSetting.DRIVER_PATH.getString()
122 if (driverPath.isEmpty()) {
123 setSelectedDriverIndex(0)
124 } else {
125 findSelectedDriver(GpuDriverHelper.getMetadataFromZip(File(driverPath)))
126 }
127 }
128
129 fun onCloseDriverManager(game: Game?) {
115 _isDeletingDrivers.value = true 130 _isDeletingDrivers.value = true
131 StringSetting.DRIVER_PATH.setString(driverList.value[selectedDriver].first)
132 updateDriverNameForGame(game)
133 if (game == null) {
134 NativeConfig.saveGlobalConfig()
135 } else {
136 NativeConfig.savePerGameConfig()
137 NativeConfig.unloadPerGameConfig()
138 NativeConfig.reloadGlobalConfig()
139 }
140
116 viewModelScope.launch { 141 viewModelScope.launch {
117 withContext(Dispatchers.IO) { 142 withContext(Dispatchers.IO) {
118 driversToDelete.forEach { 143 driversToDelete.forEach {
@@ -125,23 +150,29 @@ class DriverViewModel : ViewModel() {
125 _isDeletingDrivers.value = false 150 _isDeletingDrivers.value = false
126 } 151 }
127 } 152 }
153 }
154
155 // It is the Emulation Fragment's responsibility to load per-game settings so that this function
156 // knows what driver to load.
157 fun onLaunchGame() {
158 _isDriverReady.value = false
128 159
129 if (GpuDriverHelper.customDriverData == driverList.value[selectedDriver].second) { 160 val selectedDriverFile = File(StringSetting.DRIVER_PATH.getString())
161 val selectedDriverMetadata = GpuDriverHelper.customDriverSettingData
162 if (GpuDriverHelper.installedCustomDriverData == selectedDriverMetadata) {
130 return 163 return
131 } 164 }
132 165
133 _isDriverReady.value = false
134 viewModelScope.launch { 166 viewModelScope.launch {
135 withContext(Dispatchers.IO) { 167 withContext(Dispatchers.IO) {
136 if (selectedDriver == 0) { 168 if (selectedDriverMetadata.name == null) {
137 GpuDriverHelper.installDefaultDriver() 169 GpuDriverHelper.installDefaultDriver()
138 setDriverReady() 170 setDriverReady()
139 return@withContext 171 return@withContext
140 } 172 }
141 173
142 val driverToInstall = File(driverList.value[selectedDriver].first) 174 if (selectedDriverFile.exists()) {
143 if (driverToInstall.exists()) { 175 GpuDriverHelper.installCustomDriver(selectedDriverFile)
144 GpuDriverHelper.installCustomDriver(driverToInstall)
145 } else { 176 } else {
146 GpuDriverHelper.installDefaultDriver() 177 GpuDriverHelper.installDefaultDriver()
147 } 178 }
@@ -150,9 +181,43 @@ class DriverViewModel : ViewModel() {
150 } 181 }
151 } 182 }
152 183
184 private fun findSelectedDriver(currentDriverMetadata: GpuDriverMetadata) {
185 if (driverList.value.size == 1) {
186 setSelectedDriverIndex(0)
187 return
188 }
189
190 driverList.value.forEachIndexed { i: Int, driver: Pair<String, GpuDriverMetadata> ->
191 if (driver.second == currentDriverMetadata) {
192 setSelectedDriverIndex(i)
193 return
194 }
195 }
196 }
197
198 fun updateDriverNameForGame(game: Game?) {
199 if (!GpuDriverHelper.supportsCustomDriverLoading()) {
200 return
201 }
202
203 if (game == null || NativeConfig.isPerGameConfigLoaded()) {
204 updateName()
205 } else {
206 SettingsFile.loadCustomConfig(game)
207 updateName()
208 NativeConfig.unloadPerGameConfig()
209 NativeConfig.reloadGlobalConfig()
210 }
211 }
212
213 private fun updateName() {
214 _selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name
215 ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
216 }
217
153 private fun setDriverReady() { 218 private fun setDriverReady() {
154 _isDriverReady.value = true 219 _isDriverReady.value = true
155 _selectedDriverMetadata.value = GpuDriverHelper.customDriverData.name 220 _selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name
156 ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver) 221 ?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
157 } 222 }
158} 223}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
index 2fa3ab31b..f1ea1e20f 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Game.kt
@@ -3,10 +3,18 @@
3 3
4package org.yuzu.yuzu_emu.model 4package org.yuzu.yuzu_emu.model
5 5
6import android.net.Uri
6import android.os.Parcelable 7import android.os.Parcelable
7import java.util.HashSet 8import java.util.HashSet
8import kotlinx.parcelize.Parcelize 9import kotlinx.parcelize.Parcelize
9import kotlinx.serialization.Serializable 10import kotlinx.serialization.Serializable
11import org.yuzu.yuzu_emu.NativeLibrary
12import org.yuzu.yuzu_emu.R
13import org.yuzu.yuzu_emu.YuzuApplication
14import org.yuzu.yuzu_emu.utils.DirectoryInitialization
15import org.yuzu.yuzu_emu.utils.FileUtil
16import java.time.LocalDateTime
17import java.time.format.DateTimeFormatter
10 18
11@Parcelize 19@Parcelize
12@Serializable 20@Serializable
@@ -15,12 +23,44 @@ class Game(
15 val path: String, 23 val path: String,
16 val programId: String = "", 24 val programId: String = "",
17 val developer: String = "", 25 val developer: String = "",
18 val version: String = "", 26 var version: String = "",
19 val isHomebrew: Boolean = false 27 val isHomebrew: Boolean = false
20) : Parcelable { 28) : Parcelable {
21 val keyAddedToLibraryTime get() = "${path}_AddedToLibraryTime" 29 val keyAddedToLibraryTime get() = "${path}_AddedToLibraryTime"
22 val keyLastPlayedTime get() = "${path}_LastPlayed" 30 val keyLastPlayedTime get() = "${path}_LastPlayed"
23 31
32 val settingsName: String
33 get() {
34 val programIdLong = programId.toLong()
35 return if (programIdLong == 0L) {
36 FileUtil.getFilename(Uri.parse(path))
37 } else {
38 "0" + programIdLong.toString(16).uppercase()
39 }
40 }
41
42 val programIdHex: String
43 get() {
44 val programIdLong = programId.toLong()
45 return if (programIdLong == 0L) {
46 "0"
47 } else {
48 "0" + programIdLong.toString(16).uppercase()
49 }
50 }
51
52 val saveZipName: String
53 get() = "$title ${YuzuApplication.appContext.getString(R.string.save_data).lowercase()} - ${
54 LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
55 }.zip"
56
57 val saveDir: String
58 get() = DirectoryInitialization.userDirectory + "/nand" +
59 NativeLibrary.getSavePath(programId)
60
61 val addonDir: String
62 get() = DirectoryInitialization.userDirectory + "/load/" + programIdHex + "/"
63
24 override fun equals(other: Any?): Boolean { 64 override fun equals(other: Any?): Boolean {
25 if (other !is Game) { 65 if (other !is Game) {
26 return false 66 return false
@@ -34,6 +74,7 @@ class Game(
34 result = 31 * result + path.hashCode() 74 result = 31 * result + path.hashCode()
35 result = 31 * result + programId.hashCode() 75 result = 31 * result + programId.hashCode()
36 result = 31 * result + developer.hashCode() 76 result = 31 * result + developer.hashCode()
77 result = 31 * result + version.hashCode()
37 result = 31 * result + isHomebrew.hashCode() 78 result = 31 * result + isHomebrew.hashCode()
38 return result 79 return result
39 } 80 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt
new file mode 100644
index 000000000..0135a95be
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt
@@ -0,0 +1,36 @@
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.DrawableRes
7import androidx.annotation.StringRes
8import kotlinx.coroutines.flow.StateFlow
9
10interface GameProperty {
11 @get:StringRes
12 val titleId: Int
13
14 @get:StringRes
15 val descriptionId: Int
16
17 @get:DrawableRes
18 val iconId: Int
19}
20
21data class SubmenuProperty(
22 override val titleId: Int,
23 override val descriptionId: Int,
24 override val iconId: Int,
25 val details: (() -> String)? = null,
26 val detailsFlow: StateFlow<String>? = null,
27 val action: () -> Unit
28) : GameProperty
29
30data class InstallableProperty(
31 override val titleId: Int,
32 override val descriptionId: Int,
33 override val iconId: Int,
34 val install: (() -> Unit)? = null,
35 val export: (() -> Unit)? = null
36) : GameProperty
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
index fd925235b..d19f20dc2 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt
@@ -20,8 +20,8 @@ import kotlinx.serialization.json.Json
20import org.yuzu.yuzu_emu.NativeLibrary 20import org.yuzu.yuzu_emu.NativeLibrary
21import org.yuzu.yuzu_emu.YuzuApplication 21import org.yuzu.yuzu_emu.YuzuApplication
22import org.yuzu.yuzu_emu.utils.GameHelper 22import org.yuzu.yuzu_emu.utils.GameHelper
23import org.yuzu.yuzu_emu.utils.GameMetadata
24import org.yuzu.yuzu_emu.utils.NativeConfig 23import org.yuzu.yuzu_emu.utils.NativeConfig
24import java.util.concurrent.atomic.AtomicBoolean
25 25
26class GamesViewModel : ViewModel() { 26class GamesViewModel : ViewModel() {
27 val games: StateFlow<List<Game>> get() = _games 27 val games: StateFlow<List<Game>> get() = _games
@@ -33,6 +33,8 @@ class GamesViewModel : ViewModel() {
33 val isReloading: StateFlow<Boolean> get() = _isReloading 33 val isReloading: StateFlow<Boolean> get() = _isReloading
34 private val _isReloading = MutableStateFlow(false) 34 private val _isReloading = MutableStateFlow(false)
35 35
36 private val reloading = AtomicBoolean(false)
37
36 val shouldSwapData: StateFlow<Boolean> get() = _shouldSwapData 38 val shouldSwapData: StateFlow<Boolean> get() = _shouldSwapData
37 private val _shouldSwapData = MutableStateFlow(false) 39 private val _shouldSwapData = MutableStateFlow(false)
38 40
@@ -49,38 +51,8 @@ class GamesViewModel : ViewModel() {
49 // Ensure keys are loaded so that ROM metadata can be decrypted. 51 // Ensure keys are loaded so that ROM metadata can be decrypted.
50 NativeLibrary.reloadKeys() 52 NativeLibrary.reloadKeys()
51 53
52 // Retrieve list of cached games 54 getGameDirs()
53 val storedGames = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) 55 reloadGames(directoriesChanged = false, firstStartup = true)
54 .getStringSet(GameHelper.KEY_GAMES, emptySet())
55
56 viewModelScope.launch {
57 withContext(Dispatchers.IO) {
58 getGameDirs()
59 if (storedGames!!.isNotEmpty()) {
60 val deserializedGames = mutableSetOf<Game>()
61 storedGames.forEach {
62 val game: Game
63 try {
64 game = Json.decodeFromString(it)
65 } catch (e: Exception) {
66 // We don't care about any errors related to parsing the game cache
67 return@forEach
68 }
69
70 val gameExists =
71 DocumentFile.fromSingleUri(
72 YuzuApplication.appContext,
73 Uri.parse(game.path)
74 )?.exists()
75 if (gameExists == true) {
76 deserializedGames.add(game)
77 }
78 }
79 setGames(deserializedGames.toList())
80 }
81 reloadGames(false)
82 }
83 }
84 } 56 }
85 57
86 fun setGames(games: List<Game>) { 58 fun setGames(games: List<Game>) {
@@ -110,16 +82,46 @@ class GamesViewModel : ViewModel() {
110 _searchFocused.value = searchFocused 82 _searchFocused.value = searchFocused
111 } 83 }
112 84
113 fun reloadGames(directoriesChanged: Boolean) { 85 fun reloadGames(directoriesChanged: Boolean, firstStartup: Boolean = false) {
114 if (isReloading.value) { 86 if (reloading.get()) {
115 return 87 return
116 } 88 }
89 reloading.set(true)
117 _isReloading.value = true 90 _isReloading.value = true
118 91
119 viewModelScope.launch { 92 viewModelScope.launch {
120 withContext(Dispatchers.IO) { 93 withContext(Dispatchers.IO) {
121 GameMetadata.resetMetadata() 94 if (firstStartup) {
95 // Retrieve list of cached games
96 val storedGames =
97 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
98 .getStringSet(GameHelper.KEY_GAMES, emptySet())
99 if (storedGames!!.isNotEmpty()) {
100 val deserializedGames = mutableSetOf<Game>()
101 storedGames.forEach {
102 val game: Game
103 try {
104 game = Json.decodeFromString(it)
105 } catch (e: Exception) {
106 // We don't care about any errors related to parsing the game cache
107 return@forEach
108 }
109
110 val gameExists =
111 DocumentFile.fromSingleUri(
112 YuzuApplication.appContext,
113 Uri.parse(game.path)
114 )?.exists()
115 if (gameExists == true) {
116 deserializedGames.add(game)
117 }
118 }
119 setGames(deserializedGames.toList())
120 }
121 }
122
122 setGames(GameHelper.getGames()) 123 setGames(GameHelper.getGames())
124 reloading.set(false)
123 _isReloading.value = false 125 _isReloading.value = false
124 126
125 if (directoriesChanged) { 127 if (directoriesChanged) {
@@ -168,6 +170,7 @@ class GamesViewModel : ViewModel() {
168 fun onCloseGameFoldersFragment() = 170 fun onCloseGameFoldersFragment() =
169 viewModelScope.launch { 171 viewModelScope.launch {
170 withContext(Dispatchers.IO) { 172 withContext(Dispatchers.IO) {
173 NativeConfig.saveGlobalConfig()
171 getGameDirs(true) 174 getGameDirs(true)
172 } 175 }
173 } 176 }
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
index 07e65b028..513ac2fc5 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeViewModel.kt
@@ -3,6 +3,7 @@
3 3
4package org.yuzu.yuzu_emu.model 4package org.yuzu.yuzu_emu.model
5 5
6import android.net.Uri
6import androidx.lifecycle.ViewModel 7import androidx.lifecycle.ViewModel
7import kotlinx.coroutines.flow.MutableStateFlow 8import kotlinx.coroutines.flow.MutableStateFlow
8import kotlinx.coroutines.flow.StateFlow 9import kotlinx.coroutines.flow.StateFlow
@@ -21,6 +22,15 @@ class HomeViewModel : ViewModel() {
21 private val _gamesDirSelected = MutableStateFlow(false) 22 private val _gamesDirSelected = MutableStateFlow(false)
22 val gamesDirSelected get() = _gamesDirSelected.asStateFlow() 23 val gamesDirSelected get() = _gamesDirSelected.asStateFlow()
23 24
25 private val _openImportSaves = MutableStateFlow(false)
26 val openImportSaves get() = _openImportSaves.asStateFlow()
27
28 private val _contentToInstall = MutableStateFlow<List<Uri>?>(null)
29 val contentToInstall get() = _contentToInstall.asStateFlow()
30
31 private val _reloadPropertiesList = MutableStateFlow(false)
32 val reloadPropertiesList get() = _reloadPropertiesList.asStateFlow()
33
24 var navigatedToSetup = false 34 var navigatedToSetup = false
25 35
26 fun setNavigationVisibility(visible: Boolean, animated: Boolean) { 36 fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
@@ -44,4 +54,16 @@ class HomeViewModel : ViewModel() {
44 fun setGamesDirSelected(selected: Boolean) { 54 fun setGamesDirSelected(selected: Boolean) {
45 _gamesDirSelected.value = selected 55 _gamesDirSelected.value = selected
46 } 56 }
57
58 fun setOpenImportSaves(import: Boolean) {
59 _openImportSaves.value = import
60 }
61
62 fun setContentToInstall(documents: List<Uri>?) {
63 _contentToInstall.value = documents
64 }
65
66 fun reloadPropertiesList(reload: Boolean) {
67 _reloadPropertiesList.value = reload
68 }
47} 69}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt
index 36ffd08d2..641c5cb17 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt
@@ -6,9 +6,9 @@ package org.yuzu.yuzu_emu.model
6import androidx.lifecycle.ViewModel 6import androidx.lifecycle.ViewModel
7 7
8class MessageDialogViewModel : ViewModel() { 8class MessageDialogViewModel : ViewModel() {
9 var dismissAction: () -> Unit = {} 9 var positiveAction: (() -> Unit)? = null
10 10
11 fun clear() { 11 fun clear() {
12 dismissAction = {} 12 positiveAction = null
13 } 13 }
14} 14}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt
index ccc981e95..5cb6a5d57 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt
@@ -68,8 +68,4 @@ class SettingsViewModel : ViewModel() {
68 fun setAdapterItemChanged(value: Int) { 68 fun setAdapterItemChanged(value: Int) {
69 _adapterItemChanged.value = value 69 _adapterItemChanged.value = value
70 } 70 }
71
72 fun clear() {
73 game = null
74 }
75} 71}
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 16a794dee..e59c95733 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
@@ -23,7 +23,7 @@ class TaskViewModel : ViewModel() {
23 val cancelled: StateFlow<Boolean> get() = _cancelled 23 val cancelled: StateFlow<Boolean> get() = _cancelled
24 private val _cancelled = MutableStateFlow(false) 24 private val _cancelled = MutableStateFlow(false)
25 25
26 lateinit var task: () -> Any 26 lateinit var task: suspend () -> Any
27 27
28 fun clear() { 28 fun clear() {
29 _result.value = Any() 29 _result.value = Any()
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
index 805b89b31..fc0eeb9ad 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/GamesFragment.kt
@@ -19,7 +19,7 @@ import androidx.lifecycle.Lifecycle
19import androidx.lifecycle.lifecycleScope 19import androidx.lifecycle.lifecycleScope
20import androidx.lifecycle.repeatOnLifecycle 20import androidx.lifecycle.repeatOnLifecycle
21import com.google.android.material.color.MaterialColors 21import com.google.android.material.color.MaterialColors
22import com.google.android.material.transition.MaterialFadeThrough 22import kotlinx.coroutines.flow.collectLatest
23import kotlinx.coroutines.launch 23import kotlinx.coroutines.launch
24import org.yuzu.yuzu_emu.R 24import org.yuzu.yuzu_emu.R
25import org.yuzu.yuzu_emu.adapters.GameAdapter 25import org.yuzu.yuzu_emu.adapters.GameAdapter
@@ -35,11 +35,6 @@ class GamesFragment : Fragment() {
35 private val gamesViewModel: GamesViewModel by activityViewModels() 35 private val gamesViewModel: GamesViewModel by activityViewModels()
36 private val homeViewModel: HomeViewModel by activityViewModels() 36 private val homeViewModel: HomeViewModel by activityViewModels()
37 37
38 override fun onCreate(savedInstanceState: Bundle?) {
39 super.onCreate(savedInstanceState)
40 enterTransition = MaterialFadeThrough()
41 }
42
43 override fun onCreateView( 38 override fun onCreateView(
44 inflater: LayoutInflater, 39 inflater: LayoutInflater,
45 container: ViewGroup?, 40 container: ViewGroup?,
@@ -52,7 +47,9 @@ class GamesFragment : Fragment() {
52 // This is using the correct scope, lint is just acting up 47 // This is using the correct scope, lint is just acting up
53 @SuppressLint("UnsafeRepeatOnLifecycleDetector") 48 @SuppressLint("UnsafeRepeatOnLifecycleDetector")
54 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 49 override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
55 homeViewModel.setNavigationVisibility(visible = true, animated = false) 50 super.onViewCreated(view, savedInstanceState)
51 homeViewModel.setNavigationVisibility(visible = true, animated = true)
52 homeViewModel.setStatusBarShadeVisibility(true)
56 53
57 binding.gridGames.apply { 54 binding.gridGames.apply {
58 layoutManager = AutofitGridLayoutManager( 55 layoutManager = AutofitGridLayoutManager(
@@ -99,7 +96,7 @@ class GamesFragment : Fragment() {
99 } 96 }
100 launch { 97 launch {
101 repeatOnLifecycle(Lifecycle.State.RESUMED) { 98 repeatOnLifecycle(Lifecycle.State.RESUMED) {
102 gamesViewModel.games.collect { 99 gamesViewModel.games.collectLatest {
103 (binding.gridGames.adapter as GameAdapter).submitList(it) 100 (binding.gridGames.adapter as GameAdapter).submitList(it)
104 if (it.isEmpty()) { 101 if (it.isEmpty()) {
105 binding.noticeText.visibility = View.VISIBLE 102 binding.noticeText.visibility = View.VISIBLE
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 16323a316..b4117d761 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
@@ -28,12 +28,9 @@ import androidx.navigation.ui.setupWithNavController
28import androidx.preference.PreferenceManager 28import androidx.preference.PreferenceManager
29import com.google.android.material.color.MaterialColors 29import com.google.android.material.color.MaterialColors
30import com.google.android.material.navigation.NavigationBarView 30import com.google.android.material.navigation.NavigationBarView
31import kotlinx.coroutines.CoroutineScope
32import java.io.File 31import java.io.File
33import java.io.FilenameFilter 32import java.io.FilenameFilter
34import kotlinx.coroutines.Dispatchers
35import kotlinx.coroutines.launch 33import kotlinx.coroutines.launch
36import kotlinx.coroutines.withContext
37import org.yuzu.yuzu_emu.HomeNavigationDirections 34import org.yuzu.yuzu_emu.HomeNavigationDirections
38import org.yuzu.yuzu_emu.NativeLibrary 35import org.yuzu.yuzu_emu.NativeLibrary
39import org.yuzu.yuzu_emu.R 36import org.yuzu.yuzu_emu.R
@@ -43,7 +40,7 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings
43import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment 40import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
44import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment 41import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
45import org.yuzu.yuzu_emu.fragments.MessageDialogFragment 42import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
46import org.yuzu.yuzu_emu.getPublicFilesDir 43import org.yuzu.yuzu_emu.model.AddonViewModel
47import org.yuzu.yuzu_emu.model.GamesViewModel 44import org.yuzu.yuzu_emu.model.GamesViewModel
48import org.yuzu.yuzu_emu.model.HomeViewModel 45import org.yuzu.yuzu_emu.model.HomeViewModel
49import org.yuzu.yuzu_emu.model.TaskState 46import org.yuzu.yuzu_emu.model.TaskState
@@ -60,15 +57,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
60 private val homeViewModel: HomeViewModel by viewModels() 57 private val homeViewModel: HomeViewModel by viewModels()
61 private val gamesViewModel: GamesViewModel by viewModels() 58 private val gamesViewModel: GamesViewModel by viewModels()
62 private val taskViewModel: TaskViewModel by viewModels() 59 private val taskViewModel: TaskViewModel by viewModels()
60 private val addonViewModel: AddonViewModel by viewModels()
63 61
64 override var themeId: Int = 0 62 override var themeId: Int = 0
65 63
66 private val savesFolder
67 get() = "${getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000"
68
69 // Get first subfolder in saves folder (should be the user folder)
70 val savesFolderRoot get() = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: ""
71
72 override fun onCreate(savedInstanceState: Bundle?) { 64 override fun onCreate(savedInstanceState: Bundle?) {
73 val splashScreen = installSplashScreen() 65 val splashScreen = installSplashScreen()
74 splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } 66 splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
@@ -145,6 +137,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
145 homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) } 137 homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) }
146 } 138 }
147 } 139 }
140 launch {
141 repeatOnLifecycle(Lifecycle.State.CREATED) {
142 homeViewModel.contentToInstall.collect {
143 if (it != null) {
144 installContent(it)
145 homeViewModel.setContentToInstall(null)
146 }
147 }
148 }
149 }
148 } 150 }
149 151
150 // Dismiss previous notifications (should not happen unless a crash occurred) 152 // Dismiss previous notifications (should not happen unless a crash occurred)
@@ -253,13 +255,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
253 super.onResume() 255 super.onResume()
254 } 256 }
255 257
256 override fun onStop() {
257 super.onStop()
258 CoroutineScope(Dispatchers.IO).launch {
259 NativeConfig.saveSettings()
260 }
261 }
262
263 override fun onDestroy() { 258 override fun onDestroy() {
264 EmulationActivity.stopForegroundService(this) 259 EmulationActivity.stopForegroundService(this)
265 super.onDestroy() 260 super.onDestroy()
@@ -468,110 +463,150 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
468 val installGameUpdate = registerForActivityResult( 463 val installGameUpdate = registerForActivityResult(
469 ActivityResultContracts.OpenMultipleDocuments() 464 ActivityResultContracts.OpenMultipleDocuments()
470 ) { documents: List<Uri> -> 465 ) { documents: List<Uri> ->
471 if (documents.isNotEmpty()) { 466 if (documents.isEmpty()) {
472 IndeterminateProgressDialogFragment.newInstance( 467 return@registerForActivityResult
473 this@MainActivity, 468 }
474 R.string.installing_game_content
475 ) {
476 var installSuccess = 0
477 var installOverwrite = 0
478 var errorBaseGame = 0
479 var errorExtension = 0
480 var errorOther = 0
481 documents.forEach {
482 when (
483 NativeLibrary.installFileToNand(
484 it.toString(),
485 FileUtil.getExtension(it)
486 )
487 ) {
488 NativeLibrary.InstallFileToNandResult.Success -> {
489 installSuccess += 1
490 }
491 469
492 NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> { 470 if (addonViewModel.game == null) {
493 installOverwrite += 1 471 installContent(documents)
494 } 472 return@registerForActivityResult
473 }
495 474
496 NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> { 475 IndeterminateProgressDialogFragment.newInstance(
497 errorBaseGame += 1 476 this@MainActivity,
498 } 477 R.string.verifying_content,
478 false
479 ) {
480 var updatesMatchProgram = true
481 for (document in documents) {
482 val valid = NativeLibrary.doesUpdateMatchProgram(
483 addonViewModel.game!!.programId,
484 document.toString()
485 )
486 if (!valid) {
487 updatesMatchProgram = false
488 break
489 }
490 }
499 491
500 NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> { 492 if (updatesMatchProgram) {
501 errorExtension += 1 493 homeViewModel.setContentToInstall(documents)
502 } 494 } else {
495 MessageDialogFragment.newInstance(
496 this@MainActivity,
497 titleId = R.string.content_install_notice,
498 descriptionId = R.string.content_install_notice_description,
499 positiveAction = { homeViewModel.setContentToInstall(documents) }
500 )
501 }
502 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
503 }
503 504
504 else -> { 505 private fun installContent(documents: List<Uri>) {
505 errorOther += 1 506 IndeterminateProgressDialogFragment.newInstance(
506 } 507 this@MainActivity,
508 R.string.installing_game_content
509 ) {
510 var installSuccess = 0
511 var installOverwrite = 0
512 var errorBaseGame = 0
513 var errorExtension = 0
514 var errorOther = 0
515 documents.forEach {
516 when (
517 NativeLibrary.installFileToNand(
518 it.toString(),
519 FileUtil.getExtension(it)
520 )
521 ) {
522 NativeLibrary.InstallFileToNandResult.Success -> {
523 installSuccess += 1
524 }
525
526 NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> {
527 installOverwrite += 1
528 }
529
530 NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> {
531 errorBaseGame += 1
532 }
533
534 NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> {
535 errorExtension += 1
536 }
537
538 else -> {
539 errorOther += 1
507 } 540 }
508 } 541 }
542 }
509 543
510 val separator = System.getProperty("line.separator") ?: "\n" 544 addonViewModel.refreshAddons()
511 val installResult = StringBuilder() 545
512 if (installSuccess > 0) { 546 val separator = System.getProperty("line.separator") ?: "\n"
513 installResult.append( 547 val installResult = StringBuilder()
514 getString( 548 if (installSuccess > 0) {
515 R.string.install_game_content_success_install, 549 installResult.append(
516 installSuccess 550 getString(
517 ) 551 R.string.install_game_content_success_install,
552 installSuccess
518 ) 553 )
554 )
555 installResult.append(separator)
556 }
557 if (installOverwrite > 0) {
558 installResult.append(
559 getString(
560 R.string.install_game_content_success_overwrite,
561 installOverwrite
562 )
563 )
564 installResult.append(separator)
565 }
566 val errorTotal: Int = errorBaseGame + errorExtension + errorOther
567 if (errorTotal > 0) {
568 installResult.append(separator)
569 installResult.append(
570 getString(
571 R.string.install_game_content_failed_count,
572 errorTotal
573 )
574 )
575 installResult.append(separator)
576 if (errorBaseGame > 0) {
519 installResult.append(separator) 577 installResult.append(separator)
520 }
521 if (installOverwrite > 0) {
522 installResult.append( 578 installResult.append(
523 getString( 579 getString(R.string.install_game_content_failure_base)
524 R.string.install_game_content_success_overwrite,
525 installOverwrite
526 )
527 ) 580 )
528 installResult.append(separator) 581 installResult.append(separator)
529 } 582 }
530 val errorTotal: Int = errorBaseGame + errorExtension + errorOther 583 if (errorExtension > 0) {
531 if (errorTotal > 0) {
532 installResult.append(separator) 584 installResult.append(separator)
533 installResult.append( 585 installResult.append(
534 getString( 586 getString(R.string.install_game_content_failure_file_extension)
535 R.string.install_game_content_failed_count,
536 errorTotal
537 )
538 ) 587 )
539 installResult.append(separator) 588 installResult.append(separator)
540 if (errorBaseGame > 0) { 589 }
541 installResult.append(separator) 590 if (errorOther > 0) {
542 installResult.append( 591 installResult.append(
543 getString(R.string.install_game_content_failure_base) 592 getString(R.string.install_game_content_failure_description)
544 )
545 installResult.append(separator)
546 }
547 if (errorExtension > 0) {
548 installResult.append(separator)
549 installResult.append(
550 getString(R.string.install_game_content_failure_file_extension)
551 )
552 installResult.append(separator)
553 }
554 if (errorOther > 0) {
555 installResult.append(
556 getString(R.string.install_game_content_failure_description)
557 )
558 installResult.append(separator)
559 }
560 return@newInstance MessageDialogFragment.newInstance(
561 this,
562 titleId = R.string.install_game_content_failure,
563 descriptionString = installResult.toString().trim(),
564 helpLinkId = R.string.install_game_content_help_link
565 )
566 } else {
567 return@newInstance MessageDialogFragment.newInstance(
568 this,
569 titleId = R.string.install_game_content_success,
570 descriptionString = installResult.toString().trim()
571 ) 593 )
594 installResult.append(separator)
572 } 595 }
573 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) 596 return@newInstance MessageDialogFragment.newInstance(
574 } 597 this,
598 titleId = R.string.install_game_content_failure,
599 descriptionString = installResult.toString().trim(),
600 helpLinkId = R.string.install_game_content_help_link
601 )
602 } else {
603 return@newInstance MessageDialogFragment.newInstance(
604 this,
605 titleId = R.string.install_game_content_success,
606 descriptionString = installResult.toString().trim()
607 )
608 }
609 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
575 } 610 }
576 611
577 val exportUserData = registerForActivityResult( 612 val exportUserData = registerForActivityResult(
@@ -632,7 +667,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
632 } 667 }
633 668
634 // Clear existing user data 669 // Clear existing user data
635 NativeConfig.unloadConfig() 670 NativeConfig.unloadGlobalConfig()
636 File(DirectoryInitialization.userDirectory!!).deleteRecursively() 671 File(DirectoryInitialization.userDirectory!!).deleteRecursively()
637 672
638 // Copy archive to internal storage 673 // Copy archive to internal storage
@@ -651,108 +686,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
651 686
652 // Reinitialize relevant data 687 // Reinitialize relevant data
653 NativeLibrary.initializeSystem(true) 688 NativeLibrary.initializeSystem(true)
654 NativeConfig.initializeConfig() 689 NativeConfig.initializeGlobalConfig()
655 gamesViewModel.reloadGames(false) 690 gamesViewModel.reloadGames(false)
656 691
657 return@newInstance getString(R.string.user_data_import_success) 692 return@newInstance getString(R.string.user_data_import_success)
658 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) 693 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
659 } 694 }
660
661 /**
662 * Exports the save file located in the given folder path by creating a zip file and sharing it via intent.
663 */
664 val exportSaves = registerForActivityResult(
665 ActivityResultContracts.CreateDocument("application/zip")
666 ) { result ->
667 if (result == null) {
668 return@registerForActivityResult
669 }
670
671 IndeterminateProgressDialogFragment.newInstance(
672 this,
673 R.string.save_files_exporting,
674 false
675 ) {
676 val zipResult = FileUtil.zipFromInternalStorage(
677 File(savesFolderRoot),
678 savesFolderRoot,
679 BufferedOutputStream(contentResolver.openOutputStream(result))
680 )
681 return@newInstance when (zipResult) {
682 TaskState.Completed -> getString(R.string.export_success)
683 TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
684 }
685 }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
686 }
687
688 private val startForResultExportSave =
689 registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ ->
690 File(getPublicFilesDir().canonicalPath, "temp").deleteRecursively()
691 }
692
693 val importSaves =
694 registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
695 if (result == null) {
696 return@registerForActivityResult
697 }
698
699 NativeLibrary.initializeEmptyUserDirectory()
700
701 val inputZip = contentResolver.openInputStream(result)
702 // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid.
703 var validZip = false
704 val savesFolder = File(savesFolderRoot)
705 val cacheSaveDir = File("${applicationContext.cacheDir.path}/saves/")
706 cacheSaveDir.mkdir()
707
708 if (inputZip == null) {
709 Toast.makeText(
710 applicationContext,
711 getString(R.string.fatal_error),
712 Toast.LENGTH_LONG
713 ).show()
714 return@registerForActivityResult
715 }
716
717 val filterTitleId =
718 FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) }
719
720 try {
721 CoroutineScope(Dispatchers.IO).launch {
722 FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
723 cacheSaveDir.list(filterTitleId)?.forEach { savePath ->
724 File(savesFolder, savePath).deleteRecursively()
725 File(cacheSaveDir, savePath).copyRecursively(
726 File(savesFolder, savePath),
727 true
728 )
729 validZip = true
730 }
731
732 withContext(Dispatchers.Main) {
733 if (!validZip) {
734 MessageDialogFragment.newInstance(
735 this@MainActivity,
736 titleId = R.string.save_file_invalid_zip_structure,
737 descriptionId = R.string.save_file_invalid_zip_structure_description
738 ).show(supportFragmentManager, MessageDialogFragment.TAG)
739 return@withContext
740 }
741 Toast.makeText(
742 applicationContext,
743 getString(R.string.save_file_imported_success),
744 Toast.LENGTH_LONG
745 ).show()
746 }
747
748 cacheSaveDir.deleteRecursively()
749 }
750 } catch (e: Exception) {
751 Toast.makeText(
752 applicationContext,
753 getString(R.string.fatal_error),
754 Toast.LENGTH_LONG
755 ).show()
756 }
757 }
758} 695}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddonUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddonUtil.kt
new file mode 100644
index 000000000..8cc5ea71f
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddonUtil.kt
@@ -0,0 +1,8 @@
1// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
2// SPDX-License-Identifier: GPL-2.0-or-later
3
4package org.yuzu.yuzu_emu.utils
5
6object AddonUtil {
7 val validAddonDirectories = listOf("cheats", "exefs", "romfs")
8}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
index 21270fc84..0197fd712 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/DirectoryInitialization.kt
@@ -16,7 +16,7 @@ object DirectoryInitialization {
16 if (!areDirectoriesReady) { 16 if (!areDirectoriesReady) {
17 initializeInternalStorage() 17 initializeInternalStorage()
18 NativeLibrary.initializeSystem(false) 18 NativeLibrary.initializeSystem(false)
19 NativeConfig.initializeConfig() 19 NativeConfig.initializeGlobalConfig()
20 areDirectoriesReady = true 20 areDirectoriesReady = true
21 } 21 }
22 } 22 }
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 bbe7bfa92..00c6bf90e 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
@@ -22,6 +22,7 @@ import java.io.BufferedOutputStream
22import java.lang.NullPointerException 22import java.lang.NullPointerException
23import java.nio.charset.StandardCharsets 23import java.nio.charset.StandardCharsets
24import java.util.zip.ZipOutputStream 24import java.util.zip.ZipOutputStream
25import kotlin.IllegalStateException
25 26
26object FileUtil { 27object FileUtil {
27 const val PATH_TREE = "tree" 28 const val PATH_TREE = "tree"
@@ -342,6 +343,37 @@ object FileUtil {
342 return TaskState.Completed 343 return TaskState.Completed
343 } 344 }
344 345
346 /**
347 * Helper function that copies the contents of a DocumentFile folder into a [File]
348 * @param file [File] representation of the folder to copy into
349 * @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa
350 */
351 fun DocumentFile.copyFilesTo(file: File) {
352 file.mkdirs()
353 if (!this.isDirectory || !file.isDirectory) {
354 throw IllegalStateException(
355 "[FileUtil] Tried to copy a folder into a file or vice versa"
356 )
357 }
358
359 this.listFiles().forEach {
360 val newFile = File(file, it.name!!)
361 if (it.isDirectory) {
362 newFile.mkdirs()
363 DocumentFile.fromTreeUri(YuzuApplication.appContext, it.uri)?.copyFilesTo(newFile)
364 } else {
365 val inputStream =
366 YuzuApplication.appContext.contentResolver.openInputStream(it.uri)
367 BufferedInputStream(inputStream).use { bos ->
368 if (!newFile.exists()) {
369 newFile.createNewFile()
370 }
371 newFile.outputStream().use { os -> bos.copyTo(os) }
372 }
373 }
374 }
375 }
376
345 fun isRootTreeUri(uri: Uri): Boolean { 377 fun isRootTreeUri(uri: Uri): Boolean {
346 val paths = uri.pathSegments 378 val paths = uri.pathSegments
347 return paths.size == 2 && PATH_TREE == paths[0] 379 return paths.size == 2 && PATH_TREE == paths[0]
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
index 55010dc59..579b600f1 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameHelper.kt
@@ -36,6 +36,12 @@ object GameHelper {
36 // Ensure keys are loaded so that ROM metadata can be decrypted. 36 // Ensure keys are loaded so that ROM metadata can be decrypted.
37 NativeLibrary.reloadKeys() 37 NativeLibrary.reloadKeys()
38 38
39 // Reset metadata so we don't use stale information
40 GameMetadata.resetMetadata()
41
42 // Remove previous filesystem provider information so we can get up to date version info
43 NativeLibrary.clearFilesystemProvider()
44
39 val badDirs = mutableListOf<Int>() 45 val badDirs = mutableListOf<Int>()
40 gameDirs.forEachIndexed { index: Int, gameDir: GameDir -> 46 gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
41 val gameDirUri = Uri.parse(gameDir.uriString) 47 val gameDirUri = Uri.parse(gameDir.uriString)
@@ -92,14 +98,24 @@ object GameHelper {
92 ) 98 )
93 } else { 99 } else {
94 if (Game.extensions.contains(FileUtil.getExtension(it.uri))) { 100 if (Game.extensions.contains(FileUtil.getExtension(it.uri))) {
95 games.add(getGame(it.uri, true)) 101 val game = getGame(it.uri, true)
102 if (game != null) {
103 games.add(game)
104 }
96 } 105 }
97 } 106 }
98 } 107 }
99 } 108 }
100 109
101 fun getGame(uri: Uri, addedToLibrary: Boolean): Game { 110 fun getGame(uri: Uri, addedToLibrary: Boolean): Game? {
102 val filePath = uri.toString() 111 val filePath = uri.toString()
112 if (!GameMetadata.getIsValid(filePath)) {
113 return null
114 }
115
116 // Needed to update installed content information
117 NativeLibrary.addFileToFilesystemProvider(filePath)
118
103 var name = GameMetadata.getTitle(filePath) 119 var name = GameMetadata.getTitle(filePath)
104 120
105 // If the game's title field is empty, use the filename. 121 // If the game's title field is empty, use the filename.
@@ -118,7 +134,7 @@ object GameHelper {
118 filePath, 134 filePath,
119 programId, 135 programId,
120 GameMetadata.getDeveloper(filePath), 136 GameMetadata.getDeveloper(filePath),
121 GameMetadata.getVersion(filePath), 137 GameMetadata.getVersion(filePath, false),
122 GameMetadata.getIsHomebrew(filePath) 138 GameMetadata.getIsHomebrew(filePath)
123 ) 139 )
124 140
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameMetadata.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameMetadata.kt
index 0f3542ac6..8e412482a 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameMetadata.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameMetadata.kt
@@ -4,13 +4,15 @@
4package org.yuzu.yuzu_emu.utils 4package org.yuzu.yuzu_emu.utils
5 5
6object GameMetadata { 6object GameMetadata {
7 external fun getIsValid(path: String): Boolean
8
7 external fun getTitle(path: String): String 9 external fun getTitle(path: String): String
8 10
9 external fun getProgramId(path: String): String 11 external fun getProgramId(path: String): String
10 12
11 external fun getDeveloper(path: String): String 13 external fun getDeveloper(path: String): String
12 14
13 external fun getVersion(path: String): String 15 external fun getVersion(path: String, reload: Boolean): String
14 16
15 external fun getIcon(path: String): ByteArray 17 external fun getIcon(path: String): ByteArray
16 18
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 f6882ce6c..685272288 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
@@ -10,6 +10,8 @@ import java.io.File
10import java.io.IOException 10import java.io.IOException
11import org.yuzu.yuzu_emu.NativeLibrary 11import org.yuzu.yuzu_emu.NativeLibrary
12import org.yuzu.yuzu_emu.YuzuApplication 12import org.yuzu.yuzu_emu.YuzuApplication
13import org.yuzu.yuzu_emu.features.settings.model.StringSetting
14import java.io.FileNotFoundException
13import java.util.zip.ZipException 15import java.util.zip.ZipException
14import java.util.zip.ZipFile 16import java.util.zip.ZipFile
15 17
@@ -44,7 +46,7 @@ object GpuDriverHelper {
44 NativeLibrary.initializeGpuDriver( 46 NativeLibrary.initializeGpuDriver(
45 hookLibPath, 47 hookLibPath,
46 driverInstallationPath, 48 driverInstallationPath,
47 customDriverData.libraryName, 49 installedCustomDriverData.libraryName,
48 fileRedirectionPath 50 fileRedirectionPath
49 ) 51 )
50 } 52 }
@@ -190,6 +192,7 @@ object GpuDriverHelper {
190 } 192 }
191 } 193 }
192 } catch (_: ZipException) { 194 } catch (_: ZipException) {
195 } catch (_: FileNotFoundException) {
193 } 196 }
194 return GpuDriverMetadata() 197 return GpuDriverMetadata()
195 } 198 }
@@ -197,9 +200,12 @@ object GpuDriverHelper {
197 external fun supportsCustomDriverLoading(): Boolean 200 external fun supportsCustomDriverLoading(): Boolean
198 201
199 // Parse the custom driver metadata to retrieve the name. 202 // Parse the custom driver metadata to retrieve the name.
200 val customDriverData: GpuDriverMetadata 203 val installedCustomDriverData: GpuDriverMetadata
201 get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME)) 204 get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME))
202 205
206 val customDriverSettingData: GpuDriverMetadata
207 get() = getMetadataFromZip(File(StringSetting.DRIVER_PATH.getString()))
208
203 fun initializeDirectories() { 209 fun initializeDirectories() {
204 // Ensure the file redirection directory exists. 210 // Ensure the file redirection directory exists.
205 val fileRedirectionDir = File(fileRedirectionPath!!) 211 val fileRedirectionDir = File(fileRedirectionPath!!)
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt
index 9076a86c4..0b94c73e5 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/MemoryUtil.kt
@@ -27,13 +27,13 @@ object MemoryUtil {
27 const val Pb = Tb * 1024 27 const val Pb = Tb * 1024
28 const val Eb = Pb * 1024 28 const val Eb = Pb * 1024
29 29
30 private fun bytesToSizeUnit(size: Float, roundUp: Boolean = false): String = 30 fun bytesToSizeUnit(size: Float, roundUp: Boolean = false): String =
31 when { 31 when {
32 size < Kb -> { 32 size < Kb -> {
33 context.getString( 33 context.getString(
34 R.string.memory_formatted, 34 R.string.memory_formatted,
35 size.hundredths, 35 size.hundredths,
36 context.getString(R.string.memory_byte) 36 context.getString(R.string.memory_byte_shorthand)
37 ) 37 )
38 } 38 }
39 size < Mb -> { 39 size < Mb -> {
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
index f4e1bb13f..7512d5eed 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/NativeConfig.kt
@@ -7,56 +7,113 @@ import org.yuzu.yuzu_emu.model.GameDir
7 7
8object NativeConfig { 8object NativeConfig {
9 /** 9 /**
10 * Creates a Config object and opens the emulation config. 10 * Loads global config.
11 */ 11 */
12 @Synchronized 12 @Synchronized
13 external fun initializeConfig() 13 external fun initializeGlobalConfig()
14 14
15 /** 15 /**
16 * Destroys the stored config object. This automatically saves the existing config. 16 * Destroys the stored global config object. This does not save the existing config.
17 */ 17 */
18 @Synchronized 18 @Synchronized
19 external fun unloadConfig() 19 external fun unloadGlobalConfig()
20 20
21 /** 21 /**
22 * Reads values saved to the config file and saves them. 22 * Reads values in the global config file and saves them.
23 */ 23 */
24 @Synchronized 24 @Synchronized
25 external fun reloadSettings() 25 external fun reloadGlobalConfig()
26 26
27 /** 27 /**
28 * Saves settings values in memory to disk. 28 * Saves global settings values in memory to disk.
29 */ 29 */
30 @Synchronized 30 @Synchronized
31 external fun saveSettings() 31 external fun saveGlobalConfig()
32 32
33 external fun getBoolean(key: String, getDefault: Boolean): Boolean 33 /**
34 * Creates per-game config for the specified parameters. Must be unloaded once per-game config
35 * is closed with [unloadPerGameConfig]. All switchable values that [NativeConfig] gets/sets
36 * will follow the per-game config until the global config is reloaded.
37 *
38 * @param programId String representation of the u64 programId
39 * @param fileName Filename of the game, including its extension
40 */
41 @Synchronized
42 external fun initializePerGameConfig(programId: String, fileName: String)
43
44 @Synchronized
45 external fun isPerGameConfigLoaded(): Boolean
46
47 /**
48 * Saves per-game settings values in memory to disk.
49 */
50 @Synchronized
51 external fun savePerGameConfig()
52
53 /**
54 * Destroys the stored per-game config object. This does not save the config.
55 */
56 @Synchronized
57 external fun unloadPerGameConfig()
58
59 @Synchronized
60 external fun getBoolean(key: String, needsGlobal: Boolean): Boolean
61
62 @Synchronized
34 external fun setBoolean(key: String, value: Boolean) 63 external fun setBoolean(key: String, value: Boolean)
35 64
36 external fun getByte(key: String, getDefault: Boolean): Byte 65 @Synchronized
66 external fun getByte(key: String, needsGlobal: Boolean): Byte
67
68 @Synchronized
37 external fun setByte(key: String, value: Byte) 69 external fun setByte(key: String, value: Byte)
38 70
39 external fun getShort(key: String, getDefault: Boolean): Short 71 @Synchronized
72 external fun getShort(key: String, needsGlobal: Boolean): Short
73
74 @Synchronized
40 external fun setShort(key: String, value: Short) 75 external fun setShort(key: String, value: Short)
41 76
42 external fun getInt(key: String, getDefault: Boolean): Int 77 @Synchronized
78 external fun getInt(key: String, needsGlobal: Boolean): Int
79
80 @Synchronized
43 external fun setInt(key: String, value: Int) 81 external fun setInt(key: String, value: Int)
44 82
45 external fun getFloat(key: String, getDefault: Boolean): Float 83 @Synchronized
84 external fun getFloat(key: String, needsGlobal: Boolean): Float
85
86 @Synchronized
46 external fun setFloat(key: String, value: Float) 87 external fun setFloat(key: String, value: Float)
47 88
48 external fun getLong(key: String, getDefault: Boolean): Long 89 @Synchronized
90 external fun getLong(key: String, needsGlobal: Boolean): Long
91
92 @Synchronized
49 external fun setLong(key: String, value: Long) 93 external fun setLong(key: String, value: Long)
50 94
51 external fun getString(key: String, getDefault: Boolean): String 95 @Synchronized
96 external fun getString(key: String, needsGlobal: Boolean): String
97
98 @Synchronized
52 external fun setString(key: String, value: String) 99 external fun setString(key: String, value: String)
53 100
54 external fun getIsRuntimeModifiable(key: String): Boolean 101 external fun getIsRuntimeModifiable(key: String): Boolean
55 102
56 external fun getConfigHeader(category: Int): String
57
58 external fun getPairedSettingKey(key: String): String 103 external fun getPairedSettingKey(key: String): String
59 104
105 external fun getIsSwitchable(key: String): Boolean
106
107 @Synchronized
108 external fun usingGlobal(key: String): Boolean
109
110 @Synchronized
111 external fun setGlobal(key: String, global: Boolean)
112
113 external fun getIsSaveable(key: String): Boolean
114
115 external fun getDefaultToString(key: String): String
116
60 /** 117 /**
61 * Gets every [GameDir] in AndroidSettings::values.game_dirs 118 * Gets every [GameDir] in AndroidSettings::values.game_dirs
62 */ 119 */
@@ -74,4 +131,23 @@ object NativeConfig {
74 */ 131 */
75 @Synchronized 132 @Synchronized
76 external fun addGameDir(dir: GameDir) 133 external fun addGameDir(dir: GameDir)
134
135 /**
136 * Gets an array of the addons that are disabled for a given game
137 *
138 * @param programId String representation of a game's program ID
139 * @return An array of disabled addons
140 */
141 @Synchronized
142 external fun getDisabledAddons(programId: String): Array<String>
143
144 /**
145 * Clears the disabled addons array corresponding to [programId] and replaces them
146 * with [disabledAddons]
147 *
148 * @param programId String representation of a game's program ID
149 * @param disabledAddons Replacement array of disabled addons
150 */
151 @Synchronized
152 external fun setDisabledAddons(programId: String, disabledAddons: Array<String>)
77} 153}
diff --git a/src/android/app/src/main/jni/android_config.cpp b/src/android/app/src/main/jni/android_config.cpp
index 767d8ea83..9c3a5a9b2 100644
--- a/src/android/app/src/main/jni/android_config.cpp
+++ b/src/android/app/src/main/jni/android_config.cpp
@@ -36,6 +36,7 @@ void AndroidConfig::ReadAndroidValues() {
36 ReadAndroidUIValues(); 36 ReadAndroidUIValues();
37 ReadUIValues(); 37 ReadUIValues();
38 } 38 }
39 ReadDriverValues();
39} 40}
40 41
41void AndroidConfig::ReadAndroidUIValues() { 42void AndroidConfig::ReadAndroidUIValues() {
@@ -57,6 +58,7 @@ void AndroidConfig::ReadUIValues() {
57void AndroidConfig::ReadPathValues() { 58void AndroidConfig::ReadPathValues() {
58 BeginGroup(Settings::TranslateCategory(Settings::Category::Paths)); 59 BeginGroup(Settings::TranslateCategory(Settings::Category::Paths));
59 60
61 AndroidSettings::values.game_dirs.clear();
60 const int gamedirs_size = BeginArray(std::string("gamedirs")); 62 const int gamedirs_size = BeginArray(std::string("gamedirs"));
61 for (int i = 0; i < gamedirs_size; ++i) { 63 for (int i = 0; i < gamedirs_size; ++i) {
62 SetArrayIndex(i); 64 SetArrayIndex(i);
@@ -71,11 +73,20 @@ void AndroidConfig::ReadPathValues() {
71 EndGroup(); 73 EndGroup();
72} 74}
73 75
76void AndroidConfig::ReadDriverValues() {
77 BeginGroup(Settings::TranslateCategory(Settings::Category::GpuDriver));
78
79 ReadCategory(Settings::Category::GpuDriver);
80
81 EndGroup();
82}
83
74void AndroidConfig::SaveAndroidValues() { 84void AndroidConfig::SaveAndroidValues() {
75 if (global) { 85 if (global) {
76 SaveAndroidUIValues(); 86 SaveAndroidUIValues();
77 SaveUIValues(); 87 SaveUIValues();
78 } 88 }
89 SaveDriverValues();
79 90
80 WriteToIni(); 91 WriteToIni();
81} 92}
@@ -111,6 +122,14 @@ void AndroidConfig::SavePathValues() {
111 EndGroup(); 122 EndGroup();
112} 123}
113 124
125void AndroidConfig::SaveDriverValues() {
126 BeginGroup(Settings::TranslateCategory(Settings::Category::GpuDriver));
127
128 WriteCategory(Settings::Category::GpuDriver);
129
130 EndGroup();
131}
132
114std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) { 133std::vector<Settings::BasicSetting*>& AndroidConfig::FindRelevantList(Settings::Category category) {
115 auto& map = Settings::values.linkage.by_category; 134 auto& map = Settings::values.linkage.by_category;
116 if (map.contains(category)) { 135 if (map.contains(category)) {
diff --git a/src/android/app/src/main/jni/android_config.h b/src/android/app/src/main/jni/android_config.h
index f490be016..2c12874e1 100644
--- a/src/android/app/src/main/jni/android_config.h
+++ b/src/android/app/src/main/jni/android_config.h
@@ -17,6 +17,7 @@ public:
17protected: 17protected:
18 void ReadAndroidValues(); 18 void ReadAndroidValues();
19 void ReadAndroidUIValues(); 19 void ReadAndroidUIValues();
20 void ReadDriverValues();
20 void ReadHidbusValues() override {} 21 void ReadHidbusValues() override {}
21 void ReadDebugControlValues() override {} 22 void ReadDebugControlValues() override {}
22 void ReadPathValues() override; 23 void ReadPathValues() override;
@@ -28,6 +29,7 @@ protected:
28 29
29 void SaveAndroidValues(); 30 void SaveAndroidValues();
30 void SaveAndroidUIValues(); 31 void SaveAndroidUIValues();
32 void SaveDriverValues();
31 void SaveHidbusValues() override {} 33 void SaveHidbusValues() override {}
32 void SaveDebugControlValues() override {} 34 void SaveDebugControlValues() override {}
33 void SavePathValues() override; 35 void SavePathValues() override;
diff --git a/src/android/app/src/main/jni/android_settings.h b/src/android/app/src/main/jni/android_settings.h
index fc0523206..3733f5a3c 100644
--- a/src/android/app/src/main/jni/android_settings.h
+++ b/src/android/app/src/main/jni/android_settings.h
@@ -30,6 +30,9 @@ struct Values {
30 Settings::Specialization::Default, 30 Settings::Specialization::Default,
31 true, 31 true,
32 true}; 32 true};
33
34 Settings::SwitchableSetting<std::string, false> driver_path{linkage, "", "driver_path",
35 Settings::Category::GpuDriver};
33}; 36};
34 37
35extern Values values; 38extern Values values;
diff --git a/src/android/app/src/main/jni/game_metadata.cpp b/src/android/app/src/main/jni/game_metadata.cpp
index 24d9df702..78f604c70 100644
--- a/src/android/app/src/main/jni/game_metadata.cpp
+++ b/src/android/app/src/main/jni/game_metadata.cpp
@@ -2,6 +2,7 @@
2// SPDX-License-Identifier: GPL-2.0-or-later 2// SPDX-License-Identifier: GPL-2.0-or-later
3 3
4#include <core/core.h> 4#include <core/core.h>
5#include <core/file_sys/mode.h>
5#include <core/file_sys/patch_manager.h> 6#include <core/file_sys/patch_manager.h>
6#include <core/loader/nro.h> 7#include <core/loader/nro.h>
7#include <jni.h> 8#include <jni.h>
@@ -61,7 +62,11 @@ RomMetadata CacheRomMetadata(const std::string& path) {
61 return entry; 62 return entry;
62} 63}
63 64
64RomMetadata GetRomMetadata(const std::string& path) { 65RomMetadata GetRomMetadata(const std::string& path, bool reload = false) {
66 if (reload) {
67 return CacheRomMetadata(path);
68 }
69
65 if (auto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) { 70 if (auto search = m_rom_metadata_cache.find(path); search != m_rom_metadata_cache.end()) {
66 return search->second; 71 return search->second;
67 } 72 }
@@ -71,6 +76,32 @@ RomMetadata GetRomMetadata(const std::string& path) {
71 76
72extern "C" { 77extern "C" {
73 78
79jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsValid(JNIEnv* env, jobject obj,
80 jstring jpath) {
81 const auto file = EmulationSession::GetInstance().System().GetFilesystem()->OpenFile(
82 GetJString(env, jpath), FileSys::Mode::Read);
83 if (!file) {
84 return false;
85 }
86
87 auto loader = Loader::GetLoader(EmulationSession::GetInstance().System(), file);
88 if (!loader) {
89 return false;
90 }
91
92 const auto file_type = loader->GetFileType();
93 if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) {
94 return false;
95 }
96
97 u64 program_id = 0;
98 Loader::ResultStatus res = loader->ReadProgramId(program_id);
99 if (res != Loader::ResultStatus::Success) {
100 return false;
101 }
102 return true;
103}
104
74jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getTitle(JNIEnv* env, jobject obj, 105jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getTitle(JNIEnv* env, jobject obj,
75 jstring jpath) { 106 jstring jpath) {
76 return ToJString(env, GetRomMetadata(GetJString(env, jpath)).title); 107 return ToJString(env, GetRomMetadata(GetJString(env, jpath)).title);
@@ -87,8 +118,8 @@ jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getDeveloper(JNIEnv* env, job
87} 118}
88 119
89jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getVersion(JNIEnv* env, jobject obj, 120jstring Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getVersion(JNIEnv* env, jobject obj,
90 jstring jpath) { 121 jstring jpath, jboolean jreload) {
91 return ToJString(env, GetRomMetadata(GetJString(env, jpath)).version); 122 return ToJString(env, GetRomMetadata(GetJString(env, jpath), jreload).version);
92} 123}
93 124
94jbyteArray Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIcon(JNIEnv* env, jobject obj, 125jbyteArray Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIcon(JNIEnv* env, jobject obj,
@@ -106,7 +137,7 @@ jboolean Java_org_yuzu_yuzu_1emu_utils_GameMetadata_getIsHomebrew(JNIEnv* env, j
106} 137}
107 138
108void Java_org_yuzu_yuzu_1emu_utils_GameMetadata_resetMetadata(JNIEnv* env, jobject obj) { 139void Java_org_yuzu_yuzu_1emu_utils_GameMetadata_resetMetadata(JNIEnv* env, jobject obj) {
109 return m_rom_metadata_cache.clear(); 140 m_rom_metadata_cache.clear();
110} 141}
111 142
112} // extern "C" 143} // extern "C"
diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp
index a56ed5662..e7a86d3fd 100644
--- a/src/android/app/src/main/jni/id_cache.cpp
+++ b/src/android/app/src/main/jni/id_cache.cpp
@@ -20,6 +20,21 @@ static jmethodID s_disk_cache_load_progress;
20static jmethodID s_on_emulation_started; 20static jmethodID s_on_emulation_started;
21static jmethodID s_on_emulation_stopped; 21static jmethodID s_on_emulation_stopped;
22 22
23static jclass s_game_class;
24static jmethodID s_game_constructor;
25static jfieldID s_game_title_field;
26static jfieldID s_game_path_field;
27static jfieldID s_game_program_id_field;
28static jfieldID s_game_developer_field;
29static jfieldID s_game_version_field;
30static jfieldID s_game_is_homebrew_field;
31
32static jclass s_string_class;
33static jclass s_pair_class;
34static jmethodID s_pair_constructor;
35static jfieldID s_pair_first_field;
36static jfieldID s_pair_second_field;
37
23static constexpr jint JNI_VERSION = JNI_VERSION_1_6; 38static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
24 39
25namespace IDCache { 40namespace IDCache {
@@ -79,6 +94,58 @@ jmethodID GetOnEmulationStopped() {
79 return s_on_emulation_stopped; 94 return s_on_emulation_stopped;
80} 95}
81 96
97jclass GetGameClass() {
98 return s_game_class;
99}
100
101jmethodID GetGameConstructor() {
102 return s_game_constructor;
103}
104
105jfieldID GetGameTitleField() {
106 return s_game_title_field;
107}
108
109jfieldID GetGamePathField() {
110 return s_game_path_field;
111}
112
113jfieldID GetGameProgramIdField() {
114 return s_game_program_id_field;
115}
116
117jfieldID GetGameDeveloperField() {
118 return s_game_developer_field;
119}
120
121jfieldID GetGameVersionField() {
122 return s_game_version_field;
123}
124
125jfieldID GetGameIsHomebrewField() {
126 return s_game_is_homebrew_field;
127}
128
129jclass GetStringClass() {
130 return s_string_class;
131}
132
133jclass GetPairClass() {
134 return s_pair_class;
135}
136
137jmethodID GetPairConstructor() {
138 return s_pair_constructor;
139}
140
141jfieldID GetPairFirstField() {
142 return s_pair_first_field;
143}
144
145jfieldID GetPairSecondField() {
146 return s_pair_second_field;
147}
148
82} // namespace IDCache 149} // namespace IDCache
83 150
84#ifdef __cplusplus 151#ifdef __cplusplus
@@ -115,6 +182,31 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) {
115 s_on_emulation_stopped = 182 s_on_emulation_stopped =
116 env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V"); 183 env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V");
117 184
185 const jclass game_class = env->FindClass("org/yuzu/yuzu_emu/model/Game");
186 s_game_class = reinterpret_cast<jclass>(env->NewGlobalRef(game_class));
187 s_game_constructor = env->GetMethodID(game_class, "<init>",
188 "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/"
189 "String;Ljava/lang/String;Ljava/lang/String;Z)V");
190 s_game_title_field = env->GetFieldID(game_class, "title", "Ljava/lang/String;");
191 s_game_path_field = env->GetFieldID(game_class, "path", "Ljava/lang/String;");
192 s_game_program_id_field = env->GetFieldID(game_class, "programId", "Ljava/lang/String;");
193 s_game_developer_field = env->GetFieldID(game_class, "developer", "Ljava/lang/String;");
194 s_game_version_field = env->GetFieldID(game_class, "version", "Ljava/lang/String;");
195 s_game_is_homebrew_field = env->GetFieldID(game_class, "isHomebrew", "Z");
196 env->DeleteLocalRef(game_class);
197
198 const jclass string_class = env->FindClass("java/lang/String");
199 s_string_class = reinterpret_cast<jclass>(env->NewGlobalRef(string_class));
200 env->DeleteLocalRef(string_class);
201
202 const jclass pair_class = env->FindClass("kotlin/Pair");
203 s_pair_class = reinterpret_cast<jclass>(env->NewGlobalRef(pair_class));
204 s_pair_constructor =
205 env->GetMethodID(pair_class, "<init>", "(Ljava/lang/Object;Ljava/lang/Object;)V");
206 s_pair_first_field = env->GetFieldID(pair_class, "first", "Ljava/lang/Object;");
207 s_pair_second_field = env->GetFieldID(pair_class, "second", "Ljava/lang/Object;");
208 env->DeleteLocalRef(pair_class);
209
118 // Initialize Android Storage 210 // Initialize Android Storage
119 Common::FS::Android::RegisterCallbacks(env, s_native_library_class); 211 Common::FS::Android::RegisterCallbacks(env, s_native_library_class);
120 212
@@ -136,6 +228,9 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) {
136 env->DeleteGlobalRef(s_disk_cache_progress_class); 228 env->DeleteGlobalRef(s_disk_cache_progress_class);
137 env->DeleteGlobalRef(s_load_callback_stage_class); 229 env->DeleteGlobalRef(s_load_callback_stage_class);
138 env->DeleteGlobalRef(s_game_dir_class); 230 env->DeleteGlobalRef(s_game_dir_class);
231 env->DeleteGlobalRef(s_game_class);
232 env->DeleteGlobalRef(s_string_class);
233 env->DeleteGlobalRef(s_pair_class);
139 234
140 // UnInitialize applets 235 // UnInitialize applets
141 SoftwareKeyboard::CleanupJNI(env); 236 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 855649efa..24030be42 100644
--- a/src/android/app/src/main/jni/id_cache.h
+++ b/src/android/app/src/main/jni/id_cache.h
@@ -20,4 +20,19 @@ jmethodID GetDiskCacheLoadProgress();
20jmethodID GetOnEmulationStarted(); 20jmethodID GetOnEmulationStarted();
21jmethodID GetOnEmulationStopped(); 21jmethodID GetOnEmulationStopped();
22 22
23jclass GetGameClass();
24jmethodID GetGameConstructor();
25jfieldID GetGameTitleField();
26jfieldID GetGamePathField();
27jfieldID GetGameProgramIdField();
28jfieldID GetGameDeveloperField();
29jfieldID GetGameVersionField();
30jfieldID GetGameIsHomebrewField();
31
32jclass GetStringClass();
33jclass GetPairClass();
34jmethodID GetPairConstructor();
35jfieldID GetPairFirstField();
36jfieldID GetPairSecondField();
37
23} // namespace IDCache 38} // namespace IDCache
diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index e5d3158c8..0c1db7d46 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -14,6 +14,7 @@
14#include <android/api-level.h> 14#include <android/api-level.h>
15#include <android/native_window_jni.h> 15#include <android/native_window_jni.h>
16#include <common/fs/fs.h> 16#include <common/fs/fs.h>
17#include <core/file_sys/patch_manager.h>
17#include <core/file_sys/savedata_factory.h> 18#include <core/file_sys/savedata_factory.h>
18#include <core/loader/nro.h> 19#include <core/loader/nro.h>
19#include <jni.h> 20#include <jni.h>
@@ -79,6 +80,10 @@ Core::System& EmulationSession::System() {
79 return m_system; 80 return m_system;
80} 81}
81 82
83FileSys::ManualContentProvider* EmulationSession::GetContentProvider() {
84 return m_manual_provider.get();
85}
86
82const EmuWindow_Android& EmulationSession::Window() const { 87const EmuWindow_Android& EmulationSession::Window() const {
83 return *m_window; 88 return *m_window;
84} 89}
@@ -455,6 +460,15 @@ void EmulationSession::OnEmulationStopped(Core::SystemResultStatus result) {
455 static_cast<jint>(result)); 460 static_cast<jint>(result));
456} 461}
457 462
463u64 EmulationSession::GetProgramId(JNIEnv* env, jstring jprogramId) {
464 auto program_id_string = GetJString(env, jprogramId);
465 try {
466 return std::stoull(program_id_string);
467 } catch (...) {
468 return 0;
469 }
470}
471
458static Core::SystemResultStatus RunEmulation(const std::string& filepath) { 472static Core::SystemResultStatus RunEmulation(const std::string& filepath) {
459 MicroProfileOnThreadCreate("EmuThread"); 473 MicroProfileOnThreadCreate("EmuThread");
460 SCOPE_EXIT({ MicroProfileShutdown(); }); 474 SCOPE_EXIT({ MicroProfileShutdown(); });
@@ -504,6 +518,27 @@ int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject
504 GetJString(env, j_file_extension)); 518 GetJString(env, j_file_extension));
505} 519}
506 520
521jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj,
522 jstring jprogramId,
523 jstring jupdatePath) {
524 u64 program_id = EmulationSession::GetProgramId(env, jprogramId);
525 std::string updatePath = GetJString(env, jupdatePath);
526 std::shared_ptr<FileSys::NSP> nsp = std::make_shared<FileSys::NSP>(
527 EmulationSession::GetInstance().System().GetFilesystem()->OpenFile(updatePath,
528 FileSys::Mode::Read));
529 for (const auto& item : nsp->GetNCAs()) {
530 for (const auto& nca_details : item.second) {
531 if (nca_details.second->GetName().ends_with(".cnmt.nca")) {
532 auto update_id = nca_details.second->GetTitleId() & ~0xFFFULL;
533 if (update_id == program_id) {
534 return true;
535 }
536 }
537 }
538 }
539 return false;
540}
541
507void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, jclass clazz, 542void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, jclass clazz,
508 jstring hook_lib_dir, 543 jstring hook_lib_dir,
509 jstring custom_driver_dir, 544 jstring custom_driver_dir,
@@ -665,13 +700,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass
665 EmulationSession::GetInstance().InitializeSystem(reload); 700 EmulationSession::GetInstance().InitializeSystem(reload);
666} 701}
667 702
668jint Java_org_yuzu_yuzu_1emu_NativeLibrary_defaultCPUCore(JNIEnv* env, jclass clazz) {
669 return {};
670}
671
672void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2Ljava_lang_String_2Z(
673 JNIEnv* env, jclass clazz, jstring j_file, jstring j_savestate, jboolean j_delete_savestate) {}
674
675jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats(JNIEnv* env, jclass clazz) { 703jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats(JNIEnv* env, jclass clazz) {
676 jdoubleArray j_stats = env->NewDoubleArray(4); 704 jdoubleArray j_stats = env->NewDoubleArray(4);
677 705
@@ -696,9 +724,13 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuBackend(JNIEnv* env, jclass
696 return ToJString(env, "JIT"); 724 return ToJString(env, "JIT");
697} 725}
698 726
699void Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_setSysDirectory(JNIEnv* env, 727void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) {
700 jclass clazz, 728 EmulationSession::GetInstance().System().ApplySettings();
701 jstring j_path) {} 729}
730
731void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj) {
732 Settings::LogSettings();
733}
702 734
703void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2(JNIEnv* env, jclass clazz, 735void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2(JNIEnv* env, jclass clazz,
704 jstring j_path) { 736 jstring j_path) {
@@ -792,4 +824,69 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env,
792 return true; 824 return true;
793} 825}
794 826
827jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, jobject jobj,
828 jstring jpath,
829 jstring jprogramId) {
830 const auto path = GetJString(env, jpath);
831 const auto vFile =
832 Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path);
833 if (vFile == nullptr) {
834 return nullptr;
835 }
836
837 auto& system = EmulationSession::GetInstance().System();
838 auto program_id = EmulationSession::GetProgramId(env, jprogramId);
839 const FileSys::PatchManager pm{program_id, system.GetFileSystemController(),
840 system.GetContentProvider()};
841 const auto loader = Loader::GetLoader(system, vFile);
842
843 FileSys::VirtualFile update_raw;
844 loader->ReadUpdateRaw(update_raw);
845
846 auto addons = pm.GetPatchVersionNames(update_raw);
847 auto jemptyString = ToJString(env, "");
848 auto jemptyStringPair = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(),
849 jemptyString, jemptyString);
850 jobjectArray jaddonsArray =
851 env->NewObjectArray(addons.size(), IDCache::GetPairClass(), jemptyStringPair);
852 int i = 0;
853 for (const auto& addon : addons) {
854 jobject jaddon = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(),
855 ToJString(env, addon.first), ToJString(env, addon.second));
856 env->SetObjectArrayElement(jaddonsArray, i, jaddon);
857 ++i;
858 }
859 return jaddonsArray;
860}
861
862jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj,
863 jstring jprogramId) {
864 auto program_id = EmulationSession::GetProgramId(env, jprogramId);
865
866 auto& system = EmulationSession::GetInstance().System();
867
868 Service::Account::ProfileManager manager;
869 // TODO: Pass in a selected user once we get the relevant UI working
870 const auto user_id = manager.GetUser(static_cast<std::size_t>(0));
871 ASSERT(user_id);
872
873 const auto nandDir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir);
874 auto vfsNandDir = system.GetFilesystem()->OpenDirectory(Common::FS::PathToUTF8String(nandDir),
875 FileSys::Mode::Read);
876
877 const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath(
878 system, vfsNandDir, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData,
879 program_id, user_id->AsU128(), 0);
880 return ToJString(env, user_save_data_path);
881}
882
883void Java_org_yuzu_yuzu_1emu_NativeLibrary_addFileToFilesystemProvider(JNIEnv* env, jobject jobj,
884 jstring jpath) {
885 EmulationSession::GetInstance().ConfigureFilesystemProvider(GetJString(env, jpath));
886}
887
888void Java_org_yuzu_yuzu_1emu_NativeLibrary_clearFilesystemProvider(JNIEnv* env, jobject jobj) {
889 EmulationSession::GetInstance().GetContentProvider()->ClearAllEntries();
890}
891
795} // extern "C" 892} // extern "C"
diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h
index f1457bd1f..4a8049578 100644
--- a/src/android/app/src/main/jni/native.h
+++ b/src/android/app/src/main/jni/native.h
@@ -21,6 +21,7 @@ public:
21 static EmulationSession& GetInstance(); 21 static EmulationSession& GetInstance();
22 const Core::System& System() const; 22 const Core::System& System() const;
23 Core::System& System(); 23 Core::System& System();
24 FileSys::ManualContentProvider* GetContentProvider();
24 25
25 const EmuWindow_Android& Window() const; 26 const EmuWindow_Android& Window() const;
26 EmuWindow_Android& Window(); 27 EmuWindow_Android& Window();
@@ -54,6 +55,8 @@ public:
54 55
55 static void OnEmulationStarted(); 56 static void OnEmulationStarted();
56 57
58 static u64 GetProgramId(JNIEnv* env, jstring jprogramId);
59
57private: 60private:
58 static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max); 61 static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max);
59 static void OnEmulationStopped(Core::SystemResultStatus result); 62 static void OnEmulationStopped(Core::SystemResultStatus result);
diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp
index 763b2164c..324d9e9cd 100644
--- a/src/android/app/src/main/jni/native_config.cpp
+++ b/src/android/app/src/main/jni/native_config.cpp
@@ -3,6 +3,7 @@
3 3
4#include <string> 4#include <string>
5 5
6#include <common/fs/fs_util.h>
6#include <jni.h> 7#include <jni.h>
7 8
8#include "android_config.h" 9#include "android_config.h"
@@ -12,19 +13,21 @@
12#include "frontend_common/config.h" 13#include "frontend_common/config.h"
13#include "jni/android_common/android_common.h" 14#include "jni/android_common/android_common.h"
14#include "jni/id_cache.h" 15#include "jni/id_cache.h"
16#include "native.h"
15 17
16std::unique_ptr<AndroidConfig> config; 18std::unique_ptr<AndroidConfig> global_config;
19std::unique_ptr<AndroidConfig> per_game_config;
17 20
18template <typename T> 21template <typename T>
19Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) { 22Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) {
20 auto key = GetJString(env, jkey); 23 auto key = GetJString(env, jkey);
21 auto basicSetting = Settings::values.linkage.by_key[key]; 24 auto basic_setting = Settings::values.linkage.by_key[key];
22 auto basicAndroidSetting = AndroidSettings::values.linkage.by_key[key]; 25 if (basic_setting != 0) {
23 if (basicSetting != 0) { 26 return static_cast<Settings::Setting<T>*>(basic_setting);
24 return static_cast<Settings::Setting<T>*>(basicSetting);
25 } 27 }
26 if (basicAndroidSetting != 0) { 28 auto basic_android_setting = AndroidSettings::values.linkage.by_key[key];
27 return static_cast<Settings::Setting<T>*>(basicAndroidSetting); 29 if (basic_android_setting != 0) {
30 return static_cast<Settings::Setting<T>*>(basic_android_setting);
28 } 31 }
29 LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key); 32 LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key);
30 return nullptr; 33 return nullptr;
@@ -32,35 +35,52 @@ Settings::Setting<T>* getSetting(JNIEnv* env, jstring jkey) {
32 35
33extern "C" { 36extern "C" {
34 37
35void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeConfig(JNIEnv* env, jobject obj) { 38void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializeGlobalConfig(JNIEnv* env, jobject obj) {
36 config = std::make_unique<AndroidConfig>(); 39 global_config = std::make_unique<AndroidConfig>();
37} 40}
38 41
39void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadConfig(JNIEnv* env, jobject obj) { 42void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadGlobalConfig(JNIEnv* env, jobject obj) {
40 config.reset(); 43 global_config.reset();
41} 44}
42 45
43void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_reloadSettings(JNIEnv* env, jobject obj) { 46void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_reloadGlobalConfig(JNIEnv* env, jobject obj) {
44 config->AndroidConfig::ReloadAllValues(); 47 global_config->AndroidConfig::ReloadAllValues();
45} 48}
46 49
47void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveSettings(JNIEnv* env, jobject obj) { 50void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveGlobalConfig(JNIEnv* env, jobject obj) {
48 config->AndroidConfig::SaveAllValues(); 51 global_config->AndroidConfig::SaveAllValues();
52}
53
54void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_initializePerGameConfig(JNIEnv* env, jobject obj,
55 jstring jprogramId,
56 jstring jfileName) {
57 auto program_id = EmulationSession::GetProgramId(env, jprogramId);
58 auto file_name = GetJString(env, jfileName);
59 const auto config_file_name = program_id == 0 ? file_name : fmt::format("{:016X}", program_id);
60 per_game_config =
61 std::make_unique<AndroidConfig>(config_file_name, Config::ConfigType::PerGameConfig);
62}
63
64jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_isPerGameConfigLoaded(JNIEnv* env,
65 jobject obj) {
66 return per_game_config != nullptr;
67}
68
69void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_savePerGameConfig(JNIEnv* env, jobject obj) {
70 per_game_config->AndroidConfig::SaveAllValues();
71}
72
73void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_unloadPerGameConfig(JNIEnv* env, jobject obj) {
74 per_game_config.reset();
49} 75}
50 76
51jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getBoolean(JNIEnv* env, jobject obj, 77jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getBoolean(JNIEnv* env, jobject obj,
52 jstring jkey, jboolean getDefault) { 78 jstring jkey, jboolean needGlobal) {
53 auto setting = getSetting<bool>(env, jkey); 79 auto setting = getSetting<bool>(env, jkey);
54 if (setting == nullptr) { 80 if (setting == nullptr) {
55 return false; 81 return false;
56 } 82 }
57 setting->SetGlobal(true); 83 return setting->GetValue(static_cast<bool>(needGlobal));
58
59 if (static_cast<bool>(getDefault)) {
60 return setting->GetDefault();
61 }
62
63 return setting->GetValue();
64} 84}
65 85
66void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setBoolean(JNIEnv* env, jobject obj, jstring jkey, 86void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setBoolean(JNIEnv* env, jobject obj, jstring jkey,
@@ -69,23 +89,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setBoolean(JNIEnv* env, jobject
69 if (setting == nullptr) { 89 if (setting == nullptr) {
70 return; 90 return;
71 } 91 }
72 setting->SetGlobal(true);
73 setting->SetValue(static_cast<bool>(value)); 92 setting->SetValue(static_cast<bool>(value));
74} 93}
75 94
76jbyte Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getByte(JNIEnv* env, jobject obj, jstring jkey, 95jbyte Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getByte(JNIEnv* env, jobject obj, jstring jkey,
77 jboolean getDefault) { 96 jboolean needGlobal) {
78 auto setting = getSetting<u8>(env, jkey); 97 auto setting = getSetting<u8>(env, jkey);
79 if (setting == nullptr) { 98 if (setting == nullptr) {
80 return -1; 99 return -1;
81 } 100 }
82 setting->SetGlobal(true); 101 return setting->GetValue(static_cast<bool>(needGlobal));
83
84 if (static_cast<bool>(getDefault)) {
85 return setting->GetDefault();
86 }
87
88 return setting->GetValue();
89} 102}
90 103
91void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setByte(JNIEnv* env, jobject obj, jstring jkey, 104void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setByte(JNIEnv* env, jobject obj, jstring jkey,
@@ -94,23 +107,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setByte(JNIEnv* env, jobject obj
94 if (setting == nullptr) { 107 if (setting == nullptr) {
95 return; 108 return;
96 } 109 }
97 setting->SetGlobal(true);
98 setting->SetValue(value); 110 setting->SetValue(value);
99} 111}
100 112
101jshort Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getShort(JNIEnv* env, jobject obj, jstring jkey, 113jshort Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getShort(JNIEnv* env, jobject obj, jstring jkey,
102 jboolean getDefault) { 114 jboolean needGlobal) {
103 auto setting = getSetting<u16>(env, jkey); 115 auto setting = getSetting<u16>(env, jkey);
104 if (setting == nullptr) { 116 if (setting == nullptr) {
105 return -1; 117 return -1;
106 } 118 }
107 setting->SetGlobal(true); 119 return setting->GetValue(static_cast<bool>(needGlobal));
108
109 if (static_cast<bool>(getDefault)) {
110 return setting->GetDefault();
111 }
112
113 return setting->GetValue();
114} 120}
115 121
116void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setShort(JNIEnv* env, jobject obj, jstring jkey, 122void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setShort(JNIEnv* env, jobject obj, jstring jkey,
@@ -119,23 +125,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setShort(JNIEnv* env, jobject ob
119 if (setting == nullptr) { 125 if (setting == nullptr) {
120 return; 126 return;
121 } 127 }
122 setting->SetGlobal(true);
123 setting->SetValue(value); 128 setting->SetValue(value);
124} 129}
125 130
126jint Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInt(JNIEnv* env, jobject obj, jstring jkey, 131jint Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInt(JNIEnv* env, jobject obj, jstring jkey,
127 jboolean getDefault) { 132 jboolean needGlobal) {
128 auto setting = getSetting<int>(env, jkey); 133 auto setting = getSetting<int>(env, jkey);
129 if (setting == nullptr) { 134 if (setting == nullptr) {
130 return -1; 135 return -1;
131 } 136 }
132 setting->SetGlobal(true); 137 return setting->GetValue(needGlobal);
133
134 if (static_cast<bool>(getDefault)) {
135 return setting->GetDefault();
136 }
137
138 return setting->GetValue();
139} 138}
140 139
141void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInt(JNIEnv* env, jobject obj, jstring jkey, 140void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInt(JNIEnv* env, jobject obj, jstring jkey,
@@ -144,23 +143,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInt(JNIEnv* env, jobject obj,
144 if (setting == nullptr) { 143 if (setting == nullptr) {
145 return; 144 return;
146 } 145 }
147 setting->SetGlobal(true);
148 setting->SetValue(value); 146 setting->SetValue(value);
149} 147}
150 148
151jfloat Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getFloat(JNIEnv* env, jobject obj, jstring jkey, 149jfloat Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getFloat(JNIEnv* env, jobject obj, jstring jkey,
152 jboolean getDefault) { 150 jboolean needGlobal) {
153 auto setting = getSetting<float>(env, jkey); 151 auto setting = getSetting<float>(env, jkey);
154 if (setting == nullptr) { 152 if (setting == nullptr) {
155 return -1; 153 return -1;
156 } 154 }
157 setting->SetGlobal(true); 155 return setting->GetValue(static_cast<bool>(needGlobal));
158
159 if (static_cast<bool>(getDefault)) {
160 return setting->GetDefault();
161 }
162
163 return setting->GetValue();
164} 156}
165 157
166void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setFloat(JNIEnv* env, jobject obj, jstring jkey, 158void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setFloat(JNIEnv* env, jobject obj, jstring jkey,
@@ -169,23 +161,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setFloat(JNIEnv* env, jobject ob
169 if (setting == nullptr) { 161 if (setting == nullptr) {
170 return; 162 return;
171 } 163 }
172 setting->SetGlobal(true);
173 setting->SetValue(value); 164 setting->SetValue(value);
174} 165}
175 166
176jlong Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getLong(JNIEnv* env, jobject obj, jstring jkey, 167jlong Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getLong(JNIEnv* env, jobject obj, jstring jkey,
177 jboolean getDefault) { 168 jboolean needGlobal) {
178 auto setting = getSetting<long>(env, jkey); 169 auto setting = getSetting<s64>(env, jkey);
179 if (setting == nullptr) { 170 if (setting == nullptr) {
180 return -1; 171 return -1;
181 } 172 }
182 setting->SetGlobal(true); 173 return setting->GetValue(static_cast<bool>(needGlobal));
183
184 if (static_cast<bool>(getDefault)) {
185 return setting->GetDefault();
186 }
187
188 return setting->GetValue();
189} 174}
190 175
191void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setLong(JNIEnv* env, jobject obj, jstring jkey, 176void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setLong(JNIEnv* env, jobject obj, jstring jkey,
@@ -194,23 +179,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setLong(JNIEnv* env, jobject obj
194 if (setting == nullptr) { 179 if (setting == nullptr) {
195 return; 180 return;
196 } 181 }
197 setting->SetGlobal(true);
198 setting->SetValue(value); 182 setting->SetValue(value);
199} 183}
200 184
201jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getString(JNIEnv* env, jobject obj, jstring jkey, 185jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getString(JNIEnv* env, jobject obj, jstring jkey,
202 jboolean getDefault) { 186 jboolean needGlobal) {
203 auto setting = getSetting<std::string>(env, jkey); 187 auto setting = getSetting<std::string>(env, jkey);
204 if (setting == nullptr) { 188 if (setting == nullptr) {
205 return ToJString(env, ""); 189 return ToJString(env, "");
206 } 190 }
207 setting->SetGlobal(true); 191 return ToJString(env, setting->GetValue(static_cast<bool>(needGlobal)));
208
209 if (static_cast<bool>(getDefault)) {
210 return ToJString(env, setting->GetDefault());
211 }
212
213 return ToJString(env, setting->GetValue());
214} 192}
215 193
216void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject obj, jstring jkey, 194void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject obj, jstring jkey,
@@ -220,27 +198,18 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject o
220 return; 198 return;
221 } 199 }
222 200
223 setting->SetGlobal(true);
224 setting->SetValue(GetJString(env, value)); 201 setting->SetValue(GetJString(env, value));
225} 202}
226 203
227jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsRuntimeModifiable(JNIEnv* env, jobject obj, 204jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsRuntimeModifiable(JNIEnv* env, jobject obj,
228 jstring jkey) { 205 jstring jkey) {
229 auto key = GetJString(env, jkey); 206 auto setting = getSetting<std::string>(env, jkey);
230 auto setting = Settings::values.linkage.by_key[key]; 207 if (setting != nullptr) {
231 if (setting != 0) {
232 return setting->RuntimeModfiable(); 208 return setting->RuntimeModfiable();
233 } 209 }
234 LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key);
235 return true; 210 return true;
236} 211}
237 212
238jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getConfigHeader(JNIEnv* env, jobject obj,
239 jint jcategory) {
240 auto category = static_cast<Settings::Category>(jcategory);
241 return ToJString(env, Settings::TranslateCategory(category));
242}
243
244jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* env, jobject obj, 213jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* env, jobject obj,
245 jstring jkey) { 214 jstring jkey) {
246 auto setting = getSetting<std::string>(env, jkey); 215 auto setting = getSetting<std::string>(env, jkey);
@@ -254,6 +223,50 @@ jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* e
254 return ToJString(env, setting->PairedSetting()->GetLabel()); 223 return ToJString(env, setting->PairedSetting()->GetLabel());
255} 224}
256 225
226jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsSwitchable(JNIEnv* env, jobject obj,
227 jstring jkey) {
228 auto setting = getSetting<std::string>(env, jkey);
229 if (setting != nullptr) {
230 return setting->Switchable();
231 }
232 return false;
233}
234
235jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_usingGlobal(JNIEnv* env, jobject obj,
236 jstring jkey) {
237 auto setting = getSetting<std::string>(env, jkey);
238 if (setting != nullptr) {
239 return setting->UsingGlobal();
240 }
241 return true;
242}
243
244void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setGlobal(JNIEnv* env, jobject obj, jstring jkey,
245 jboolean global) {
246 auto setting = getSetting<std::string>(env, jkey);
247 if (setting != nullptr) {
248 setting->SetGlobal(static_cast<bool>(global));
249 }
250}
251
252jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsSaveable(JNIEnv* env, jobject obj,
253 jstring jkey) {
254 auto setting = getSetting<std::string>(env, jkey);
255 if (setting != nullptr) {
256 return setting->Save();
257 }
258 return false;
259}
260
261jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getDefaultToString(JNIEnv* env, jobject obj,
262 jstring jkey) {
263 auto setting = getSetting<std::string>(env, jkey);
264 if (setting != nullptr) {
265 return ToJString(env, setting->DefaultToString());
266 }
267 return ToJString(env, "");
268}
269
257jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getGameDirs(JNIEnv* env, jobject obj) { 270jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getGameDirs(JNIEnv* env, jobject obj) {
258 jclass gameDirClass = IDCache::GetGameDirClass(); 271 jclass gameDirClass = IDCache::GetGameDirClass();
259 jmethodID gameDirConstructor = IDCache::GetGameDirConstructor(); 272 jmethodID gameDirConstructor = IDCache::GetGameDirConstructor();
@@ -305,4 +318,30 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_addGameDir(JNIEnv* env, jobject
305 AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)}); 318 AndroidSettings::GameDir{uriString, static_cast<bool>(jdeepScanBoolean)});
306} 319}
307 320
321jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getDisabledAddons(JNIEnv* env, jobject obj,
322 jstring jprogramId) {
323 auto program_id = EmulationSession::GetProgramId(env, jprogramId);
324 auto& disabledAddons = Settings::values.disabled_addons[program_id];
325 jobjectArray jdisabledAddonsArray =
326 env->NewObjectArray(disabledAddons.size(), IDCache::GetStringClass(), ToJString(env, ""));
327 for (size_t i = 0; i < disabledAddons.size(); ++i) {
328 env->SetObjectArrayElement(jdisabledAddonsArray, i, ToJString(env, disabledAddons[i]));
329 }
330 return jdisabledAddonsArray;
331}
332
333void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setDisabledAddons(JNIEnv* env, jobject obj,
334 jstring jprogramId,
335 jobjectArray jdisabledAddons) {
336 auto program_id = EmulationSession::GetProgramId(env, jprogramId);
337 Settings::values.disabled_addons[program_id].clear();
338 std::vector<std::string> disabled_addons;
339 const int size = env->GetArrayLength(jdisabledAddons);
340 for (int i = 0; i < size; ++i) {
341 auto jaddon = static_cast<jstring>(env->GetObjectArrayElement(jdisabledAddons, i));
342 disabled_addons.push_back(GetJString(env, jaddon));
343 }
344 Settings::values.disabled_addons[program_id] = disabled_addons;
345}
346
308} // extern "C" 347} // extern "C"
diff --git a/src/android/app/src/main/res/drawable/ic_save.xml b/src/android/app/src/main/res/drawable/ic_save.xml
index a9af3d9cf..5acc2bbab 100644
--- a/src/android/app/src/main/res/drawable/ic_save.xml
+++ b/src/android/app/src/main/res/drawable/ic_save.xml
@@ -1,10 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android" 1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp" 2 android:width="24dp"
3 android:height="24dp" 3 android:height="24dp"
4 android:viewportWidth="960" 4 android:viewportWidth="24"
5 android:viewportHeight="960" 5 android:viewportHeight="24">
6 android:tint="?attr/colorControlNormal">
7 <path 6 <path
8 android:fillColor="@android:color/white" 7 android:fillColor="?attr/colorControlNormal"
9 android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L647,120Q663,120 677.5,126Q692,132 703,143L817,257Q828,268 834,282.5Q840,297 840,313L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM760,314L646,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L760,760Q760,760 760,760Q760,760 760,760L760,314ZM480,720Q530,720 565,685Q600,650 600,600Q600,550 565,515Q530,480 480,480Q430,480 395,515Q360,550 360,600Q360,650 395,685Q430,720 480,720ZM280,400L560,400Q577,400 588.5,388.5Q600,377 600,360L600,280Q600,263 588.5,251.5Q577,240 560,240L280,240Q263,240 251.5,251.5Q240,263 240,280L240,360Q240,377 251.5,388.5Q263,400 280,400ZM200,314L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200L200,200L200,314Z"/> 8 android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z" />
10</vector> 9</vector>
diff --git a/src/android/app/src/main/res/layout-w1000dp/card_installable_icon.xml b/src/android/app/src/main/res/layout-w1000dp/card_installable_icon.xml
new file mode 100644
index 000000000..59ee1aad3
--- /dev/null
+++ b/src/android/app/src/main/res/layout-w1000dp/card_installable_icon.xml
@@ -0,0 +1,82 @@
1<?xml version="1.0" encoding="utf-8"?>
2<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 style="?attr/materialCardViewOutlinedStyle"
6 android:layout_width="match_parent"
7 android:layout_height="wrap_content"
8 android:layout_marginHorizontal="16dp"
9 android:layout_marginVertical="12dp">
10
11 <LinearLayout
12 android:layout_width="match_parent"
13 android:layout_height="wrap_content"
14 android:layout_gravity="center"
15 android:orientation="horizontal"
16 android:gravity="center_vertical"
17 android:paddingHorizontal="24dp"
18 android:paddingVertical="16dp">
19
20 <ImageView
21 android:id="@+id/icon"
22 android:layout_width="24dp"
23 android:layout_height="24dp"
24 android:layout_marginEnd="20dp"
25 android:layout_gravity="center_vertical"
26 app:tint="?attr/colorOnSurface"
27 tools:src="@drawable/ic_settings" />
28
29 <LinearLayout
30 android:layout_width="0dp"
31 android:layout_height="wrap_content"
32 android:layout_marginEnd="16dp"
33 android:layout_weight="1"
34 android:orientation="vertical">
35
36 <com.google.android.material.textview.MaterialTextView
37 android:id="@+id/title"
38 style="@style/TextAppearance.Material3.TitleMedium"
39 android:layout_width="match_parent"
40 android:layout_height="wrap_content"
41 android:text="@string/user_data"
42 android:textAlignment="viewStart" />
43
44 <com.google.android.material.textview.MaterialTextView
45 android:id="@+id/description"
46 style="@style/TextAppearance.Material3.BodyMedium"
47 android:layout_width="match_parent"
48 android:layout_height="wrap_content"
49 android:layout_marginTop="6dp"
50 android:text="@string/user_data_description"
51 android:textAlignment="viewStart" />
52
53 </LinearLayout>
54
55 <Button
56 android:id="@+id/button_export"
57 style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
58 android:layout_width="wrap_content"
59 android:layout_height="wrap_content"
60 android:layout_gravity="center_vertical"
61 android:contentDescription="@string/export"
62 android:tooltipText="@string/export"
63 android:visibility="gone"
64 app:icon="@drawable/ic_export"
65 tools:visibility="visible" />
66
67 <Button
68 android:id="@+id/button_install"
69 style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
70 android:layout_width="wrap_content"
71 android:layout_height="wrap_content"
72 android:layout_gravity="center_vertical"
73 android:layout_marginStart="12dp"
74 android:contentDescription="@string/string_import"
75 android:tooltipText="@string/string_import"
76 android:visibility="gone"
77 app:icon="@drawable/ic_import"
78 tools:visibility="visible" />
79
80 </LinearLayout>
81
82</com.google.android.material.card.MaterialCardView>
diff --git a/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml b/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml
new file mode 100644
index 000000000..0b9633855
--- /dev/null
+++ b/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml
@@ -0,0 +1,99 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent"
8 android:background="?attr/colorSurface">
9
10 <androidx.core.widget.NestedScrollView
11 android:id="@+id/list_all"
12 android:layout_width="0dp"
13 android:layout_height="match_parent"
14 android:clipToPadding="false"
15 android:fadeScrollbars="false"
16 android:scrollbars="vertical"
17 app:layout_constraintEnd_toEndOf="parent"
18 app:layout_constraintStart_toEndOf="@+id/icon_layout"
19 app:layout_constraintTop_toTopOf="parent">
20
21 <LinearLayout
22 android:id="@+id/layout_all"
23 android:layout_width="match_parent"
24 android:layout_height="wrap_content"
25 android:gravity="center_horizontal"
26 android:orientation="horizontal">
27
28 <androidx.recyclerview.widget.RecyclerView
29 android:id="@+id/list_properties"
30 android:layout_width="match_parent"
31 android:layout_height="match_parent"
32 tools:listitem="@layout/card_simple_outlined" />
33
34 </LinearLayout>
35
36 </androidx.core.widget.NestedScrollView>
37
38 <LinearLayout
39 android:id="@+id/icon_layout"
40 android:layout_width="wrap_content"
41 android:layout_height="wrap_content"
42 android:orientation="vertical"
43 app:layout_constraintStart_toStartOf="parent"
44 app:layout_constraintTop_toTopOf="parent">
45
46 <Button
47 android:id="@+id/button_back"
48 style="?attr/materialIconButtonStyle"
49 android:layout_width="wrap_content"
50 android:layout_height="wrap_content"
51 android:layout_gravity="start"
52 android:layout_margin="8dp"
53 app:icon="@drawable/ic_back"
54 app:iconSize="24dp"
55 app:iconTint="?attr/colorOnSurface" />
56
57 <com.google.android.material.card.MaterialCardView
58 style="?attr/materialCardViewElevatedStyle"
59 android:layout_width="wrap_content"
60 android:layout_height="wrap_content"
61 android:layout_marginHorizontal="16dp"
62 android:layout_marginTop="8dp"
63 app:cardCornerRadius="4dp"
64 app:cardElevation="4dp">
65
66 <ImageView
67 android:id="@+id/image_game_screen"
68 android:layout_width="175dp"
69 android:layout_height="175dp"
70 tools:src="@drawable/default_icon" />
71
72 </com.google.android.material.card.MaterialCardView>
73
74 <com.google.android.material.textview.MaterialTextView
75 android:id="@+id/title"
76 style="@style/TextAppearance.Material3.TitleMedium"
77 android:layout_width="match_parent"
78 android:layout_height="wrap_content"
79 android:layout_marginHorizontal="16dp"
80 android:layout_marginTop="12dp"
81 android:ellipsize="none"
82 android:marqueeRepeatLimit="marquee_forever"
83 android:requiresFadingEdge="horizontal"
84 android:singleLine="true"
85 android:textAlignment="center"
86 tools:text="deko_basic" />
87
88 </LinearLayout>
89
90 <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
91 android:id="@+id/button_start"
92 android:layout_width="wrap_content"
93 android:layout_height="wrap_content"
94 android:text="@string/start"
95 app:icon="@drawable/ic_play"
96 app:layout_constraintBottom_toBottomOf="parent"
97 app:layout_constraintEnd_toEndOf="parent" />
98
99</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/card_installable.xml b/src/android/app/src/main/res/layout/card_installable.xml
index f5b0e3741..ce2402d7a 100644
--- a/src/android/app/src/main/res/layout/card_installable.xml
+++ b/src/android/app/src/main/res/layout/card_installable.xml
@@ -11,7 +11,8 @@
11 <LinearLayout 11 <LinearLayout
12 android:layout_width="match_parent" 12 android:layout_width="match_parent"
13 android:layout_height="wrap_content" 13 android:layout_height="wrap_content"
14 android:layout_margin="16dp" 14 android:paddingVertical="16dp"
15 android:paddingHorizontal="24dp"
15 android:orientation="horizontal" 16 android:orientation="horizontal"
16 android:layout_gravity="center"> 17 android:layout_gravity="center">
17 18
diff --git a/src/android/app/src/main/res/layout/card_installable_icon.xml b/src/android/app/src/main/res/layout/card_installable_icon.xml
new file mode 100644
index 000000000..4ae5423b1
--- /dev/null
+++ b/src/android/app/src/main/res/layout/card_installable_icon.xml
@@ -0,0 +1,89 @@
1<?xml version="1.0" encoding="utf-8"?>
2<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 style="?attr/materialCardViewOutlinedStyle"
6 android:layout_width="match_parent"
7 android:layout_height="wrap_content"
8 android:layout_marginHorizontal="16dp"
9 android:layout_marginVertical="12dp">
10
11 <LinearLayout
12 android:layout_width="match_parent"
13 android:layout_height="wrap_content"
14 android:layout_gravity="center"
15 android:orientation="horizontal"
16 android:gravity="center_vertical"
17 android:paddingHorizontal="24dp"
18 android:paddingVertical="16dp">
19
20 <ImageView
21 android:id="@+id/icon"
22 android:layout_width="24dp"
23 android:layout_height="24dp"
24 android:layout_marginEnd="20dp"
25 android:layout_gravity="center_vertical"
26 app:tint="?attr/colorOnSurface"
27 tools:src="@drawable/ic_settings" />
28
29 <LinearLayout
30 android:layout_width="0dp"
31 android:layout_height="wrap_content"
32 android:layout_marginEnd="16dp"
33 android:layout_weight="1"
34 android:orientation="vertical">
35
36 <com.google.android.material.textview.MaterialTextView
37 android:id="@+id/title"
38 style="@style/TextAppearance.Material3.TitleMedium"
39 android:layout_width="match_parent"
40 android:layout_height="wrap_content"
41 android:text="@string/user_data"
42 android:textAlignment="viewStart" />
43
44 <com.google.android.material.textview.MaterialTextView
45 android:id="@+id/description"
46 style="@style/TextAppearance.Material3.BodyMedium"
47 android:layout_width="match_parent"
48 android:layout_height="wrap_content"
49 android:layout_marginTop="6dp"
50 android:text="@string/user_data_description"
51 android:textAlignment="viewStart" />
52
53 </LinearLayout>
54
55 <LinearLayout
56 android:layout_width="wrap_content"
57 android:layout_height="wrap_content"
58 android:orientation="vertical">
59
60 <Button
61 android:id="@+id/button_install"
62 style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
63 android:layout_width="wrap_content"
64 android:layout_height="wrap_content"
65 android:layout_gravity="center_vertical"
66 android:contentDescription="@string/string_import"
67 android:tooltipText="@string/string_import"
68 android:visibility="gone"
69 app:icon="@drawable/ic_import"
70 tools:visibility="visible" />
71
72 <Button
73 android:id="@+id/button_export"
74 style="@style/Widget.Material3.Button.IconButton.Filled.Tonal"
75 android:layout_width="wrap_content"
76 android:layout_height="wrap_content"
77 android:layout_gravity="center_vertical"
78 android:layout_marginTop="8dp"
79 android:contentDescription="@string/export"
80 android:tooltipText="@string/export"
81 android:visibility="gone"
82 app:icon="@drawable/ic_export"
83 tools:visibility="visible" />
84
85 </LinearLayout>
86
87 </LinearLayout>
88
89</com.google.android.material.card.MaterialCardView>
diff --git a/src/android/app/src/main/res/layout/card_applet_option.xml b/src/android/app/src/main/res/layout/card_simple_outlined.xml
index 19fbec9f1..b73930e7e 100644
--- a/src/android/app/src/main/res/layout/card_applet_option.xml
+++ b/src/android/app/src/main/res/layout/card_simple_outlined.xml
@@ -16,7 +16,8 @@
16 android:layout_height="wrap_content" 16 android:layout_height="wrap_content"
17 android:orientation="horizontal" 17 android:orientation="horizontal"
18 android:layout_gravity="center" 18 android:layout_gravity="center"
19 android:padding="24dp"> 19 android:paddingVertical="16dp"
20 android:paddingHorizontal="24dp">
20 21
21 <ImageView 22 <ImageView
22 android:id="@+id/icon" 23 android:id="@+id/icon"
@@ -50,6 +51,23 @@
50 android:textAlignment="viewStart" 51 android:textAlignment="viewStart"
51 tools:text="@string/applets_description" /> 52 tools:text="@string/applets_description" />
52 53
54 <com.google.android.material.textview.MaterialTextView
55 style="@style/TextAppearance.Material3.LabelMedium"
56 android:id="@+id/details"
57 android:layout_width="match_parent"
58 android:layout_height="wrap_content"
59 android:textAlignment="viewStart"
60 android:textSize="14sp"
61 android:textStyle="bold"
62 android:singleLine="true"
63 android:marqueeRepeatLimit="marquee_forever"
64 android:ellipsize="none"
65 android:requiresFadingEdge="horizontal"
66 android:layout_marginTop="6dp"
67 android:visibility="gone"
68 tools:visibility="visible"
69 tools:text="/tree/primary:Games" />
70
53 </LinearLayout> 71 </LinearLayout>
54 72
55 </LinearLayout> 73 </LinearLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_addons.xml b/src/android/app/src/main/res/layout/fragment_addons.xml
new file mode 100644
index 000000000..a25e82766
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_addons.xml
@@ -0,0 +1,47 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 android:id="@+id/coordinator_about"
5 android:layout_width="match_parent"
6 android:layout_height="match_parent"
7 android:background="?attr/colorSurface">
8
9 <com.google.android.material.appbar.AppBarLayout
10 android:id="@+id/appbar_addons"
11 android:layout_width="match_parent"
12 android:layout_height="wrap_content"
13 android:fitsSystemWindows="true"
14 app:layout_constraintEnd_toEndOf="parent"
15 app:layout_constraintStart_toStartOf="parent"
16 app:layout_constraintTop_toTopOf="parent">
17
18 <com.google.android.material.appbar.MaterialToolbar
19 android:id="@+id/toolbar_addons"
20 android:layout_width="match_parent"
21 android:layout_height="?attr/actionBarSize"
22 app:navigationIcon="@drawable/ic_back" />
23
24 </com.google.android.material.appbar.AppBarLayout>
25
26 <androidx.recyclerview.widget.RecyclerView
27 android:id="@+id/list_addons"
28 android:layout_width="match_parent"
29 android:layout_height="0dp"
30 android:clipToPadding="false"
31 app:layout_behavior="@string/appbar_scrolling_view_behavior"
32 app:layout_constraintBottom_toBottomOf="parent"
33 app:layout_constraintEnd_toEndOf="parent"
34 app:layout_constraintStart_toStartOf="parent"
35 app:layout_constraintTop_toBottomOf="@+id/appbar_addons" />
36
37 <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
38 android:id="@+id/button_install"
39 android:layout_width="wrap_content"
40 android:layout_height="wrap_content"
41 android:layout_gravity="bottom|end"
42 android:text="@string/install"
43 app:icon="@drawable/ic_add"
44 app:layout_constraintBottom_toBottomOf="parent"
45 app:layout_constraintEnd_toEndOf="parent" />
46
47</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_game_info.xml b/src/android/app/src/main/res/layout/fragment_game_info.xml
new file mode 100644
index 000000000..80ede8a8c
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_game_info.xml
@@ -0,0 +1,125 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:id="@+id/coordinator_about"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent"
8 android:background="?attr/colorSurface">
9
10 <com.google.android.material.appbar.AppBarLayout
11 android:id="@+id/appbar_info"
12 android:layout_width="match_parent"
13 android:layout_height="wrap_content"
14 android:fitsSystemWindows="true">
15
16 <com.google.android.material.appbar.MaterialToolbar
17 android:id="@+id/toolbar_info"
18 android:layout_width="match_parent"
19 android:layout_height="?attr/actionBarSize"
20 app:navigationIcon="@drawable/ic_back" />
21
22 </com.google.android.material.appbar.AppBarLayout>
23
24 <androidx.core.widget.NestedScrollView
25 android:id="@+id/scroll_info"
26 android:layout_width="match_parent"
27 android:layout_height="wrap_content"
28 app:layout_behavior="@string/appbar_scrolling_view_behavior">
29
30 <LinearLayout
31 android:id="@+id/content_info"
32 android:layout_width="match_parent"
33 android:layout_height="wrap_content"
34 android:orientation="vertical"
35 android:paddingHorizontal="16dp">
36
37 <com.google.android.material.textfield.TextInputLayout
38 android:id="@+id/path"
39 android:layout_width="match_parent"
40 android:layout_height="wrap_content"
41 android:paddingTop="16dp">
42
43 <com.google.android.material.textfield.TextInputEditText
44 android:id="@+id/path_field"
45 android:layout_width="match_parent"
46 android:layout_height="wrap_content"
47 android:editable="false"
48 android:importantForAutofill="no"
49 android:inputType="none"
50 android:minHeight="48dp"
51 android:textAlignment="viewStart"
52 tools:text="1.0.0" />
53
54 </com.google.android.material.textfield.TextInputLayout>
55
56 <com.google.android.material.textfield.TextInputLayout
57 android:id="@+id/program_id"
58 android:layout_width="match_parent"
59 android:layout_height="wrap_content"
60 android:paddingTop="16dp">
61
62 <com.google.android.material.textfield.TextInputEditText
63 android:id="@+id/program_id_field"
64 android:layout_width="match_parent"
65 android:layout_height="wrap_content"
66 android:editable="false"
67 android:importantForAutofill="no"
68 android:inputType="none"
69 android:minHeight="48dp"
70 android:textAlignment="viewStart"
71 tools:text="1.0.0" />
72
73 </com.google.android.material.textfield.TextInputLayout>
74
75 <com.google.android.material.textfield.TextInputLayout
76 android:id="@+id/developer"
77 android:layout_width="match_parent"
78 android:layout_height="wrap_content"
79 android:paddingTop="16dp">
80
81 <com.google.android.material.textfield.TextInputEditText
82 android:id="@+id/developer_field"
83 android:layout_width="match_parent"
84 android:layout_height="wrap_content"
85 android:editable="false"
86 android:importantForAutofill="no"
87 android:inputType="none"
88 android:minHeight="48dp"
89 android:textAlignment="viewStart"
90 tools:text="1.0.0" />
91
92 </com.google.android.material.textfield.TextInputLayout>
93
94 <com.google.android.material.textfield.TextInputLayout
95 android:id="@+id/version"
96 android:layout_width="match_parent"
97 android:layout_height="wrap_content"
98 android:paddingTop="16dp">
99
100 <com.google.android.material.textfield.TextInputEditText
101 android:id="@+id/version_field"
102 android:layout_width="match_parent"
103 android:layout_height="wrap_content"
104 android:editable="false"
105 android:importantForAutofill="no"
106 android:inputType="none"
107 android:minHeight="48dp"
108 android:textAlignment="viewStart"
109 tools:text="1.0.0" />
110
111 </com.google.android.material.textfield.TextInputLayout>
112
113 <com.google.android.material.button.MaterialButton
114 android:id="@+id/button_copy"
115 style="@style/Widget.Material3.Button"
116 android:layout_width="wrap_content"
117 android:layout_height="wrap_content"
118 android:layout_marginTop="16dp"
119 android:text="@string/copy_details" />
120
121 </LinearLayout>
122
123 </androidx.core.widget.NestedScrollView>
124
125</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/src/android/app/src/main/res/layout/fragment_game_properties.xml b/src/android/app/src/main/res/layout/fragment_game_properties.xml
new file mode 100644
index 000000000..72ecbde30
--- /dev/null
+++ b/src/android/app/src/main/res/layout/fragment_game_properties.xml
@@ -0,0 +1,86 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout
3 xmlns:android="http://schemas.android.com/apk/res/android"
4 xmlns:app="http://schemas.android.com/apk/res-auto"
5 xmlns:tools="http://schemas.android.com/tools"
6 android:layout_width="match_parent"
7 android:layout_height="match_parent"
8 android:background="?attr/colorSurface">
9
10 <androidx.core.widget.NestedScrollView
11 android:id="@+id/list_all"
12 android:layout_width="match_parent"
13 android:layout_height="match_parent"
14 android:scrollbars="vertical"
15 android:fadeScrollbars="false"
16 android:clipToPadding="false">
17
18 <LinearLayout
19 android:id="@+id/layout_all"
20 android:layout_width="match_parent"
21 android:layout_height="wrap_content"
22 android:orientation="vertical"
23 android:gravity="center_horizontal">
24
25 <Button
26 android:id="@+id/button_back"
27 style="?attr/materialIconButtonStyle"
28 android:layout_width="wrap_content"
29 android:layout_height="wrap_content"
30 android:layout_margin="8dp"
31 android:layout_gravity="start"
32 app:icon="@drawable/ic_back"
33 app:iconSize="24dp"
34 app:iconTint="?attr/colorOnSurface" />
35
36 <com.google.android.material.card.MaterialCardView
37 style="?attr/materialCardViewElevatedStyle"
38 android:layout_width="wrap_content"
39 android:layout_height="wrap_content"
40 android:layout_marginTop="8dp"
41 app:cardCornerRadius="4dp"
42 app:cardElevation="4dp">
43
44 <ImageView
45 android:id="@+id/image_game_screen"
46 android:layout_width="175dp"
47 android:layout_height="175dp"
48 tools:src="@drawable/default_icon"/>
49
50 </com.google.android.material.card.MaterialCardView>
51
52 <com.google.android.material.textview.MaterialTextView
53 android:id="@+id/title"
54 style="@style/TextAppearance.Material3.TitleMedium"
55 android:layout_width="wrap_content"
56 android:layout_height="wrap_content"
57 android:layout_marginTop="12dp"
58 android:layout_marginBottom="12dp"
59 android:layout_marginHorizontal="16dp"
60 android:ellipsize="none"
61 android:marqueeRepeatLimit="marquee_forever"
62 android:requiresFadingEdge="horizontal"
63 android:singleLine="true"
64 android:textAlignment="center"
65 tools:text="deko_basic" />
66
67 <androidx.recyclerview.widget.RecyclerView
68 android:id="@+id/list_properties"
69 android:layout_width="match_parent"
70 android:layout_height="match_parent"
71 tools:listitem="@layout/card_simple_outlined" />
72
73 </LinearLayout>
74
75 </androidx.core.widget.NestedScrollView>
76
77 <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
78 android:id="@+id/button_start"
79 android:layout_width="wrap_content"
80 android:layout_height="wrap_content"
81 android:text="@string/start"
82 app:icon="@drawable/ic_play"
83 app:layout_constraintBottom_toBottomOf="parent"
84 app:layout_constraintEnd_toEndOf="parent" />
85
86</androidx.constraintlayout.widget.ConstraintLayout>
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
new file mode 100644
index 000000000..74ca04ef1
--- /dev/null
+++ b/src/android/app/src/main/res/layout/list_item_addon.xml
@@ -0,0 +1,57 @@
1<?xml version="1.0" encoding="utf-8"?>
2<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:app="http://schemas.android.com/apk/res-auto"
4 xmlns:tools="http://schemas.android.com/tools"
5 android:id="@+id/addon_container"
6 android:layout_width="match_parent"
7 android:layout_height="wrap_content"
8 android:background="?attr/selectableItemBackground"
9 android:focusable="true"
10 android:paddingHorizontal="20dp"
11 android:paddingVertical="16dp">
12
13 <LinearLayout
14 android:id="@+id/text_container"
15 android:layout_width="0dp"
16 android:layout_height="wrap_content"
17 android:layout_marginEnd="16dp"
18 android:orientation="vertical"
19 app:layout_constraintBottom_toBottomOf="@+id/addon_switch"
20 app:layout_constraintEnd_toStartOf="@+id/addon_switch"
21 app:layout_constraintStart_toStartOf="parent"
22 app:layout_constraintTop_toTopOf="@+id/addon_switch">
23
24 <com.google.android.material.textview.MaterialTextView
25 android:id="@+id/title"
26 style="@style/TextAppearance.Material3.HeadlineMedium"
27 android:layout_width="wrap_content"
28 android:layout_height="wrap_content"
29 android:textAlignment="viewStart"
30 android:textSize="17sp"
31 app:lineHeight="28dp"
32 tools:text="1440p Resolution" />
33
34 <com.google.android.material.textview.MaterialTextView
35 android:id="@+id/version"
36 style="@style/TextAppearance.Material3.BodySmall"
37 android:layout_width="wrap_content"
38 android:layout_height="wrap_content"
39 android:layout_marginTop="@dimen/spacing_small"
40 android:textAlignment="viewStart"
41 tools:text="1.0.0" />
42
43 </LinearLayout>
44
45 <com.google.android.material.materialswitch.MaterialSwitch
46 android:id="@+id/addon_switch"
47 android:layout_width="wrap_content"
48 android:layout_height="wrap_content"
49 android:focusable="true"
50 android:gravity="center"
51 android:nextFocusLeft="@id/addon_container"
52 app:layout_constraintBottom_toBottomOf="parent"
53 app:layout_constraintEnd_toEndOf="parent"
54 app:layout_constraintStart_toEndOf="@id/text_container"
55 app:layout_constraintTop_toTopOf="parent" />
56
57</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/src/android/app/src/main/res/layout/list_item_setting.xml b/src/android/app/src/main/res/layout/list_item_setting.xml
index 544280e75..1f80682f1 100644
--- a/src/android/app/src/main/res/layout/list_item_setting.xml
+++ b/src/android/app/src/main/res/layout/list_item_setting.xml
@@ -62,6 +62,16 @@
62 android:textSize="13sp" 62 android:textSize="13sp"
63 tools:text="1x" /> 63 tools:text="1x" />
64 64
65 <com.google.android.material.button.MaterialButton
66 android:id="@+id/button_clear"
67 style="@style/Widget.Material3.Button.TonalButton"
68 android:layout_width="wrap_content"
69 android:layout_height="wrap_content"
70 android:layout_marginTop="16dp"
71 android:visibility="gone"
72 android:text="@string/clear"
73 tools:visibility="visible" />
74
65 </LinearLayout> 75 </LinearLayout>
66 76
67 </LinearLayout> 77 </LinearLayout>
diff --git a/src/android/app/src/main/res/layout/list_item_setting_switch.xml b/src/android/app/src/main/res/layout/list_item_setting_switch.xml
index a8f5aff78..5cb84182e 100644
--- a/src/android/app/src/main/res/layout/list_item_setting_switch.xml
+++ b/src/android/app/src/main/res/layout/list_item_setting_switch.xml
@@ -10,41 +10,62 @@
10 android:minHeight="72dp" 10 android:minHeight="72dp"
11 android:padding="16dp"> 11 android:padding="16dp">
12 12
13 <com.google.android.material.materialswitch.MaterialSwitch
14 android:id="@+id/switch_widget"
15 android:layout_width="wrap_content"
16 android:layout_height="wrap_content"
17 android:layout_alignParentEnd="true"
18 android:layout_centerVertical="true" />
19
20 <LinearLayout 13 <LinearLayout
21 android:layout_width="match_parent" 14 android:layout_width="match_parent"
22 android:layout_height="wrap_content" 15 android:layout_height="wrap_content"
23 android:layout_alignParentTop="true"
24 android:layout_centerVertical="true"
25 android:layout_marginEnd="24dp"
26 android:layout_toStartOf="@+id/switch_widget"
27 android:gravity="center_vertical"
28 android:orientation="vertical"> 16 android:orientation="vertical">
29 17
30 <com.google.android.material.textview.MaterialTextView 18 <LinearLayout
31 android:id="@+id/text_setting_name" 19 android:layout_width="match_parent"
32 style="@style/TextAppearance.Material3.HeadlineMedium"
33 android:layout_width="wrap_content"
34 android:layout_height="wrap_content" 20 android:layout_height="wrap_content"
35 android:textAlignment="viewStart" 21 android:orientation="horizontal">
36 android:textSize="17sp" 22
37 app:lineHeight="28dp" 23 <LinearLayout
38 tools:text="@string/frame_limit_enable" /> 24 android:layout_width="0dp"
39 25 android:layout_height="wrap_content"
40 <com.google.android.material.textview.MaterialTextView 26 android:layout_marginEnd="24dp"
41 android:id="@+id/text_setting_description" 27 android:gravity="center_vertical"
42 style="@style/TextAppearance.Material3.BodySmall" 28 android:orientation="vertical"
29 android:layout_weight="1">
30
31 <com.google.android.material.textview.MaterialTextView
32 android:id="@+id/text_setting_name"
33 style="@style/TextAppearance.Material3.HeadlineMedium"
34 android:layout_width="wrap_content"
35 android:layout_height="wrap_content"
36 android:textAlignment="viewStart"
37 android:textSize="17sp"
38 app:lineHeight="28dp"
39 tools:text="@string/frame_limit_enable" />
40
41 <com.google.android.material.textview.MaterialTextView
42 android:id="@+id/text_setting_description"
43 style="@style/TextAppearance.Material3.BodySmall"
44 android:layout_width="wrap_content"
45 android:layout_height="wrap_content"
46 android:layout_marginTop="@dimen/spacing_small"
47 android:textAlignment="viewStart"
48 tools:text="@string/frame_limit_enable_description" />
49
50 </LinearLayout>
51
52 <com.google.android.material.materialswitch.MaterialSwitch
53 android:id="@+id/switch_widget"
54 android:layout_width="wrap_content"
55 android:layout_height="wrap_content"
56 android:layout_gravity="center_vertical"/>
57
58 </LinearLayout>
59
60 <com.google.android.material.button.MaterialButton
61 android:id="@+id/button_clear"
62 style="@style/Widget.Material3.Button.TonalButton"
43 android:layout_width="wrap_content" 63 android:layout_width="wrap_content"
44 android:layout_height="wrap_content" 64 android:layout_height="wrap_content"
45 android:layout_marginTop="@dimen/spacing_small" 65 android:layout_marginTop="16dp"
46 android:textAlignment="viewStart" 66 android:text="@string/clear"
47 tools:text="@string/frame_limit_enable_description" /> 67 android:visibility="gone"
68 tools:visibility="visible" />
48 69
49 </LinearLayout> 70 </LinearLayout>
50 71
diff --git a/src/android/app/src/main/res/menu/menu_in_game.xml b/src/android/app/src/main/res/menu/menu_in_game.xml
index f98f727b6..ac6ab06ff 100644
--- a/src/android/app/src/main/res/menu/menu_in_game.xml
+++ b/src/android/app/src/main/res/menu/menu_in_game.xml
@@ -12,6 +12,11 @@
12 android:title="@string/preferences_settings" /> 12 android:title="@string/preferences_settings" />
13 13
14 <item 14 <item
15 android:id="@+id/menu_settings_per_game"
16 android:icon="@drawable/ic_settings_outline"
17 android:title="@string/per_game_settings" />
18
19 <item
15 android:id="@+id/menu_overlay_controls" 20 android:id="@+id/menu_overlay_controls"
16 android:icon="@drawable/ic_controller" 21 android:icon="@drawable/ic_controller"
17 android:title="@string/emulation_input_overlay" /> 22 android:title="@string/emulation_input_overlay" />
diff --git a/src/android/app/src/main/res/navigation/emulation_navigation.xml b/src/android/app/src/main/res/navigation/emulation_navigation.xml
index cfc494b3f..2f8c3fa0d 100644
--- a/src/android/app/src/main/res/navigation/emulation_navigation.xml
+++ b/src/android/app/src/main/res/navigation/emulation_navigation.xml
@@ -15,6 +15,10 @@
15 app:argType="org.yuzu.yuzu_emu.model.Game" 15 app:argType="org.yuzu.yuzu_emu.model.Game"
16 app:nullable="true" 16 app:nullable="true"
17 android:defaultValue="@null" /> 17 android:defaultValue="@null" />
18 <argument
19 android:name="custom"
20 app:argType="boolean"
21 android:defaultValue="false" />
18 </fragment> 22 </fragment>
19 23
20 <activity 24 <activity
diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml
index cf70b4bc4..37a03a8d1 100644
--- a/src/android/app/src/main/res/navigation/home_navigation.xml
+++ b/src/android/app/src/main/res/navigation/home_navigation.xml
@@ -77,6 +77,10 @@
77 app:argType="org.yuzu.yuzu_emu.model.Game" 77 app:argType="org.yuzu.yuzu_emu.model.Game"
78 app:nullable="true" 78 app:nullable="true"
79 android:defaultValue="@null" /> 79 android:defaultValue="@null" />
80 <argument
81 android:name="custom"
82 app:argType="boolean"
83 android:defaultValue="false" />
80 </activity> 84 </activity>
81 85
82 <action 86 <action
@@ -107,7 +111,13 @@
107 <fragment 111 <fragment
108 android:id="@+id/driverManagerFragment" 112 android:id="@+id/driverManagerFragment"
109 android:name="org.yuzu.yuzu_emu.fragments.DriverManagerFragment" 113 android:name="org.yuzu.yuzu_emu.fragments.DriverManagerFragment"
110 android:label="DriverManagerFragment" /> 114 android:label="DriverManagerFragment" >
115 <argument
116 android:name="game"
117 app:argType="org.yuzu.yuzu_emu.model.Game"
118 app:nullable="true"
119 android:defaultValue="@null" />
120 </fragment>
111 <fragment 121 <fragment
112 android:id="@+id/appletLauncherFragment" 122 android:id="@+id/appletLauncherFragment"
113 android:name="org.yuzu.yuzu_emu.fragments.AppletLauncherFragment" 123 android:name="org.yuzu.yuzu_emu.fragments.AppletLauncherFragment"
@@ -124,5 +134,41 @@
124 android:id="@+id/gameFoldersFragment" 134 android:id="@+id/gameFoldersFragment"
125 android:name="org.yuzu.yuzu_emu.fragments.GameFoldersFragment" 135 android:name="org.yuzu.yuzu_emu.fragments.GameFoldersFragment"
126 android:label="GameFoldersFragment" /> 136 android:label="GameFoldersFragment" />
137 <fragment
138 android:id="@+id/perGamePropertiesFragment"
139 android:name="org.yuzu.yuzu_emu.fragments.GamePropertiesFragment"
140 android:label="PerGamePropertiesFragment" >
141 <argument
142 android:name="game"
143 app:argType="org.yuzu.yuzu_emu.model.Game" />
144 <action
145 android:id="@+id/action_perGamePropertiesFragment_to_gameInfoFragment"
146 app:destination="@id/gameInfoFragment" />
147 <action
148 android:id="@+id/action_perGamePropertiesFragment_to_addonsFragment"
149 app:destination="@id/addonsFragment" />
150 <action
151 android:id="@+id/action_perGamePropertiesFragment_to_driverManagerFragment"
152 app:destination="@id/driverManagerFragment" />
153 </fragment>
154 <action
155 android:id="@+id/action_global_perGamePropertiesFragment"
156 app:destination="@id/perGamePropertiesFragment" />
157 <fragment
158 android:id="@+id/gameInfoFragment"
159 android:name="org.yuzu.yuzu_emu.fragments.GameInfoFragment"
160 android:label="GameInfoFragment" >
161 <argument
162 android:name="game"
163 app:argType="org.yuzu.yuzu_emu.model.Game" />
164 </fragment>
165 <fragment
166 android:id="@+id/addonsFragment"
167 android:name="org.yuzu.yuzu_emu.fragments.AddonsFragment"
168 android:label="AddonsFragment" >
169 <argument
170 android:name="game"
171 app:argType="org.yuzu.yuzu_emu.model.Game" />
172 </fragment>
127 173
128</navigation> 174</navigation>
diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml
index 380d14213..992b5ae44 100644
--- a/src/android/app/src/main/res/values/dimens.xml
+++ b/src/android/app/src/main/res/values/dimens.xml
@@ -13,7 +13,7 @@
13 <dimen name="menu_width">256dp</dimen> 13 <dimen name="menu_width">256dp</dimen>
14 <dimen name="card_width">165dp</dimen> 14 <dimen name="card_width">165dp</dimen>
15 <dimen name="icon_inset">24dp</dimen> 15 <dimen name="icon_inset">24dp</dimen>
16 <dimen name="spacing_bottom_list_fab">76dp</dimen> 16 <dimen name="spacing_bottom_list_fab">96dp</dimen>
17 <dimen name="spacing_fab">24dp</dimen> 17 <dimen name="spacing_fab">24dp</dimen>
18 18
19 <dimen name="dialog_margin">20dp</dimen> 19 <dimen name="dialog_margin">20dp</dimen>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index a6ccef8a1..c86c43df2 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -91,7 +91,10 @@
91 <string name="notification_no_directory_link_description">Please locate the user folder with the file manager\'s side panel manually.</string> 91 <string name="notification_no_directory_link_description">Please locate the user folder with the file manager\'s side panel manually.</string>
92 <string name="manage_save_data">Manage save data</string> 92 <string name="manage_save_data">Manage save data</string>
93 <string name="manage_save_data_description">Save data found. Please select an option below.</string> 93 <string name="manage_save_data_description">Save data found. Please select an option below.</string>
94 <string name="import_save_warning">Import save data</string>
95 <string name="import_save_warning_description">This will overwrite all existing save data with the provided file. Are you sure that you want to continue?</string>
94 <string name="import_export_saves_description">Import or export save files</string> 96 <string name="import_export_saves_description">Import or export save files</string>
97 <string name="save_files_importing">Importing save files…</string>
95 <string name="save_files_exporting">Exporting save files…</string> 98 <string name="save_files_exporting">Exporting save files…</string>
96 <string name="save_file_imported_success">Imported successfully</string> 99 <string name="save_file_imported_success">Imported successfully</string>
97 <string name="save_file_invalid_zip_structure">Invalid save directory structure</string> 100 <string name="save_file_invalid_zip_structure">Invalid save directory structure</string>
@@ -266,6 +269,11 @@
266 <string name="delete">Delete</string> 269 <string name="delete">Delete</string>
267 <string name="edit">Edit</string> 270 <string name="edit">Edit</string>
268 <string name="export_success">Exported successfully</string> 271 <string name="export_success">Exported successfully</string>
272 <string name="start">Start</string>
273 <string name="clear">Clear</string>
274 <string name="global">Global</string>
275 <string name="custom">Custom</string>
276 <string name="notice">Notice</string>
269 277
270 <!-- GPU driver installation --> 278 <!-- GPU driver installation -->
271 <string name="select_gpu_driver">Select GPU driver</string> 279 <string name="select_gpu_driver">Select GPU driver</string>
@@ -291,6 +299,44 @@
291 <string name="preferences_debug">Debug</string> 299 <string name="preferences_debug">Debug</string>
292 <string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string> 300 <string name="preferences_debug_description">CPU/GPU debugging, graphics API, fastmem</string>
293 301
302 <!-- Game properties -->
303 <string name="info">Info</string>
304 <string name="info_description">Program ID, developer, version</string>
305 <string name="per_game_settings">Per-game settings</string>
306 <string name="per_game_settings_description">Edit settings specific to this game</string>
307 <string name="launch_options">Launch config</string>
308 <string name="path">Path</string>
309 <string name="program_id">Program ID</string>
310 <string name="developer">Developer</string>
311 <string name="version">Version</string>
312 <string name="copy_details">Copy details</string>
313 <string name="add_ons">Add-ons</string>
314 <string name="add_ons_description">Toggle mods, updates and DLC</string>
315 <string name="clear_shader_cache">Clear shader cache</string>
316 <string name="clear_shader_cache_description">Removes all shaders built while playing this game</string>
317 <string name="clear_shader_cache_warning_description">You will experience more stuttering as the shader cache regenerates</string>
318 <string name="cleared_shaders_successfully">Cleared shaders successfully</string>
319 <string name="addons_game">Addons: %1$s</string>
320 <string name="save_data">Save data</string>
321 <string name="save_data_description">Manage save data specific to this game</string>
322 <string name="delete_save_data">Delete save data</string>
323 <string name="delete_save_data_description">Removes all save data specific to this game</string>
324 <string name="delete_save_data_warning_description">This irrecoverably removes all of this game\'s save data. Are you sure you want to continue?</string>
325 <string name="save_data_deleted_successfully">Save data deleted successfully</string>
326 <string name="select_content_type">Content type</string>
327 <string name="updates_and_dlc">Updates and DLC</string>
328 <string name="mods_and_cheats">Mods and cheats</string>
329 <string name="addon_notice">Important addon notice</string>
330 <!-- "cheats/" "romfs/" and "exefs/ should not be translated -->
331 <string name="addon_notice_description">In order to install mods and cheats, you must select a folder that contains a cheats/, romfs/, or exefs/ directory. We can\'t verify if these will be compatible with your game so be careful!</string>
332 <string name="invalid_directory">Invalid directory</string>
333 <!-- "cheats/" "romfs/" and "exefs/ should not be translated -->
334 <string name="invalid_directory_description">Please make sure that the directory you selected contains a cheats/, romfs/, or exefs/ folder and try again.</string>
335 <string name="addon_installed_successfully">Addon installed successfully</string>
336 <string name="verifying_content">Verifying content…</string>
337 <string name="content_install_notice">Content install notice</string>
338 <string name="content_install_notice_description">The content that you selected does not match this game.\nInstall anyway?</string>
339
294 <!-- ROM loading errors --> 340 <!-- ROM loading errors -->
295 <string name="loader_error_encrypted">Your ROM is encrypted</string> 341 <string name="loader_error_encrypted">Your ROM is encrypted</string>
296 <string name="loader_error_encrypted_roms_description"><![CDATA[Please follow the guides to redump your <a href="https://yuzu-emu.org/help/quickstart/#dumping-physical-titles-game-cards">game cartidges</a> or <a href="https://yuzu-emu.org/help/quickstart/#dumping-digital-titles-eshop">installed titles</a>.]]></string> 342 <string name="loader_error_encrypted_roms_description"><![CDATA[Please follow the guides to redump your <a href="https://yuzu-emu.org/help/quickstart/#dumping-physical-titles-game-cards">game cartidges</a> or <a href="https://yuzu-emu.org/help/quickstart/#dumping-digital-titles-eshop">installed titles</a>.]]></string>
@@ -369,6 +415,7 @@
369 415
370 <!-- Memory Sizes --> 416 <!-- Memory Sizes -->
371 <string name="memory_byte">Byte</string> 417 <string name="memory_byte">Byte</string>
418 <string name="memory_byte_shorthand">B</string>
372 <string name="memory_kilobyte">KB</string> 419 <string name="memory_kilobyte">KB</string>
373 <string name="memory_megabyte">MB</string> 420 <string name="memory_megabyte">MB</string>
374 <string name="memory_gigabyte">GB</string> 421 <string name="memory_gigabyte">GB</string>
diff --git a/src/common/settings.cpp b/src/common/settings.cpp
index 88f509ba7..ea52bbfa6 100644
--- a/src/common/settings.cpp
+++ b/src/common/settings.cpp
@@ -211,6 +211,8 @@ const char* TranslateCategory(Category category) {
211 case Category::Debugging: 211 case Category::Debugging:
212 case Category::DebuggingGraphics: 212 case Category::DebuggingGraphics:
213 return "Debugging"; 213 return "Debugging";
214 case Category::GpuDriver:
215 return "GpuDriver";
214 case Category::Miscellaneous: 216 case Category::Miscellaneous:
215 return "Miscellaneous"; 217 return "Miscellaneous";
216 case Category::Network: 218 case Category::Network:
diff --git a/src/common/settings.h b/src/common/settings.h
index 7dc18fffe..07dba53ab 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -197,7 +197,7 @@ struct Values {
197 SwitchableSetting<CpuAccuracy, true> cpu_accuracy{linkage, CpuAccuracy::Auto, 197 SwitchableSetting<CpuAccuracy, true> cpu_accuracy{linkage, CpuAccuracy::Auto,
198 CpuAccuracy::Auto, CpuAccuracy::Paranoid, 198 CpuAccuracy::Auto, CpuAccuracy::Paranoid,
199 "cpu_accuracy", Category::Cpu}; 199 "cpu_accuracy", Category::Cpu};
200 Setting<bool> cpu_debug_mode{linkage, false, "cpu_debug_mode", Category::CpuDebug}; 200 SwitchableSetting<bool> cpu_debug_mode{linkage, false, "cpu_debug_mode", Category::CpuDebug};
201 201
202 Setting<bool> cpuopt_page_tables{linkage, true, "cpuopt_page_tables", Category::CpuDebug}; 202 Setting<bool> cpuopt_page_tables{linkage, true, "cpuopt_page_tables", Category::CpuDebug};
203 Setting<bool> cpuopt_block_linking{linkage, true, "cpuopt_block_linking", Category::CpuDebug}; 203 Setting<bool> cpuopt_block_linking{linkage, true, "cpuopt_block_linking", Category::CpuDebug};
@@ -211,9 +211,9 @@ struct Values {
211 Setting<bool> cpuopt_misc_ir{linkage, true, "cpuopt_misc_ir", Category::CpuDebug}; 211 Setting<bool> cpuopt_misc_ir{linkage, true, "cpuopt_misc_ir", Category::CpuDebug};
212 Setting<bool> cpuopt_reduce_misalign_checks{linkage, true, "cpuopt_reduce_misalign_checks", 212 Setting<bool> cpuopt_reduce_misalign_checks{linkage, true, "cpuopt_reduce_misalign_checks",
213 Category::CpuDebug}; 213 Category::CpuDebug};
214 Setting<bool> cpuopt_fastmem{linkage, true, "cpuopt_fastmem", Category::CpuDebug}; 214 SwitchableSetting<bool> cpuopt_fastmem{linkage, true, "cpuopt_fastmem", Category::CpuDebug};
215 Setting<bool> cpuopt_fastmem_exclusives{linkage, true, "cpuopt_fastmem_exclusives", 215 SwitchableSetting<bool> cpuopt_fastmem_exclusives{linkage, true, "cpuopt_fastmem_exclusives",
216 Category::CpuDebug}; 216 Category::CpuDebug};
217 Setting<bool> cpuopt_recompile_exclusives{linkage, true, "cpuopt_recompile_exclusives", 217 Setting<bool> cpuopt_recompile_exclusives{linkage, true, "cpuopt_recompile_exclusives",
218 Category::CpuDebug}; 218 Category::CpuDebug};
219 Setting<bool> cpuopt_ignore_memory_aborts{linkage, true, "cpuopt_ignore_memory_aborts", 219 Setting<bool> cpuopt_ignore_memory_aborts{linkage, true, "cpuopt_ignore_memory_aborts",
@@ -256,7 +256,7 @@ struct Values {
256 AstcDecodeMode::CpuAsynchronous, 256 AstcDecodeMode::CpuAsynchronous,
257 "accelerate_astc", 257 "accelerate_astc",
258 Category::Renderer}; 258 Category::Renderer};
259 Setting<VSyncMode, true> vsync_mode{ 259 SwitchableSetting<VSyncMode, true> vsync_mode{
260 linkage, VSyncMode::Fifo, VSyncMode::Immediate, VSyncMode::FifoRelaxed, 260 linkage, VSyncMode::Fifo, VSyncMode::Immediate, VSyncMode::FifoRelaxed,
261 "use_vsync", Category::Renderer, Specialization::RuntimeList, true, 261 "use_vsync", Category::Renderer, Specialization::RuntimeList, true,
262 true}; 262 true};
diff --git a/src/common/settings_common.h b/src/common/settings_common.h
index 344c04439..c82e17495 100644
--- a/src/common/settings_common.h
+++ b/src/common/settings_common.h
@@ -26,6 +26,7 @@ enum class Category : u32 {
26 DataStorage, 26 DataStorage,
27 Debugging, 27 Debugging,
28 DebuggingGraphics, 28 DebuggingGraphics,
29 GpuDriver,
29 Miscellaneous, 30 Miscellaneous,
30 Network, 31 Network,
31 WebService, 32 WebService,
diff --git a/src/common/settings_setting.h b/src/common/settings_setting.h
index 3175ab07d..0b18ca5ec 100644
--- a/src/common/settings_setting.h
+++ b/src/common/settings_setting.h
@@ -81,6 +81,9 @@ public:
81 [[nodiscard]] virtual const Type& GetValue() const { 81 [[nodiscard]] virtual const Type& GetValue() const {
82 return value; 82 return value;
83 } 83 }
84 [[nodiscard]] virtual const Type& GetValue(bool need_global) const {
85 return value;
86 }
84 87
85 /** 88 /**
86 * Sets the setting to the given value. 89 * Sets the setting to the given value.
@@ -353,7 +356,7 @@ public:
353 } 356 }
354 return custom; 357 return custom;
355 } 358 }
356 [[nodiscard]] const Type& GetValue(bool need_global) const { 359 [[nodiscard]] const Type& GetValue(bool need_global) const override final {
357 if (use_global || need_global) { 360 if (use_global || need_global) {
358 return this->value; 361 return this->value;
359 } 362 }
diff --git a/src/core/hle/service/nvnflinger/fb_share_buffer_manager.cpp b/src/core/hle/service/nvnflinger/fb_share_buffer_manager.cpp
index 75bf31e32..2fef6cc1a 100644
--- a/src/core/hle/service/nvnflinger/fb_share_buffer_manager.cpp
+++ b/src/core/hle/service/nvnflinger/fb_share_buffer_manager.cpp
@@ -204,8 +204,9 @@ Result FbShareBufferManager::Initialize(u64* out_buffer_id, u64* out_layer_id, u
204 // Record the display id. 204 // Record the display id.
205 m_display_id = display_id; 205 m_display_id = display_id;
206 206
207 // Create a layer for the display. 207 // Create and open a layer for the display.
208 m_layer_id = m_flinger.CreateLayer(m_display_id).value(); 208 m_layer_id = m_flinger.CreateLayer(m_display_id).value();
209 m_flinger.OpenLayer(m_layer_id);
209 210
210 // Set up the buffer. 211 // Set up the buffer.
211 m_buffer_id = m_next_buffer_id++; 212 m_buffer_id = m_next_buffer_id++;
diff --git a/src/core/hle/service/nvnflinger/nvnflinger.cpp b/src/core/hle/service/nvnflinger/nvnflinger.cpp
index 0745434c5..6352b09a9 100644
--- a/src/core/hle/service/nvnflinger/nvnflinger.cpp
+++ b/src/core/hle/service/nvnflinger/nvnflinger.cpp
@@ -176,17 +176,37 @@ void Nvnflinger::CreateLayerAtId(VI::Display& display, u64 layer_id) {
176 display.CreateLayer(layer_id, buffer_id, nvdrv->container); 176 display.CreateLayer(layer_id, buffer_id, nvdrv->container);
177} 177}
178 178
179void Nvnflinger::OpenLayer(u64 layer_id) {
180 const auto lock_guard = Lock();
181
182 for (auto& display : displays) {
183 if (auto* layer = display.FindLayer(layer_id); layer) {
184 layer->Open();
185 }
186 }
187}
188
179void Nvnflinger::CloseLayer(u64 layer_id) { 189void Nvnflinger::CloseLayer(u64 layer_id) {
180 const auto lock_guard = Lock(); 190 const auto lock_guard = Lock();
181 191
182 for (auto& display : displays) { 192 for (auto& display : displays) {
183 display.CloseLayer(layer_id); 193 if (auto* layer = display.FindLayer(layer_id); layer) {
194 layer->Close();
195 }
196 }
197}
198
199void Nvnflinger::DestroyLayer(u64 layer_id) {
200 const auto lock_guard = Lock();
201
202 for (auto& display : displays) {
203 display.DestroyLayer(layer_id);
184 } 204 }
185} 205}
186 206
187std::optional<u32> Nvnflinger::FindBufferQueueId(u64 display_id, u64 layer_id) { 207std::optional<u32> Nvnflinger::FindBufferQueueId(u64 display_id, u64 layer_id) {
188 const auto lock_guard = Lock(); 208 const auto lock_guard = Lock();
189 const auto* const layer = FindOrCreateLayer(display_id, layer_id); 209 const auto* const layer = FindLayer(display_id, layer_id);
190 210
191 if (layer == nullptr) { 211 if (layer == nullptr) {
192 return std::nullopt; 212 return std::nullopt;
@@ -240,24 +260,6 @@ VI::Layer* Nvnflinger::FindLayer(u64 display_id, u64 layer_id) {
240 return display->FindLayer(layer_id); 260 return display->FindLayer(layer_id);
241} 261}
242 262
243VI::Layer* Nvnflinger::FindOrCreateLayer(u64 display_id, u64 layer_id) {
244 auto* const display = FindDisplay(display_id);
245
246 if (display == nullptr) {
247 return nullptr;
248 }
249
250 auto* layer = display->FindLayer(layer_id);
251
252 if (layer == nullptr) {
253 LOG_DEBUG(Service_Nvnflinger, "Layer at id {} not found. Trying to create it.", layer_id);
254 CreateLayerAtId(*display, layer_id);
255 return display->FindLayer(layer_id);
256 }
257
258 return layer;
259}
260
261void Nvnflinger::Compose() { 263void Nvnflinger::Compose() {
262 for (auto& display : displays) { 264 for (auto& display : displays) {
263 // Trigger vsync for this display at the end of drawing 265 // Trigger vsync for this display at the end of drawing
diff --git a/src/core/hle/service/nvnflinger/nvnflinger.h b/src/core/hle/service/nvnflinger/nvnflinger.h
index f5d73acdb..871285764 100644
--- a/src/core/hle/service/nvnflinger/nvnflinger.h
+++ b/src/core/hle/service/nvnflinger/nvnflinger.h
@@ -73,9 +73,15 @@ public:
73 /// If an invalid display ID is specified, then an empty optional is returned. 73 /// If an invalid display ID is specified, then an empty optional is returned.
74 [[nodiscard]] std::optional<u64> CreateLayer(u64 display_id); 74 [[nodiscard]] std::optional<u64> CreateLayer(u64 display_id);
75 75
76 /// Opens a layer on all displays for the given layer ID.
77 void OpenLayer(u64 layer_id);
78
76 /// Closes a layer on all displays for the given layer ID. 79 /// Closes a layer on all displays for the given layer ID.
77 void CloseLayer(u64 layer_id); 80 void CloseLayer(u64 layer_id);
78 81
82 /// Destroys the given layer ID.
83 void DestroyLayer(u64 layer_id);
84
79 /// Finds the buffer queue ID of the specified layer in the specified display. 85 /// Finds the buffer queue ID of the specified layer in the specified display.
80 /// 86 ///
81 /// If an invalid display ID or layer ID is provided, then an empty optional is returned. 87 /// If an invalid display ID or layer ID is provided, then an empty optional is returned.
@@ -117,11 +123,6 @@ private:
117 /// Finds the layer identified by the specified ID in the desired display. 123 /// Finds the layer identified by the specified ID in the desired display.
118 [[nodiscard]] VI::Layer* FindLayer(u64 display_id, u64 layer_id); 124 [[nodiscard]] VI::Layer* FindLayer(u64 display_id, u64 layer_id);
119 125
120 /// Finds the layer identified by the specified ID in the desired display,
121 /// or creates the layer if it is not found.
122 /// To be used when the system expects the specified ID to already exist.
123 [[nodiscard]] VI::Layer* FindOrCreateLayer(u64 display_id, u64 layer_id);
124
125 /// Creates a layer with the specified layer ID in the desired display. 126 /// Creates a layer with the specified layer ID in the desired display.
126 void CreateLayerAtId(VI::Display& display, u64 layer_id); 127 void CreateLayerAtId(VI::Display& display, u64 layer_id);
127 128
diff --git a/src/core/hle/service/vi/display/vi_display.cpp b/src/core/hle/service/vi/display/vi_display.cpp
index d30f49877..71ce9be50 100644
--- a/src/core/hle/service/vi/display/vi_display.cpp
+++ b/src/core/hle/service/vi/display/vi_display.cpp
@@ -51,11 +51,24 @@ Display::~Display() {
51} 51}
52 52
53Layer& Display::GetLayer(std::size_t index) { 53Layer& Display::GetLayer(std::size_t index) {
54 return *layers.at(index); 54 size_t i = 0;
55 for (auto& layer : layers) {
56 if (!layer->IsOpen()) {
57 continue;
58 }
59
60 if (i == index) {
61 return *layer;
62 }
63
64 i++;
65 }
66
67 UNREACHABLE();
55} 68}
56 69
57const Layer& Display::GetLayer(std::size_t index) const { 70size_t Display::GetNumLayers() const {
58 return *layers.at(index); 71 return std::ranges::count_if(layers, [](auto& l) { return l->IsOpen(); });
59} 72}
60 73
61Result Display::GetVSyncEvent(Kernel::KReadableEvent** out_vsync_event) { 74Result Display::GetVSyncEvent(Kernel::KReadableEvent** out_vsync_event) {
@@ -92,7 +105,11 @@ void Display::CreateLayer(u64 layer_id, u32 binder_id,
92 hos_binder_driver_server.RegisterProducer(std::move(producer)); 105 hos_binder_driver_server.RegisterProducer(std::move(producer));
93} 106}
94 107
95void Display::CloseLayer(u64 layer_id) { 108void Display::DestroyLayer(u64 layer_id) {
109 if (auto* layer = this->FindLayer(layer_id); layer != nullptr) {
110 layer->GetConsumer().Abandon();
111 }
112
96 std::erase_if(layers, 113 std::erase_if(layers,
97 [layer_id](const auto& layer) { return layer->GetLayerId() == layer_id; }); 114 [layer_id](const auto& layer) { return layer->GetLayerId() == layer_id; });
98} 115}
diff --git a/src/core/hle/service/vi/display/vi_display.h b/src/core/hle/service/vi/display/vi_display.h
index 101cbce20..1d9360b96 100644
--- a/src/core/hle/service/vi/display/vi_display.h
+++ b/src/core/hle/service/vi/display/vi_display.h
@@ -66,18 +66,13 @@ public:
66 66
67 /// Whether or not this display has any layers added to it. 67 /// Whether or not this display has any layers added to it.
68 bool HasLayers() const { 68 bool HasLayers() const {
69 return !layers.empty(); 69 return GetNumLayers() > 0;
70 } 70 }
71 71
72 /// Gets a layer for this display based off an index. 72 /// Gets a layer for this display based off an index.
73 Layer& GetLayer(std::size_t index); 73 Layer& GetLayer(std::size_t index);
74 74
75 /// Gets a layer for this display based off an index. 75 std::size_t GetNumLayers() const;
76 const Layer& GetLayer(std::size_t index) const;
77
78 std::size_t GetNumLayers() const {
79 return layers.size();
80 }
81 76
82 /** 77 /**
83 * Gets the internal vsync event. 78 * Gets the internal vsync event.
@@ -100,11 +95,11 @@ public:
100 /// 95 ///
101 void CreateLayer(u64 layer_id, u32 binder_id, Service::Nvidia::NvCore::Container& core); 96 void CreateLayer(u64 layer_id, u32 binder_id, Service::Nvidia::NvCore::Container& core);
102 97
103 /// Closes and removes a layer from this display with the given ID. 98 /// Removes a layer from this display with the given ID.
104 /// 99 ///
105 /// @param layer_id The ID assigned to the layer to close. 100 /// @param layer_id The ID assigned to the layer to destroy.
106 /// 101 ///
107 void CloseLayer(u64 layer_id); 102 void DestroyLayer(u64 layer_id);
108 103
109 /// Resets the display for a new connection. 104 /// Resets the display for a new connection.
110 void Reset() { 105 void Reset() {
diff --git a/src/core/hle/service/vi/layer/vi_layer.cpp b/src/core/hle/service/vi/layer/vi_layer.cpp
index 9ae2e0e44..04e52a23b 100644
--- a/src/core/hle/service/vi/layer/vi_layer.cpp
+++ b/src/core/hle/service/vi/layer/vi_layer.cpp
@@ -8,8 +8,8 @@ namespace Service::VI {
8Layer::Layer(u64 layer_id_, u32 binder_id_, android::BufferQueueCore& core_, 8Layer::Layer(u64 layer_id_, u32 binder_id_, android::BufferQueueCore& core_,
9 android::BufferQueueProducer& binder_, 9 android::BufferQueueProducer& binder_,
10 std::shared_ptr<android::BufferItemConsumer>&& consumer_) 10 std::shared_ptr<android::BufferItemConsumer>&& consumer_)
11 : layer_id{layer_id_}, binder_id{binder_id_}, core{core_}, binder{binder_}, consumer{std::move( 11 : layer_id{layer_id_}, binder_id{binder_id_}, core{core_}, binder{binder_},
12 consumer_)} {} 12 consumer{std::move(consumer_)}, open{false} {}
13 13
14Layer::~Layer() = default; 14Layer::~Layer() = default;
15 15
diff --git a/src/core/hle/service/vi/layer/vi_layer.h b/src/core/hle/service/vi/layer/vi_layer.h
index 8cf1b5275..295005e23 100644
--- a/src/core/hle/service/vi/layer/vi_layer.h
+++ b/src/core/hle/service/vi/layer/vi_layer.h
@@ -71,12 +71,25 @@ public:
71 return core; 71 return core;
72 } 72 }
73 73
74 bool IsOpen() const {
75 return open;
76 }
77
78 void Close() {
79 open = false;
80 }
81
82 void Open() {
83 open = true;
84 }
85
74private: 86private:
75 const u64 layer_id; 87 const u64 layer_id;
76 const u32 binder_id; 88 const u32 binder_id;
77 android::BufferQueueCore& core; 89 android::BufferQueueCore& core;
78 android::BufferQueueProducer& binder; 90 android::BufferQueueProducer& binder;
79 std::shared_ptr<android::BufferItemConsumer> consumer; 91 std::shared_ptr<android::BufferItemConsumer> consumer;
92 bool open;
80}; 93};
81 94
82} // namespace Service::VI 95} // namespace Service::VI
diff --git a/src/core/hle/service/vi/vi.cpp b/src/core/hle/service/vi/vi.cpp
index b1bfb9898..9ab8788e3 100644
--- a/src/core/hle/service/vi/vi.cpp
+++ b/src/core/hle/service/vi/vi.cpp
@@ -719,6 +719,8 @@ private:
719 return; 719 return;
720 } 720 }
721 721
722 nv_flinger.OpenLayer(layer_id);
723
722 android::OutputParcel parcel; 724 android::OutputParcel parcel;
723 parcel.WriteInterface(NativeWindow{*buffer_queue_id}); 725 parcel.WriteInterface(NativeWindow{*buffer_queue_id});
724 726
@@ -783,6 +785,7 @@ private:
783 const u64 layer_id = rp.Pop<u64>(); 785 const u64 layer_id = rp.Pop<u64>();
784 786
785 LOG_WARNING(Service_VI, "(STUBBED) called. layer_id=0x{:016X}", layer_id); 787 LOG_WARNING(Service_VI, "(STUBBED) called. layer_id=0x{:016X}", layer_id);
788 nv_flinger.DestroyLayer(layer_id);
786 789
787 IPC::ResponseBuilder rb{ctx, 2}; 790 IPC::ResponseBuilder rb{ctx, 2};
788 rb.Push(ResultSuccess); 791 rb.Push(ResultSuccess);
diff --git a/src/frontend_common/config.cpp b/src/frontend_common/config.cpp
index 1a0491c2c..d9f99148b 100644
--- a/src/frontend_common/config.cpp
+++ b/src/frontend_common/config.cpp
@@ -214,6 +214,7 @@ void Config::ReadControlValues() {
214} 214}
215 215
216void Config::ReadMotionTouchValues() { 216void Config::ReadMotionTouchValues() {
217 Settings::values.touch_from_button_maps.clear();
217 int num_touch_from_button_maps = BeginArray(std::string("touch_from_button_maps")); 218 int num_touch_from_button_maps = BeginArray(std::string("touch_from_button_maps"));
218 219
219 if (num_touch_from_button_maps > 0) { 220 if (num_touch_from_button_maps > 0) {
diff --git a/src/video_core/renderer_opengl/gl_buffer_cache.cpp b/src/video_core/renderer_opengl/gl_buffer_cache.cpp
index e6c70fb34..a71866b75 100644
--- a/src/video_core/renderer_opengl/gl_buffer_cache.cpp
+++ b/src/video_core/renderer_opengl/gl_buffer_cache.cpp
@@ -333,7 +333,7 @@ void BufferCacheRuntime::BindTransformFeedbackBuffers(VideoCommon::HostBindings<
333 glBindBuffersRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0, 333 glBindBuffersRange(GL_TRANSFORM_FEEDBACK_BUFFER, 0,
334 static_cast<GLsizei>(bindings.buffers.size()), buffer_handles.data(), 334 static_cast<GLsizei>(bindings.buffers.size()), buffer_handles.data(),
335 reinterpret_cast<const GLintptr*>(bindings.offsets.data()), 335 reinterpret_cast<const GLintptr*>(bindings.offsets.data()),
336 reinterpret_cast<const GLsizeiptr*>(bindings.strides.data())); 336 reinterpret_cast<const GLsizeiptr*>(bindings.sizes.data()));
337} 337}
338 338
339void BufferCacheRuntime::BindTextureBuffer(Buffer& buffer, u32 offset, u32 size, 339void BufferCacheRuntime::BindTextureBuffer(Buffer& buffer, u32 offset, u32 size,
diff --git a/src/video_core/renderer_vulkan/vk_present_manager.cpp b/src/video_core/renderer_vulkan/vk_present_manager.cpp
index 8e4c74b5c..a59e2d2d1 100644
--- a/src/video_core/renderer_vulkan/vk_present_manager.cpp
+++ b/src/video_core/renderer_vulkan/vk_present_manager.cpp
@@ -102,8 +102,8 @@ PresentManager::PresentManager(const vk::Instance& instance_,
102 memory_allocator{memory_allocator_}, scheduler{scheduler_}, swapchain{swapchain_}, 102 memory_allocator{memory_allocator_}, scheduler{scheduler_}, swapchain{swapchain_},
103 surface{surface_}, blit_supported{CanBlitToSwapchain(device.GetPhysical(), 103 surface{surface_}, blit_supported{CanBlitToSwapchain(device.GetPhysical(),
104 swapchain.GetImageViewFormat())}, 104 swapchain.GetImageViewFormat())},
105 use_present_thread{Settings::values.async_presentation.GetValue()}, 105 use_present_thread{Settings::values.async_presentation.GetValue()} {
106 image_count{swapchain.GetImageCount()} { 106 SetImageCount();
107 107
108 auto& dld = device.GetLogical(); 108 auto& dld = device.GetLogical();
109 cmdpool = dld.CreateCommandPool({ 109 cmdpool = dld.CreateCommandPool({
@@ -289,7 +289,14 @@ void PresentManager::PresentThread(std::stop_token token) {
289 289
290void PresentManager::RecreateSwapchain(Frame* frame) { 290void PresentManager::RecreateSwapchain(Frame* frame) {
291 swapchain.Create(*surface, frame->width, frame->height); 291 swapchain.Create(*surface, frame->width, frame->height);
292 image_count = swapchain.GetImageCount(); 292 SetImageCount();
293}
294
295void PresentManager::SetImageCount() {
296 // We cannot have more than 5 images in flight at any given time.
297 // FRAMES_IN_FLIGHT is 7, and the cache TICKS_TO_DESTROY is 6.
298 // Mali drivers will give us 6.
299 image_count = std::min<size_t>(swapchain.GetImageCount(), 5);
293} 300}
294 301
295void PresentManager::CopyToSwapchain(Frame* frame) { 302void PresentManager::CopyToSwapchain(Frame* frame) {
diff --git a/src/video_core/renderer_vulkan/vk_present_manager.h b/src/video_core/renderer_vulkan/vk_present_manager.h
index 337171a09..23ee61c8c 100644
--- a/src/video_core/renderer_vulkan/vk_present_manager.h
+++ b/src/video_core/renderer_vulkan/vk_present_manager.h
@@ -62,6 +62,8 @@ private:
62 62
63 void RecreateSwapchain(Frame* frame); 63 void RecreateSwapchain(Frame* frame);
64 64
65 void SetImageCount();
66
65private: 67private:
66 const vk::Instance& instance; 68 const vk::Instance& instance;
67 Core::Frontend::EmuWindow& render_window; 69 Core::Frontend::EmuWindow& render_window;