From 70c3d36536ad98a5569d51f1d0f68a6f01890f11 Mon Sep 17 00:00:00 2001 From: t895 Date: Sun, 10 Dec 2023 20:10:36 -0500 Subject: [PATCH 01/20] android: Refactor settings to expose more options In AbstractSetting, this removes the category, androidDefault, and valueAsString properties as they are no longer needed and have replacements. isSwitchable, global, and getValueAsString are all exposed and give better options for working with global/per-game settings. --- .../yuzu_emu/activities/EmulationActivity.kt | 20 ++- .../settings/model/AbstractBooleanSetting.kt | 3 +- .../settings/model/AbstractByteSetting.kt | 3 +- .../settings/model/AbstractFloatSetting.kt | 3 +- .../settings/model/AbstractIntSetting.kt | 3 +- .../settings/model/AbstractLongSetting.kt | 3 +- .../settings/model/AbstractSetting.kt | 14 +- .../settings/model/AbstractShortSetting.kt | 3 +- .../settings/model/AbstractStringSetting.kt | 3 +- .../features/settings/model/BooleanSetting.kt | 50 ++++--- .../features/settings/model/ByteSetting.kt | 22 +-- .../features/settings/model/FloatSetting.kt | 22 +-- .../features/settings/model/IntSetting.kt | 49 ++++--- .../features/settings/model/LongSetting.kt | 22 +-- .../features/settings/model/Settings.kt | 51 ------- .../features/settings/model/ShortSetting.kt | 22 +-- .../features/settings/model/StringSetting.kt | 23 ++-- .../settings/model/view/DateTimeSetting.kt | 5 +- .../settings/model/view/SettingsItem.kt | 24 +++- .../model/view/SingleChoiceSetting.kt | 17 +-- .../settings/model/view/SliderSetting.kt | 30 ++--- .../model/view/StringSingleChoiceSetting.kt | 7 +- .../settings/model/view/SwitchSetting.kt | 24 ++-- .../features/settings/ui/SettingsAdapter.kt | 11 +- .../settings/ui/SettingsFragmentPresenter.kt | 29 ++-- .../ui/viewholder/DateTimeViewHolder.kt | 2 +- .../ui/viewholder/SingleChoiceViewHolder.kt | 4 +- .../ui/viewholder/SliderViewHolder.kt | 2 +- .../ui/viewholder/SwitchSettingViewHolder.kt | 2 +- .../yuzu_emu/fragments/EmulationFragment.kt | 6 +- .../fragments/SettingsDialogFragment.kt | 12 +- .../org/yuzu/yuzu_emu/utils/NativeConfig.kt | 47 +++++-- .../app/src/main/jni/native_config.cpp | 126 ++++++++---------- src/common/settings_setting.h | 5 +- 34 files changed, 320 insertions(+), 349 deletions(-) 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 f41d7bdbfa..9b08f008d1 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 { override fun onUserLeaveHint() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - if (BooleanSetting.PICTURE_IN_PICTURE.boolean && !isInPictureInPictureMode) { + if (BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && !isInPictureInPictureMode) { val pictureInPictureParamsBuilder = PictureInPictureParams.Builder() .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder() enterPictureInPictureMode(pictureInPictureParamsBuilder.build()) @@ -284,7 +284,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder(): PictureInPictureParams.Builder { - val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.int) { + val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.getInt()) { 0 -> Rational(16, 9) 1 -> Rational(4, 3) 2 -> Rational(21, 9) @@ -331,7 +331,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { pictureInPictureActions.add(pauseRemoteAction) } - if (BooleanSetting.AUDIO_MUTED.boolean) { + if (BooleanSetting.AUDIO_MUTED.getBoolean()) { val unmuteIcon = Icon.createWithResource( this@EmulationActivity, R.drawable.ic_pip_unmute @@ -376,7 +376,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { val isEmulationActive = emulationViewModel.emulationStarted.value && !emulationViewModel.isEmulationStopping.value pictureInPictureParamsBuilder.setAutoEnterEnabled( - BooleanSetting.PICTURE_IN_PICTURE.boolean && isEmulationActive + BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && isEmulationActive ) } setPictureInPictureParams(pictureInPictureParamsBuilder.build()) @@ -390,9 +390,13 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation() } if (intent.action == actionUnmute) { - if (BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(false) + if (BooleanSetting.AUDIO_MUTED.getBoolean()) { + BooleanSetting.AUDIO_MUTED.setBoolean(false) + } } else if (intent.action == actionMute) { - if (!BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(true) + if (!BooleanSetting.AUDIO_MUTED.getBoolean()) { + BooleanSetting.AUDIO_MUTED.setBoolean(true) + } } buildPictureInPictureParams() } @@ -423,7 +427,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { } catch (ignored: Exception) { } // Always resume audio, since there is no UI button - if (BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(false) + if (BooleanSetting.AUDIO_MUTED.getBoolean()) { + BooleanSetting.AUDIO_MUTED.setBoolean(false) + } } } 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 aeda8d2220..0ba4653562 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 @@ package org.yuzu.yuzu_emu.features.settings.model interface AbstractBooleanSetting : AbstractSetting { - val boolean: Boolean - + fun getBoolean(needsGlobal: Boolean = false): Boolean fun setBoolean(value: Boolean) } 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 606519ad84..cf63005359 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 @@ package org.yuzu.yuzu_emu.features.settings.model interface AbstractByteSetting : AbstractSetting { - val byte: Byte - + fun getByte(needsGlobal: Boolean = false): Byte fun setByte(value: Byte) } 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 974925eeda..c6c0bcf348 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 @@ package org.yuzu.yuzu_emu.features.settings.model interface AbstractFloatSetting : AbstractSetting { - val float: Float - + fun getFloat(needsGlobal: Boolean = false): Float fun setFloat(value: Float) } 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 89b285b108..826402c343 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 @@ package org.yuzu.yuzu_emu.features.settings.model interface AbstractIntSetting : AbstractSetting { - val int: Int - + fun getInt(needsGlobal: Boolean = false): Int fun setInt(value: Int) } 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 4873942db7..2b62cc06b5 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 @@ package org.yuzu.yuzu_emu.features.settings.model interface AbstractLongSetting : AbstractSetting { - val long: Long - + fun getLong(needsGlobal: Boolean = false): Long fun setLong(value: Long) } 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 8b6d29fe5b..e384c78c25 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 interface AbstractSetting { val key: String - val category: Settings.Category val defaultValue: Any - val androidDefault: Any? - get() = null - val valueAsString: String - get() = "" val isRuntimeModifiable: Boolean get() = NativeConfig.getIsRuntimeModifiable(key) @@ -20,5 +15,14 @@ interface AbstractSetting { val pairedSettingKey: String get() = NativeConfig.getPairedSettingKey(key) + val isSwitchable: Boolean + get() = NativeConfig.getIsSwitchable(key) + + var global: Boolean + get() = NativeConfig.usingGlobal(key) + set(value) = NativeConfig.setGlobal(key, value) + + fun getValueAsString(needsGlobal: Boolean = false): String + fun reset() } 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 91407ccbb4..8bfa81e4ac 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 @@ package org.yuzu.yuzu_emu.features.settings.model interface AbstractShortSetting : AbstractSetting { - val short: Short - + fun getShort(needsGlobal: Boolean = false): Short fun setShort(value: Short) } 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 c8935cc48c..6ff8fd3f9a 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 @@ package org.yuzu.yuzu_emu.features.settings.model interface AbstractStringSetting : AbstractSetting { - val string: String - + fun getString(needsGlobal: Boolean = false): String fun setString(value: String) } 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 8476ce8671..16f06cd0af 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 import org.yuzu.yuzu_emu.utils.NativeConfig -enum class BooleanSetting( - override val key: String, - override val category: Settings.Category, - override val androidDefault: Boolean? = null -) : AbstractBooleanSetting { - AUDIO_MUTED("audio_muted", Settings.Category.Audio), - CPU_DEBUG_MODE("cpu_debug_mode", Settings.Category.Cpu), - FASTMEM("cpuopt_fastmem", Settings.Category.Cpu), - FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives", Settings.Category.Cpu), - RENDERER_USE_SPEED_LIMIT("use_speed_limit", Settings.Category.Core), - USE_DOCKED_MODE("use_docked_mode", Settings.Category.System, false), - RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache", Settings.Category.Renderer), - RENDERER_FORCE_MAX_CLOCK("force_max_clock", Settings.Category.Renderer), - RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders", Settings.Category.Renderer), - RENDERER_REACTIVE_FLUSHING("use_reactive_flushing", Settings.Category.Renderer, false), - RENDERER_DEBUG("debug", Settings.Category.Renderer), - PICTURE_IN_PICTURE("picture_in_picture", Settings.Category.Android), - USE_CUSTOM_RTC("custom_rtc_enabled", Settings.Category.System); +enum class BooleanSetting(override val key: String) : AbstractBooleanSetting { + AUDIO_MUTED("audio_muted"), + CPU_DEBUG_MODE("cpu_debug_mode"), + FASTMEM("cpuopt_fastmem"), + FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives"), + RENDERER_USE_SPEED_LIMIT("use_speed_limit"), + USE_DOCKED_MODE("use_docked_mode"), + RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache"), + RENDERER_FORCE_MAX_CLOCK("force_max_clock"), + RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"), + RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"), + RENDERER_DEBUG("debug"), + PICTURE_IN_PICTURE("picture_in_picture"), + USE_CUSTOM_RTC("custom_rtc_enabled"); - override val boolean: Boolean - get() = NativeConfig.getBoolean(key, false) + override fun getBoolean(needsGlobal: Boolean): Boolean = + NativeConfig.getBoolean(key, needsGlobal) - override fun setBoolean(value: Boolean) = NativeConfig.setBoolean(key, value) - - override val defaultValue: Boolean by lazy { - androidDefault ?: NativeConfig.getBoolean(key, true) + override fun setBoolean(value: Boolean) { + if (NativeConfig.isPerGameConfigLoaded()) { + global = false + } + NativeConfig.setBoolean(key, value) } - override val valueAsString: String - get() = if (boolean) "1" else "0" + override val defaultValue: Boolean by lazy { NativeConfig.getDefaultToString(key).toBoolean() } + + override fun getValueAsString(needsGlobal: Boolean): String = getBoolean(needsGlobal).toString() override fun reset() = NativeConfig.setBoolean(key, defaultValue) } 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 6ec0a765ef..7b7fac2112 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 import org.yuzu.yuzu_emu.utils.NativeConfig -enum class ByteSetting( - override val key: String, - override val category: Settings.Category -) : AbstractByteSetting { - AUDIO_VOLUME("volume", Settings.Category.Audio); +enum class ByteSetting(override val key: String) : AbstractByteSetting { + AUDIO_VOLUME("volume"); - override val byte: Byte - get() = NativeConfig.getByte(key, false) + override fun getByte(needsGlobal: Boolean): Byte = NativeConfig.getByte(key, needsGlobal) - override fun setByte(value: Byte) = NativeConfig.setByte(key, value) + override fun setByte(value: Byte) { + if (NativeConfig.isPerGameConfigLoaded()) { + global = false + } + NativeConfig.setByte(key, value) + } - override val defaultValue: Byte by lazy { NativeConfig.getByte(key, true) } + override val defaultValue: Byte by lazy { NativeConfig.getDefaultToString(key).toByte() } - override val valueAsString: String - get() = byte.toString() + override fun getValueAsString(needsGlobal: Boolean): String = getByte(needsGlobal).toString() override fun reset() = NativeConfig.setByte(key, defaultValue) } 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 0181d06f21..4644824d8a 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 import org.yuzu.yuzu_emu.utils.NativeConfig -enum class FloatSetting( - override val key: String, - override val category: Settings.Category -) : AbstractFloatSetting { +enum class FloatSetting(override val key: String) : AbstractFloatSetting { // No float settings currently exist - EMPTY_SETTING("", Settings.Category.UiGeneral); + EMPTY_SETTING(""); - override val float: Float - get() = NativeConfig.getFloat(key, false) + override fun getFloat(needsGlobal: Boolean): Float = NativeConfig.getFloat(key, false) - override fun setFloat(value: Float) = NativeConfig.setFloat(key, value) + override fun setFloat(value: Float) { + if (NativeConfig.isPerGameConfigLoaded()) { + global = false + } + NativeConfig.setFloat(key, value) + } - override val defaultValue: Float by lazy { NativeConfig.getFloat(key, true) } + override val defaultValue: Float by lazy { NativeConfig.getDefaultToString(key).toFloat() } - override val valueAsString: String - get() = float.toString() + override fun getValueAsString(needsGlobal: Boolean): String = getFloat(needsGlobal).toString() override fun reset() = NativeConfig.setFloat(key, defaultValue) } 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 ef10b209fd..21e4e1afd5 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 import org.yuzu.yuzu_emu.utils.NativeConfig -enum class IntSetting( - override val key: String, - override val category: Settings.Category, - override val androidDefault: Int? = null -) : AbstractIntSetting { - CPU_BACKEND("cpu_backend", Settings.Category.Cpu), - CPU_ACCURACY("cpu_accuracy", Settings.Category.Cpu), - REGION_INDEX("region_index", Settings.Category.System), - LANGUAGE_INDEX("language_index", Settings.Category.System), - RENDERER_BACKEND("backend", Settings.Category.Renderer), - RENDERER_ACCURACY("gpu_accuracy", Settings.Category.Renderer, 0), - RENDERER_RESOLUTION("resolution_setup", Settings.Category.Renderer), - RENDERER_VSYNC("use_vsync", Settings.Category.Renderer), - RENDERER_SCALING_FILTER("scaling_filter", Settings.Category.Renderer), - RENDERER_ANTI_ALIASING("anti_aliasing", Settings.Category.Renderer), - RENDERER_SCREEN_LAYOUT("screen_layout", Settings.Category.Android), - RENDERER_ASPECT_RATIO("aspect_ratio", Settings.Category.Renderer), - AUDIO_OUTPUT_ENGINE("output_engine", Settings.Category.Audio); +enum class IntSetting(override val key: String) : AbstractIntSetting { + CPU_BACKEND("cpu_backend"), + CPU_ACCURACY("cpu_accuracy"), + REGION_INDEX("region_index"), + LANGUAGE_INDEX("language_index"), + RENDERER_BACKEND("backend"), + RENDERER_ACCURACY("gpu_accuracy"), + RENDERER_RESOLUTION("resolution_setup"), + RENDERER_VSYNC("use_vsync"), + RENDERER_SCALING_FILTER("scaling_filter"), + RENDERER_ANTI_ALIASING("anti_aliasing"), + RENDERER_SCREEN_LAYOUT("screen_layout"), + RENDERER_ASPECT_RATIO("aspect_ratio"), + AUDIO_OUTPUT_ENGINE("output_engine"); - override val int: Int - get() = NativeConfig.getInt(key, false) + override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal) - override fun setInt(value: Int) = NativeConfig.setInt(key, value) - - override val defaultValue: Int by lazy { - androidDefault ?: NativeConfig.getInt(key, true) + override fun setInt(value: Int) { + if (NativeConfig.isPerGameConfigLoaded()) { + global = false + } + NativeConfig.setInt(key, value) } - override val valueAsString: String - get() = int.toString() + override val defaultValue: Int by lazy { NativeConfig.getDefaultToString(key).toInt() } + + override fun getValueAsString(needsGlobal: Boolean): String = getInt(needsGlobal).toString() override fun reset() = NativeConfig.setInt(key, defaultValue) } 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 c526fc4cfa..e3efd516c0 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 import org.yuzu.yuzu_emu.utils.NativeConfig -enum class LongSetting( - override val key: String, - override val category: Settings.Category -) : AbstractLongSetting { - CUSTOM_RTC("custom_rtc", Settings.Category.System); +enum class LongSetting(override val key: String) : AbstractLongSetting { + CUSTOM_RTC("custom_rtc"); - override val long: Long - get() = NativeConfig.getLong(key, false) + override fun getLong(needsGlobal: Boolean): Long = NativeConfig.getLong(key, needsGlobal) - override fun setLong(value: Long) = NativeConfig.setLong(key, value) + override fun setLong(value: Long) { + if (NativeConfig.isPerGameConfigLoaded()) { + global = false + } + NativeConfig.setLong(key, value) + } - override val defaultValue: Long by lazy { NativeConfig.getLong(key, true) } + override val defaultValue: Long by lazy { NativeConfig.getDefaultToString(key).toLong() } - override val valueAsString: String - get() = long.toString() + override fun getValueAsString(needsGlobal: Boolean): String = getLong(needsGlobal).toString() override fun reset() = NativeConfig.setLong(key, defaultValue) } 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 e3cd661859..9551fc05e4 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 import org.yuzu.yuzu_emu.R object Settings { - enum class Category { - Android, - Audio, - Core, - Cpu, - CpuDebug, - CpuUnsafe, - Renderer, - RendererAdvanced, - RendererDebug, - System, - SystemAudio, - DataStorage, - Debugging, - DebuggingGraphics, - Miscellaneous, - Network, - WebService, - AddOns, - Controls, - Ui, - UiGeneral, - UiLayout, - UiGameList, - Screenshots, - Shortcuts, - Multiplayer, - Services, - Paths, - MaxEnum - } - - val settingsList = listOf( - *BooleanSetting.values(), - *ByteSetting.values(), - *ShortSetting.values(), - *IntSetting.values(), - *FloatSetting.values(), - *LongSetting.values(), - *StringSetting.values() - ) - - const val SECTION_GENERAL = "General" - const val SECTION_SYSTEM = "System" - const val SECTION_RENDERER = "Renderer" - const val SECTION_AUDIO = "Audio" - const val SECTION_CPU = "Cpu" - const val SECTION_THEME = "Theme" - const val SECTION_DEBUG = "Debug" - enum class MenuTag(val titleId: Int) { SECTION_ROOT(R.string.advanced_settings), SECTION_SYSTEM(R.string.preferences_system), SECTION_RENDERER(R.string.preferences_graphics), SECTION_AUDIO(R.string.preferences_audio), - SECTION_CPU(R.string.cpu), SECTION_THEME(R.string.preferences_theme), SECTION_DEBUG(R.string.preferences_debug); } 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 c9a0c664cb..16eb4ffdd5 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 import org.yuzu.yuzu_emu.utils.NativeConfig -enum class ShortSetting( - override val key: String, - override val category: Settings.Category -) : AbstractShortSetting { - RENDERER_SPEED_LIMIT("speed_limit", Settings.Category.Core); +enum class ShortSetting(override val key: String) : AbstractShortSetting { + RENDERER_SPEED_LIMIT("speed_limit"); - override val short: Short - get() = NativeConfig.getShort(key, false) + override fun getShort(needsGlobal: Boolean): Short = NativeConfig.getShort(key, needsGlobal) - override fun setShort(value: Short) = NativeConfig.setShort(key, value) + override fun setShort(value: Short) { + if (NativeConfig.isPerGameConfigLoaded()) { + global = false + } + NativeConfig.setShort(key, value) + } - override val defaultValue: Short by lazy { NativeConfig.getShort(key, true) } + override val defaultValue: Short by lazy { NativeConfig.getDefaultToString(key).toShort() } - override val valueAsString: String - get() = short.toString() + override fun getValueAsString(needsGlobal: Boolean): String = getShort(needsGlobal).toString() override fun reset() = NativeConfig.setShort(key, defaultValue) } 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 9bb3e66d4f..a0d8cfedea 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 import org.yuzu.yuzu_emu.utils.NativeConfig -enum class StringSetting( - override val key: String, - override val category: Settings.Category -) : AbstractStringSetting { - // No string settings currently exist - EMPTY_SETTING("", Settings.Category.UiGeneral); +enum class StringSetting(override val key: String) : AbstractStringSetting { + DRIVER_PATH("driver_path"); - override val string: String - get() = NativeConfig.getString(key, false) + override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal) - override fun setString(value: String) = NativeConfig.setString(key, value) + override fun setString(value: String) { + if (NativeConfig.isPerGameConfigLoaded()) { + global = false + } + NativeConfig.setString(key, value) + } - override val defaultValue: String by lazy { NativeConfig.getString(key, true) } + override val defaultValue: String by lazy { NativeConfig.getDefaultToString(key) } - override val valueAsString: String - get() = string + override fun getValueAsString(needsGlobal: Boolean): String = getString(needsGlobal) override fun reset() = NativeConfig.setString(key, defaultValue) } 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 8bc1641978..1d81f5f2b4 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( ) : SettingsItem(longSetting, titleId, descriptionId) { override val type = TYPE_DATETIME_SETTING - var value: Long - get() = longSetting.long - set(value) = (setting as AbstractLongSetting).setLong(value) + fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal) + fun setValue(value: Long) = (setting as AbstractLongSetting).setLong(value) } 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 e198b18a02..3845272946 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,7 +11,6 @@ import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting import org.yuzu.yuzu_emu.features.settings.model.ByteSetting import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.LongSetting -import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.features.settings.model.ShortSetting /** @@ -48,8 +47,8 @@ abstract class SettingsItem( val emptySetting = object : AbstractSetting { override val key: String = "" - override val category: Settings.Category = Settings.Category.Ui override val defaultValue: Any = false + override fun getValueAsString(needsGlobal: Boolean): String = "" override fun reset() {} } @@ -270,9 +269,9 @@ abstract class SettingsItem( ) val fastmem = object : AbstractBooleanSetting { - override val boolean: Boolean - get() = - BooleanSetting.FASTMEM.boolean && BooleanSetting.FASTMEM_EXCLUSIVES.boolean + override fun getBoolean(needsGlobal: Boolean): Boolean = + BooleanSetting.FASTMEM.getBoolean() && + BooleanSetting.FASTMEM_EXCLUSIVES.getBoolean() override fun setBoolean(value: Boolean) { BooleanSetting.FASTMEM.setBoolean(value) @@ -280,9 +279,22 @@ abstract class SettingsItem( } override val key: String = FASTMEM_COMBINED - override val category = Settings.Category.Cpu override val isRuntimeModifiable: Boolean = false override val defaultValue: Boolean = true + override val isSwitchable: Boolean = true + override var global: Boolean + get() { + return BooleanSetting.FASTMEM.global && + BooleanSetting.FASTMEM_EXCLUSIVES.global + } + set(value) { + BooleanSetting.FASTMEM.global = value + BooleanSetting.FASTMEM_EXCLUSIVES.global = value + } + + override fun getValueAsString(needsGlobal: Boolean): String = + getBoolean().toString() + override fun reset() = setBoolean(defaultValue) } 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 705527a733..97a5a9e596 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( ) : SettingsItem(setting, titleId, descriptionId) { override val type = TYPE_SINGLE_CHOICE - var selectedValue: Int - get() { - return when (setting) { - is AbstractIntSetting -> setting.int - else -> -1 - } - } - set(value) { - when (setting) { - is AbstractIntSetting -> setting.setInt(value) - } + fun getSelectedValue(needsGlobal: Boolean = false) = + when (setting) { + is AbstractIntSetting -> setting.getInt(needsGlobal) + else -> -1 } + + fun setSelectedValue(value: Int) = (setting as AbstractIntSetting).setInt(value) } 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 c3b5df02c9..b9b709bf7b 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( ) : SettingsItem(setting, titleId, descriptionId) { override val type = TYPE_SLIDER - var selectedValue: Int - get() { - return when (setting) { - is AbstractByteSetting -> setting.byte.toInt() - is AbstractShortSetting -> setting.short.toInt() - is AbstractIntSetting -> setting.int - is AbstractFloatSetting -> setting.float.roundToInt() - else -> -1 - } + fun getSelectedValue(needsGlobal: Boolean = false) = + when (setting) { + is AbstractByteSetting -> setting.getByte(needsGlobal).toInt() + is AbstractShortSetting -> setting.getShort(needsGlobal).toInt() + is AbstractIntSetting -> setting.getInt(needsGlobal) + is AbstractFloatSetting -> setting.getFloat(needsGlobal).roundToInt() + else -> -1 } - set(value) { - when (setting) { - is AbstractByteSetting -> setting.setByte(value.toByte()) - is AbstractShortSetting -> setting.setShort(value.toShort()) - is AbstractIntSetting -> setting.setInt(value) - is AbstractFloatSetting -> setting.setFloat(value.toFloat()) - } + + fun setSelectedValue(value: Int) = + when (setting) { + is AbstractByteSetting -> setting.setByte(value.toByte()) + is AbstractShortSetting -> setting.setShort(value.toShort()) + is AbstractFloatSetting -> setting.setFloat(value.toFloat()) + else -> (setting as AbstractIntSetting).setInt(value) } } 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 871dab4f3b..ba7920f50b 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( fun getValueAt(index: Int): String = if (index >= 0 && index < values.size) values[index] else "" - var selectedValue: String - get() = stringSetting.string - set(value) = stringSetting.setString(value) + fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal) + fun setSelectedValue(value: String) = stringSetting.setString(value) val selectValueIndex: Int get() { for (i in values.indices) { - if (values[i] == selectedValue) { + if (values[i] == getSelectedValue()) { return i } } 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 416967e645..44d47dd69d 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( ) : SettingsItem(setting, titleId, descriptionId) { override val type = TYPE_SWITCH - var checked: Boolean - get() { - return when (setting) { - is AbstractIntSetting -> setting.int == 1 - is AbstractBooleanSetting -> setting.boolean - else -> false - } + fun getIsChecked(needsGlobal: Boolean = false): Boolean { + return when (setting) { + is AbstractIntSetting -> setting.getInt(needsGlobal) == 1 + is AbstractBooleanSetting -> setting.getBoolean(needsGlobal) + else -> false } - set(value) { - when (setting) { - is AbstractIntSetting -> setting.setInt(if (value) 1 else 0) - is AbstractBooleanSetting -> setting.setBoolean(value) - } + } + + fun setChecked(value: Boolean) { + when (setting) { + is AbstractIntSetting -> setting.setInt(if (value) 1 else 0) + is AbstractBooleanSetting -> setting.setBoolean(value) } + } } 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 af2c1e5820..3f23c064e2 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( return currentList[position].type } - fun onBooleanClick(item: SwitchSetting, checked: Boolean) { - item.checked = checked + fun onBooleanClick(item: SwitchSetting, checked: Boolean, position: Int) { + item.setChecked(checked) + notifyItemChanged(position) settingsViewModel.setShouldReloadSettingsList(true) } @@ -126,7 +127,7 @@ class SettingsAdapter( } fun onDateTimeClick(item: DateTimeSetting, position: Int) { - val storedTime = item.value * 1000 + val storedTime = item.getValue() * 1000 // Helper to extract hour and minute from epoch time val calendar: Calendar = Calendar.getInstance() @@ -159,9 +160,9 @@ class SettingsAdapter( var epochTime: Long = datePicker.selection!! / 1000 epochTime += timePicker.hour.toLong() * 60 * 60 epochTime += timePicker.minute.toLong() * 60 - if (item.value != epochTime) { + if (item.getValue() != epochTime) { notifyItemChanged(position) - item.value = epochTime + item.setValue(epochTime) } } datePicker.show( 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 7425728c60..12a389b37a 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 @@ -36,7 +36,14 @@ class SettingsFragmentPresenter( val item = SettingsItem.settingsItems[key]!! val pairedSettingKey = item.setting.pairedSettingKey if (pairedSettingKey.isNotEmpty()) { - val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false) + val pairedSettingValue = NativeConfig.getBoolean( + pairedSettingKey, + if (NativeLibrary.isRunning() && !NativeConfig.isPerGameConfigLoaded()) { + !NativeConfig.usingGlobal(pairedSettingKey) + } else { + NativeConfig.usingGlobal(pairedSettingKey) + } + ) if (!pairedSettingValue) return } add(item) @@ -153,8 +160,8 @@ class SettingsFragmentPresenter( private fun addThemeSettings(sl: ArrayList) { sl.apply { val theme: AbstractIntSetting = object : AbstractIntSetting { - override val int: Int - get() = preferences.getInt(Settings.PREF_THEME, 0) + override fun getInt(needsGlobal: Boolean): Int = + preferences.getInt(Settings.PREF_THEME, 0) override fun setInt(value: Int) { preferences.edit() @@ -164,8 +171,8 @@ class SettingsFragmentPresenter( } override val key: String = Settings.PREF_THEME - override val category = Settings.Category.UiGeneral override val isRuntimeModifiable: Boolean = false + override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString() override val defaultValue: Int = 0 override fun reset() { preferences.edit() @@ -197,8 +204,8 @@ class SettingsFragmentPresenter( } val themeMode: AbstractIntSetting = object : AbstractIntSetting { - override val int: Int - get() = preferences.getInt(Settings.PREF_THEME_MODE, -1) + override fun getInt(needsGlobal: Boolean): Int = + preferences.getInt(Settings.PREF_THEME_MODE, -1) override fun setInt(value: Int) { preferences.edit() @@ -208,8 +215,8 @@ class SettingsFragmentPresenter( } override val key: String = Settings.PREF_THEME_MODE - override val category = Settings.Category.UiGeneral override val isRuntimeModifiable: Boolean = false + override fun getValueAsString(needsGlobal: Boolean): String = getInt().toString() override val defaultValue: Int = -1 override fun reset() { preferences.edit() @@ -230,8 +237,8 @@ class SettingsFragmentPresenter( ) val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting { - override val boolean: Boolean - get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) + override fun getBoolean(needsGlobal: Boolean): Boolean = + preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) override fun setBoolean(value: Boolean) { preferences.edit() @@ -241,8 +248,10 @@ class SettingsFragmentPresenter( } override val key: String = Settings.PREF_BLACK_BACKGROUNDS - override val category = Settings.Category.UiGeneral override val isRuntimeModifiable: Boolean = false + override fun getValueAsString(needsGlobal: Boolean): String = + getBoolean().toString() + override val defaultValue: Boolean = false override fun reset() { 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 525f013f8b..4e159a7997 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 @@ -29,7 +29,7 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA } binding.textSettingValue.visibility = View.VISIBLE - val epochTime = setting.value + val epochTime = setting.getValue() val instant = Instant.ofEpochMilli(epochTime * 1000) val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC")) val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM) 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 80d1b22c1a..28c4d17775 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 @@ -29,14 +29,14 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti val resMgr = binding.textSettingValue.context.resources val values = resMgr.getIntArray(item.valuesId) for (i in values.indices) { - if (values[i] == item.selectedValue) { + if (values[i] == item.getSelectedValue()) { binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i] break } } } else if (item is StringSingleChoiceSetting) { for (i in item.values.indices) { - if (item.values[i] == item.selectedValue) { + if (item.values[i] == item.getSelectedValue()) { binding.textSettingValue.text = item.choices[i] break } 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 b83c901006..67432f88ef 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 @@ -26,7 +26,7 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda binding.textSettingValue.visibility = View.VISIBLE binding.textSettingValue.text = String.format( binding.textSettingValue.context.getString(R.string.value_with_units), - setting.selectedValue, + setting.getSelectedValue(), setting.units ) 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 57fdeaa208..98ed888cb5 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 @@ -27,7 +27,7 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter } binding.switchWidget.setOnCheckedChangeListener(null) - binding.switchWidget.isChecked = setting.checked + binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal) binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean -> adapter.onBooleanClick(item, binding.switchWidget.isChecked) } 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 734c1d5ca7..b09df7db34 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 @@ -435,7 +435,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { @SuppressLint("SourceLockedOrientationActivity") private fun updateOrientation() { emulationActivity?.let { - it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) { + it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.getInt()) { Settings.LayoutOption_MobileLandscape -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE Settings.LayoutOption_MobilePortrait -> @@ -617,7 +617,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { @SuppressLint("SourceLockedOrientationActivity") private fun startConfiguringControls() { // Lock the current orientation to prevent editing inconsistencies - if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) { + if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) { emulationActivity?.let { it.requestedOrientation = if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { @@ -635,7 +635,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { binding.doneControlConfig.visibility = View.GONE binding.surfaceInputOverlay.setIsInEditMode(false) // Unlock the orientation if it was locked for editing - if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) { + if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) { emulationActivity?.let { it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } 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 b88d2c0381..60e029f34f 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 sliderBinding = DialogSliderBinding.inflate(layoutInflater) val item = settingsViewModel.clickedItem as SliderSetting - settingsViewModel.setSliderTextValue(item.selectedValue.toFloat(), item.units) + settingsViewModel.setSliderTextValue(item.getSelectedValue().toFloat(), item.units) sliderBinding.slider.apply { valueFrom = item.min.toFloat() valueTo = item.max.toFloat() @@ -136,18 +136,18 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener is SingleChoiceSetting -> { val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting val value = getValueForSingleChoiceSelection(scSetting, which) - scSetting.selectedValue = value + scSetting.setSelectedValue(value) } is StringSingleChoiceSetting -> { val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting val value = scSetting.getValueAt(which) - scSetting.selectedValue = value + scSetting.setSelectedValue(value) } is SliderSetting -> { val sliderSetting = settingsViewModel.clickedItem as SliderSetting - sliderSetting.selectedValue = settingsViewModel.sliderProgress.value + sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value) } } closeDialog() @@ -171,7 +171,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener } private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int { - val value = item.selectedValue + val value = item.getSelectedValue() val valuesId = item.valuesId if (valuesId > 0) { val valuesArray = requireContext().resources.getIntArray(valuesId) @@ -211,7 +211,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!") SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress( - (clickedItem as SliderSetting).selectedValue.toFloat() + (clickedItem as SliderSetting).getSelectedValue().toFloat() ) } settingsViewModel.clickedItem = clickedItem 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 f4e1bb13fc..4c7316ba39 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 @@ -31,32 +31,63 @@ object NativeConfig { external fun saveSettings() external fun getBoolean(key: String, getDefault: Boolean): Boolean + + @Synchronized + external fun getBoolean(key: String, needsGlobal: Boolean): Boolean + + @Synchronized external fun setBoolean(key: String, value: Boolean) - external fun getByte(key: String, getDefault: Boolean): Byte + @Synchronized + external fun getByte(key: String, needsGlobal: Boolean): Byte + + @Synchronized external fun setByte(key: String, value: Byte) - external fun getShort(key: String, getDefault: Boolean): Short + @Synchronized + external fun getShort(key: String, needsGlobal: Boolean): Short + + @Synchronized external fun setShort(key: String, value: Short) - external fun getInt(key: String, getDefault: Boolean): Int + @Synchronized + external fun getInt(key: String, needsGlobal: Boolean): Int + + @Synchronized external fun setInt(key: String, value: Int) - external fun getFloat(key: String, getDefault: Boolean): Float + @Synchronized + external fun getFloat(key: String, needsGlobal: Boolean): Float + + @Synchronized external fun setFloat(key: String, value: Float) - external fun getLong(key: String, getDefault: Boolean): Long + @Synchronized + external fun getLong(key: String, needsGlobal: Boolean): Long + + @Synchronized external fun setLong(key: String, value: Long) - external fun getString(key: String, getDefault: Boolean): String + @Synchronized + external fun getString(key: String, needsGlobal: Boolean): String + + @Synchronized external fun setString(key: String, value: String) external fun getIsRuntimeModifiable(key: String): Boolean - external fun getConfigHeader(category: Int): String - external fun getPairedSettingKey(key: String): String + external fun getIsSwitchable(key: String): Boolean + + @Synchronized + external fun usingGlobal(key: String): Boolean + + @Synchronized + external fun setGlobal(key: String, global: Boolean) + + external fun getDefaultToString(key: String): String + /** * Gets every [GameDir] in AndroidSettings::values.game_dirs */ diff --git a/src/android/app/src/main/jni/native_config.cpp b/src/android/app/src/main/jni/native_config.cpp index 763b2164c5..9439d11e14 100644 --- a/src/android/app/src/main/jni/native_config.cpp +++ b/src/android/app/src/main/jni/native_config.cpp @@ -49,18 +49,12 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_saveSettings(JNIEnv* env, jobjec } jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getBoolean(JNIEnv* env, jobject obj, - jstring jkey, jboolean getDefault) { + jstring jkey, jboolean needGlobal) { auto setting = getSetting(env, jkey); if (setting == nullptr) { return false; } - setting->SetGlobal(true); - - if (static_cast(getDefault)) { - return setting->GetDefault(); - } - - return setting->GetValue(); + return setting->GetValue(static_cast(needGlobal)); } void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setBoolean(JNIEnv* env, jobject obj, jstring jkey, @@ -69,23 +63,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setBoolean(JNIEnv* env, jobject if (setting == nullptr) { return; } - setting->SetGlobal(true); setting->SetValue(static_cast(value)); } jbyte Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getByte(JNIEnv* env, jobject obj, jstring jkey, - jboolean getDefault) { + jboolean needGlobal) { auto setting = getSetting(env, jkey); if (setting == nullptr) { return -1; } - setting->SetGlobal(true); - - if (static_cast(getDefault)) { - return setting->GetDefault(); - } - - return setting->GetValue(); + return setting->GetValue(static_cast(needGlobal)); } void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setByte(JNIEnv* env, jobject obj, jstring jkey, @@ -94,23 +81,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setByte(JNIEnv* env, jobject obj if (setting == nullptr) { return; } - setting->SetGlobal(true); setting->SetValue(value); } jshort Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getShort(JNIEnv* env, jobject obj, jstring jkey, - jboolean getDefault) { + jboolean needGlobal) { auto setting = getSetting(env, jkey); if (setting == nullptr) { return -1; } - setting->SetGlobal(true); - - if (static_cast(getDefault)) { - return setting->GetDefault(); - } - - return setting->GetValue(); + return setting->GetValue(static_cast(needGlobal)); } void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setShort(JNIEnv* env, jobject obj, jstring jkey, @@ -119,23 +99,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setShort(JNIEnv* env, jobject ob if (setting == nullptr) { return; } - setting->SetGlobal(true); setting->SetValue(value); } jint Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getInt(JNIEnv* env, jobject obj, jstring jkey, - jboolean getDefault) { + jboolean needGlobal) { auto setting = getSetting(env, jkey); if (setting == nullptr) { return -1; } - setting->SetGlobal(true); - - if (static_cast(getDefault)) { - return setting->GetDefault(); - } - - return setting->GetValue(); + return setting->GetValue(needGlobal); } void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInt(JNIEnv* env, jobject obj, jstring jkey, @@ -144,23 +117,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setInt(JNIEnv* env, jobject obj, if (setting == nullptr) { return; } - setting->SetGlobal(true); setting->SetValue(value); } jfloat Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getFloat(JNIEnv* env, jobject obj, jstring jkey, - jboolean getDefault) { + jboolean needGlobal) { auto setting = getSetting(env, jkey); if (setting == nullptr) { return -1; } - setting->SetGlobal(true); - - if (static_cast(getDefault)) { - return setting->GetDefault(); - } - - return setting->GetValue(); + return setting->GetValue(static_cast(needGlobal)); } void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setFloat(JNIEnv* env, jobject obj, jstring jkey, @@ -169,23 +135,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setFloat(JNIEnv* env, jobject ob if (setting == nullptr) { return; } - setting->SetGlobal(true); setting->SetValue(value); } jlong Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getLong(JNIEnv* env, jobject obj, jstring jkey, - jboolean getDefault) { - auto setting = getSetting(env, jkey); + jboolean needGlobal) { + auto setting = getSetting(env, jkey); if (setting == nullptr) { return -1; } - setting->SetGlobal(true); - - if (static_cast(getDefault)) { - return setting->GetDefault(); - } - - return setting->GetValue(); + return setting->GetValue(static_cast(needGlobal)); } void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setLong(JNIEnv* env, jobject obj, jstring jkey, @@ -194,23 +153,16 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setLong(JNIEnv* env, jobject obj if (setting == nullptr) { return; } - setting->SetGlobal(true); setting->SetValue(value); } jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getString(JNIEnv* env, jobject obj, jstring jkey, - jboolean getDefault) { + jboolean needGlobal) { auto setting = getSetting(env, jkey); if (setting == nullptr) { return ToJString(env, ""); } - setting->SetGlobal(true); - - if (static_cast(getDefault)) { - return ToJString(env, setting->GetDefault()); - } - - return ToJString(env, setting->GetValue()); + return ToJString(env, setting->GetValue(static_cast(needGlobal))); } void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject obj, jstring jkey, @@ -220,27 +172,18 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setString(JNIEnv* env, jobject o return; } - setting->SetGlobal(true); setting->SetValue(GetJString(env, value)); } jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsRuntimeModifiable(JNIEnv* env, jobject obj, jstring jkey) { - auto key = GetJString(env, jkey); - auto setting = Settings::values.linkage.by_key[key]; - if (setting != 0) { + auto setting = getSetting(env, jkey); + if (setting != nullptr) { return setting->RuntimeModfiable(); } - LOG_ERROR(Frontend, "[Android Native] Could not find setting - {}", key); return true; } -jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getConfigHeader(JNIEnv* env, jobject obj, - jint jcategory) { - auto category = static_cast(jcategory); - return ToJString(env, Settings::TranslateCategory(category)); -} - jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* env, jobject obj, jstring jkey) { auto setting = getSetting(env, jkey); @@ -254,6 +197,41 @@ jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getPairedSettingKey(JNIEnv* e return ToJString(env, setting->PairedSetting()->GetLabel()); } +jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getIsSwitchable(JNIEnv* env, jobject obj, + jstring jkey) { + auto setting = getSetting(env, jkey); + if (setting != nullptr) { + return setting->Switchable(); + } + return false; +} + +jboolean Java_org_yuzu_yuzu_1emu_utils_NativeConfig_usingGlobal(JNIEnv* env, jobject obj, + jstring jkey) { + auto setting = getSetting(env, jkey); + if (setting != nullptr) { + return setting->UsingGlobal(); + } + return true; +} + +void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setGlobal(JNIEnv* env, jobject obj, jstring jkey, + jboolean global) { + auto setting = getSetting(env, jkey); + if (setting != nullptr) { + setting->SetGlobal(static_cast(global)); + } +} + +jstring Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getDefaultToString(JNIEnv* env, jobject obj, + jstring jkey) { + auto setting = getSetting(env, jkey); + if (setting != nullptr) { + return ToJString(env, setting->DefaultToString()); + } + return ToJString(env, ""); +} + jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getGameDirs(JNIEnv* env, jobject obj) { jclass gameDirClass = IDCache::GetGameDirClass(); jmethodID gameDirConstructor = IDCache::GetGameDirConstructor(); diff --git a/src/common/settings_setting.h b/src/common/settings_setting.h index 3175ab07d1..0b18ca5ecc 100644 --- a/src/common/settings_setting.h +++ b/src/common/settings_setting.h @@ -81,6 +81,9 @@ public: [[nodiscard]] virtual const Type& GetValue() const { return value; } + [[nodiscard]] virtual const Type& GetValue(bool need_global) const { + return value; + } /** * Sets the setting to the given value. @@ -353,7 +356,7 @@ public: } return custom; } - [[nodiscard]] const Type& GetValue(bool need_global) const { + [[nodiscard]] const Type& GetValue(bool need_global) const override final { if (use_global || need_global) { return this->value; } From 6b5fb2063f316e7eaf169d7c12c595ae7fbbcc2b Mon Sep 17 00:00:00 2001 From: t895 Date: Sun, 10 Dec 2023 20:12:05 -0500 Subject: [PATCH 02/20] frontend_common: Fix settings reload bug This clears the touch_from_button_maps array before we read new data into it because this read duplicate data on a reload otherwise. --- src/frontend_common/config.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/frontend_common/config.cpp b/src/frontend_common/config.cpp index 1a0491c2c1..d9f99148bc 100644 --- a/src/frontend_common/config.cpp +++ b/src/frontend_common/config.cpp @@ -214,6 +214,7 @@ void Config::ReadControlValues() { } void Config::ReadMotionTouchValues() { + Settings::values.touch_from_button_maps.clear(); int num_touch_from_button_maps = BeginArray(std::string("touch_from_button_maps")); if (num_touch_from_button_maps > 0) { From e975f3cde9d4dcb1d9e2bbce116f9a9ba99bf03f Mon Sep 17 00:00:00 2001 From: t895 Date: Sun, 10 Dec 2023 20:27:50 -0500 Subject: [PATCH 03/20] android: Add Game properties This commit has the UI for viewing a game's properties on long-press and some links to useful tools like - Game info - Shortcut to settings (global in this commit) - Addon manager with installer - Save data manager - Option to clear all save data - Option to clear shader cache --- .../java/org/yuzu/yuzu_emu/NativeLibrary.kt | 35 +- .../yuzu/yuzu_emu/adapters/AddonAdapter.kt | 52 +++ .../yuzu/yuzu_emu/adapters/AppletAdapter.kt | 6 +- .../org/yuzu/yuzu_emu/adapters/GameAdapter.kt | 16 +- .../adapters/GamePropertiesAdapter.kt | 133 ++++++ .../yuzu/yuzu_emu/fragments/AddonsFragment.kt | 214 +++++++++ .../ContentTypeSelectionDialogFragment.kt | 68 +++ .../yuzu_emu/fragments/GameInfoFragment.kt | 148 +++++++ .../fragments/GamePropertiesFragment.kt | 418 ++++++++++++++++++ .../IndeterminateProgressDialogFragment.kt | 2 +- .../fragments/LaunchGameDialogFragment.kt | 61 +++ .../fragments/MessageDialogFragment.kt | 37 +- .../yuzu/yuzu_emu/fragments/SearchFragment.kt | 4 +- .../java/org/yuzu/yuzu_emu/model/Addon.kt | 10 + .../org/yuzu/yuzu_emu/model/AddonViewModel.kt | 83 ++++ .../main/java/org/yuzu/yuzu_emu/model/Game.kt | 42 +- .../org/yuzu/yuzu_emu/model/GameProperties.kt | 34 ++ .../org/yuzu/yuzu_emu/model/HomeViewModel.kt | 15 + .../yuzu_emu/model/MessageDialogViewModel.kt | 4 +- .../org/yuzu/yuzu_emu/model/TaskViewModel.kt | 2 +- .../org/yuzu/yuzu_emu/ui/GamesFragment.kt | 11 +- .../org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 363 +++++++-------- .../java/org/yuzu/yuzu_emu/utils/AddonUtil.kt | 8 + .../java/org/yuzu/yuzu_emu/utils/FileUtil.kt | 32 ++ .../org/yuzu/yuzu_emu/utils/NativeConfig.kt | 19 + src/android/app/src/main/jni/id_cache.cpp | 40 ++ src/android/app/src/main/jni/id_cache.h | 6 + src/android/app/src/main/jni/native.cpp | 108 ++++- src/android/app/src/main/jni/native.h | 2 + .../app/src/main/jni/native_config.cpp | 26 ++ .../fragment_game_properties.xml | 99 +++++ .../src/main/res/layout/card_installable.xml | 3 +- ...et_option.xml => card_simple_outlined.xml} | 20 +- .../src/main/res/layout/fragment_addons.xml | 47 ++ .../main/res/layout/fragment_game_info.xml | 125 ++++++ .../res/layout/fragment_game_properties.xml | 86 ++++ .../src/main/res/layout/list_item_addon.xml | 57 +++ .../main/res/navigation/home_navigation.xml | 33 ++ .../app/src/main/res/values/dimens.xml | 2 +- .../app/src/main/res/values/strings.xml | 45 ++ 40 files changed, 2245 insertions(+), 271 deletions(-) create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ContentTypeSelectionDialogFragment.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt create mode 100644 src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddonUtil.kt create mode 100644 src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml rename src/android/app/src/main/res/layout/{card_applet_option.xml => card_simple_outlined.xml} (71%) create mode 100644 src/android/app/src/main/res/layout/fragment_addons.xml create mode 100644 src/android/app/src/main/res/layout/fragment_game_info.xml create mode 100644 src/android/app/src/main/res/layout/fragment_game_properties.xml create mode 100644 src/android/app/src/main/res/layout/list_item_addon.xml 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 e0f01127c7..95b98798db 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 { */ external fun onTouchReleased(finger_id: Int) - external fun initGameIni(gameID: String?) - external fun setAppDirectory(directory: String) /** @@ -241,6 +239,8 @@ object NativeLibrary { */ external fun installFileToNand(filename: String, extension: String): Int + external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean + external fun initializeGpuDriver( hookLibDir: String?, customDriverDir: String?, @@ -252,18 +252,11 @@ object NativeLibrary { external fun initializeSystem(reload: Boolean) - external fun defaultCPUCore(): Int - /** * Begins emulation. */ external fun run(path: String?) - /** - * Begins emulation from the specified savestate. - */ - external fun run(path: String?, savestatePath: String?, deleteSavestate: Boolean) - // Surface Handling external fun surfaceChanged(surf: Surface?) @@ -304,10 +297,9 @@ object NativeLibrary { */ external fun getCpuBackend(): String - /** - * Notifies the core emulation that the orientation has changed. - */ - external fun notifyOrientationChange(layout_option: Int, rotation: Int) + external fun applySettings() + + external fun logSettings() enum class CoreError { ErrorSystemFiles, @@ -538,6 +530,23 @@ object NativeLibrary { */ external fun isFirmwareAvailable(): Boolean + /** + * Checks the PatchManager for any addons that are available + * + * @param path Path to game file. Can be a [Uri]. + * @param programId String representation of a game's program ID + * @return Array of pairs where the first value is the name of an addon and the second is the version + */ + external fun getAddonsForFile(path: String, programId: String): Array>? + + /** + * Gets the save location for a specific game + * + * @param programId String representation of a game's program ID + * @return Save data path that may not exist yet + */ + external fun getSavePath(programId: String): String + /** * Button type for use in onTouchEvent */ 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 0000000000..15c7ca3c98 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AddonAdapter.kt @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.adapters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding +import org.yuzu.yuzu_emu.model.Addon + +class AddonAdapter : ListAdapter( + AsyncDifferConfig.Builder(DiffCallback()).build() +) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder { + ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false) + .also { return AddonViewHolder(it) } + } + + override fun getItemCount(): Int = currentList.size + + override fun onBindViewHolder(holder: AddonViewHolder, position: Int) = + holder.bind(currentList[position]) + + inner class AddonViewHolder(val binding: ListItemAddonBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind(addon: Addon) { + binding.root.setOnClickListener { + binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked + } + binding.title.text = addon.title + binding.version.text = addon.version + binding.addonSwitch.setOnCheckedChangeListener { _, checked -> + addon.enabled = checked + } + binding.addonSwitch.isChecked = addon.enabled + } + } + + private class DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Addon, newItem: Addon): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: Addon, newItem: Addon): Boolean { + return oldItem == newItem + } + } +} 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 a21a705c16..4a05c5be94 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 import org.yuzu.yuzu_emu.NativeLibrary import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication -import org.yuzu.yuzu_emu.databinding.CardAppletOptionBinding +import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding import org.yuzu.yuzu_emu.model.Applet import org.yuzu.yuzu_emu.model.AppletInfo import org.yuzu.yuzu_emu.model.Game @@ -28,7 +28,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List) : parent: ViewGroup, viewType: Int ): AppletAdapter.AppletViewHolder { - CardAppletOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) + CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false) .apply { root.setOnClickListener(this@AppletAdapter) } .also { return AppletViewHolder(it) } } @@ -65,7 +65,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List) : view.findNavController().navigate(action) } - inner class AppletViewHolder(val binding: CardAppletOptionBinding) : + inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) : RecyclerView.ViewHolder(binding.root) { lateinit var applet: Applet 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 2ef6385597..928bfe5a70 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 class GameAdapter(private val activity: AppCompatActivity) : ListAdapter(AsyncDifferConfig.Builder(DiffCallback()).build()), - View.OnClickListener { + View.OnClickListener, + View.OnLongClickListener { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { // Create a new view. val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false) binding.cardGame.setOnClickListener(this) + binding.cardGame.setOnLongClickListener(this) // Use that view to create a ViewHolder. return GameViewHolder(binding) } - override fun onBindViewHolder(holder: GameViewHolder, position: Int) { + override fun onBindViewHolder(holder: GameViewHolder, position: Int) = holder.bind(currentList[position]) - } override fun getItemCount(): Int = currentList.size @@ -125,10 +126,17 @@ class GameAdapter(private val activity: AppCompatActivity) : } } - val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game) + val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game, true) view.findNavController().navigate(action) } + override fun onLongClick(view: View): Boolean { + val holder = view.tag as GameViewHolder + val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(holder.game) + view.findNavController().navigate(action) + return true + } + inner class GameViewHolder(val binding: CardGameBinding) : RecyclerView.ViewHolder(binding.root) { lateinit var game: Game 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 0000000000..ff6270fa87 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.adapters + +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.res.ResourcesCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.launch +import org.yuzu.yuzu_emu.databinding.CardInstallableBinding +import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding +import org.yuzu.yuzu_emu.model.GameProperty +import org.yuzu.yuzu_emu.model.InstallableProperty +import org.yuzu.yuzu_emu.model.SubmenuProperty + +class GamePropertiesAdapter( + private val viewLifecycle: LifecycleOwner, + private var properties: List +) : + RecyclerView.Adapter() { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): GamePropertyViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + PropertyType.Submenu.ordinal -> { + SubmenuPropertyViewHolder( + CardSimpleOutlinedBinding.inflate( + inflater, + parent, + false + ) + ) + } + + else -> InstallablePropertyViewHolder( + CardInstallableBinding.inflate( + inflater, + parent, + false + ) + ) + } + } + + override fun getItemCount(): Int = properties.size + + override fun onBindViewHolder(holder: GamePropertyViewHolder, position: Int) = + holder.bind(properties[position]) + + override fun getItemViewType(position: Int): Int { + return when (properties[position]) { + is SubmenuProperty -> PropertyType.Submenu.ordinal + else -> PropertyType.Installable.ordinal + } + } + + sealed class GamePropertyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + abstract fun bind(property: GameProperty) + } + + inner class SubmenuPropertyViewHolder(val binding: CardSimpleOutlinedBinding) : + GamePropertyViewHolder(binding.root) { + override fun bind(property: GameProperty) { + val submenuProperty = property as SubmenuProperty + + binding.root.setOnClickListener { + submenuProperty.action.invoke() + } + + binding.title.setText(submenuProperty.titleId) + binding.description.setText(submenuProperty.descriptionId) + binding.icon.setImageDrawable( + ResourcesCompat.getDrawable( + binding.icon.context.resources, + submenuProperty.iconId, + binding.icon.context.theme + ) + ) + + binding.details.postDelayed({ + binding.details.isSelected = true + binding.details.ellipsize = TextUtils.TruncateAt.MARQUEE + }, 3000) + + if (submenuProperty.details != null) { + binding.details.visibility = View.VISIBLE + binding.details.text = submenuProperty.details.invoke() + } else if (submenuProperty.detailsFlow != null) { + binding.details.visibility = View.VISIBLE + viewLifecycle.lifecycleScope.launch { + viewLifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + submenuProperty.detailsFlow.collect { binding.details.text = it } + } + } + } else { + binding.details.visibility = View.GONE + } + } + } + + inner class InstallablePropertyViewHolder(val binding: CardInstallableBinding) : + GamePropertyViewHolder(binding.root) { + override fun bind(property: GameProperty) { + val installableProperty = property as InstallableProperty + + binding.title.setText(installableProperty.titleId) + binding.description.setText(installableProperty.descriptionId) + + if (installableProperty.install != null) { + binding.buttonInstall.visibility = View.VISIBLE + binding.buttonInstall.setOnClickListener { installableProperty.install.invoke() } + } + if (installableProperty.export != null) { + binding.buttonExport.visibility = View.VISIBLE + binding.buttonExport.setOnClickListener { installableProperty.export.invoke() } + } + } + } + + enum class PropertyType { + Submenu, + Installable + } +} 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 0000000000..0dce8ad8dc --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/AddonsFragment.kt @@ -0,0 +1,214 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.documentfile.provider.DocumentFile +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.findNavController +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.transition.MaterialSharedAxis +import kotlinx.coroutines.launch +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.adapters.AddonAdapter +import org.yuzu.yuzu_emu.databinding.FragmentAddonsBinding +import org.yuzu.yuzu_emu.model.AddonViewModel +import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.utils.AddonUtil +import org.yuzu.yuzu_emu.utils.FileUtil.copyFilesTo +import java.io.File + +class AddonsFragment : Fragment() { + private var _binding: FragmentAddonsBinding? = null + private val binding get() = _binding!! + + private val homeViewModel: HomeViewModel by activityViewModels() + private val addonViewModel: AddonViewModel by activityViewModels() + + private val args by navArgs() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + addonViewModel.onOpenAddons(args.game) + enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAddonsBinding.inflate(inflater) + return binding.root + } + + // This is using the correct scope, lint is just acting up + @SuppressLint("UnsafeRepeatOnLifecycleDetector") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + homeViewModel.setNavigationVisibility(visible = false, animated = false) + homeViewModel.setStatusBarShadeVisibility(false) + + binding.toolbarAddons.setNavigationOnClickListener { + binding.root.findNavController().popBackStack() + } + + binding.toolbarAddons.title = getString(R.string.addons_game, args.game.title) + + binding.listAddons.apply { + layoutManager = LinearLayoutManager(requireContext()) + adapter = AddonAdapter() + } + + viewLifecycleOwner.lifecycleScope.apply { + launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + addonViewModel.addonList.collect { + (binding.listAddons.adapter as AddonAdapter).submitList(it) + } + } + } + launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + addonViewModel.showModInstallPicker.collect { + if (it) { + installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data) + addonViewModel.showModInstallPicker(false) + } + } + } + } + launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + addonViewModel.showModNoticeDialog.collect { + if (it) { + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.addon_notice, + descriptionId = R.string.addon_notice_description, + positiveAction = { addonViewModel.showModInstallPicker(true) } + ).show(parentFragmentManager, MessageDialogFragment.TAG) + addonViewModel.showModNoticeDialog(false) + } + } + } + } + } + + binding.buttonInstall.setOnClickListener { + ContentTypeSelectionDialogFragment().show( + parentFragmentManager, + ContentTypeSelectionDialogFragment.TAG + ) + } + + setInsets() + } + + override fun onResume() { + super.onResume() + addonViewModel.refreshAddons() + } + + override fun onDestroy() { + super.onDestroy() + addonViewModel.onCloseAddons() + } + + val installAddon = + registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result -> + if (result == null) { + return@registerForActivityResult + } + + val externalAddonDirectory = DocumentFile.fromTreeUri(requireContext(), result) + if (externalAddonDirectory == null) { + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.invalid_directory, + descriptionId = R.string.invalid_directory_description + ).show(parentFragmentManager, MessageDialogFragment.TAG) + return@registerForActivityResult + } + + val isValid = externalAddonDirectory.listFiles() + .any { AddonUtil.validAddonDirectories.contains(it.name) } + val errorMessage = MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.invalid_directory, + descriptionId = R.string.invalid_directory_description + ) + if (isValid) { + IndeterminateProgressDialogFragment.newInstance( + requireActivity(), + R.string.installing_game_content, + false + ) { + val parentDirectoryName = externalAddonDirectory.name + val internalAddonDirectory = + File(args.game.addonDir + parentDirectoryName) + try { + externalAddonDirectory.copyFilesTo(internalAddonDirectory) + } catch (_: Exception) { + return@newInstance errorMessage + } + addonViewModel.refreshAddons() + return@newInstance getString(R.string.addon_installed_successfully) + }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) + } else { + errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG) + } + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { _: View, windowInsets: WindowInsetsCompat -> + val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + + val leftInsets = barInsets.left + cutoutInsets.left + val rightInsets = barInsets.right + cutoutInsets.right + + val mlpToolbar = binding.toolbarAddons.layoutParams as ViewGroup.MarginLayoutParams + mlpToolbar.leftMargin = leftInsets + mlpToolbar.rightMargin = rightInsets + binding.toolbarAddons.layoutParams = mlpToolbar + + val mlpAddonsList = binding.listAddons.layoutParams as ViewGroup.MarginLayoutParams + mlpAddonsList.leftMargin = leftInsets + mlpAddonsList.rightMargin = rightInsets + binding.listAddons.layoutParams = mlpAddonsList + binding.listAddons.updatePadding( + bottom = barInsets.bottom + + resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab) + ) + + val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab) + val mlpFab = + binding.buttonInstall.layoutParams as ViewGroup.MarginLayoutParams + mlpFab.leftMargin = leftInsets + fabSpacing + mlpFab.rightMargin = rightInsets + fabSpacing + mlpFab.bottomMargin = barInsets.bottom + fabSpacing + binding.buttonInstall.layoutParams = mlpFab + + windowInsets + } +} 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 0000000000..c1d8b9ea5f --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/ContentTypeSelectionDialogFragment.kt @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import androidx.preference.PreferenceManager +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.model.AddonViewModel +import org.yuzu.yuzu_emu.ui.main.MainActivity + +class ContentTypeSelectionDialogFragment : DialogFragment() { + private val addonViewModel: AddonViewModel by activityViewModels() + + private val preferences get() = + PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) + + private var selectedItem = 0 + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val launchOptions = + arrayOf(getString(R.string.updates_and_dlc), getString(R.string.mods_and_cheats)) + + if (savedInstanceState != null) { + selectedItem = savedInstanceState.getInt(SELECTED_ITEM) + } + + val mainActivity = requireActivity() as MainActivity + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.select_content_type) + .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> + when (selectedItem) { + 0 -> mainActivity.installGameUpdate.launch(arrayOf("*/*")) + else -> { + if (!preferences.getBoolean(MOD_NOTICE_SEEN, false)) { + preferences.edit().putBoolean(MOD_NOTICE_SEEN, true).apply() + addonViewModel.showModNoticeDialog(true) + return@setPositiveButton + } + addonViewModel.showModInstallPicker(true) + } + } + } + .setSingleChoiceItems(launchOptions, 0) { _: DialogInterface, i: Int -> + selectedItem = i + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putInt(SELECTED_ITEM, selectedItem) + } + + companion object { + const val TAG = "ContentTypeSelectionDialogFragment" + + private const val SELECTED_ITEM = "SelectedItem" + private const val MOD_NOTICE_SEEN = "ModNoticeSeen" + } +} 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 0000000000..fa2a4c9f9b --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GameInfoFragment.kt @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.navigation.findNavController +import androidx.navigation.fragment.navArgs +import com.google.android.material.transition.MaterialSharedAxis +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.databinding.FragmentGameInfoBinding +import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.utils.GameMetadata + +class GameInfoFragment : Fragment() { + private var _binding: FragmentGameInfoBinding? = null + private val binding get() = _binding!! + + private val homeViewModel: HomeViewModel by activityViewModels() + + private val args by navArgs() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + + // Check for an up-to-date version string + args.game.version = GameMetadata.getVersion(args.game.path, true) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentGameInfoBinding.inflate(inflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + homeViewModel.setNavigationVisibility(visible = false, animated = false) + homeViewModel.setStatusBarShadeVisibility(false) + + binding.apply { + toolbarInfo.title = args.game.title + toolbarInfo.setNavigationOnClickListener { + view.findNavController().popBackStack() + } + + val pathString = Uri.parse(args.game.path).path ?: "" + path.setHint(R.string.path) + pathField.setText(pathString) + pathField.setOnClickListener { copyToClipboard(getString(R.string.path), pathString) } + + programId.setHint(R.string.program_id) + programIdField.setText(args.game.programIdHex) + programIdField.setOnClickListener { + copyToClipboard(getString(R.string.program_id), args.game.programIdHex) + } + + if (args.game.developer.isNotEmpty()) { + developer.setHint(R.string.developer) + developerField.setText(args.game.developer) + developerField.setOnClickListener { + copyToClipboard(getString(R.string.developer), args.game.developer) + } + } else { + developer.visibility = View.GONE + } + + version.setHint(R.string.version) + versionField.setText(args.game.version) + versionField.setOnClickListener { + copyToClipboard(getString(R.string.version), args.game.version) + } + + buttonCopy.setOnClickListener { + val details = """ + ${args.game.title} + ${getString(R.string.path)} - $pathString + ${getString(R.string.program_id)} - ${args.game.programIdHex} + ${getString(R.string.developer)} - ${args.game.developer} + ${getString(R.string.version)} - ${args.game.version} + """.trimIndent() + copyToClipboard(args.game.title, details) + } + } + + setInsets() + } + + private fun copyToClipboard(label: String, body: String) { + val clipBoard = + requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText(label, body) + clipBoard.setPrimaryClip(clip) + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + Toast.makeText( + requireContext(), + R.string.copied_to_clipboard, + Toast.LENGTH_SHORT + ).show() + } + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { _: View, windowInsets: WindowInsetsCompat -> + val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + + val leftInsets = barInsets.left + cutoutInsets.left + val rightInsets = barInsets.right + cutoutInsets.right + + val mlpToolbar = binding.toolbarInfo.layoutParams as ViewGroup.MarginLayoutParams + mlpToolbar.leftMargin = leftInsets + mlpToolbar.rightMargin = rightInsets + binding.toolbarInfo.layoutParams = mlpToolbar + + val mlpScrollAbout = binding.scrollInfo.layoutParams as ViewGroup.MarginLayoutParams + mlpScrollAbout.leftMargin = leftInsets + mlpScrollAbout.rightMargin = rightInsets + binding.scrollInfo.layoutParams = mlpScrollAbout + + binding.contentInfo.updatePadding(bottom = barInsets.bottom) + + windowInsets + } +} 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 0000000000..485989e2e3 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/GamePropertiesFragment.kt @@ -0,0 +1,418 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.os.Bundle +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.findNavController +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.transition.MaterialSharedAxis +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.yuzu.yuzu_emu.HomeNavigationDirections +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.adapters.GamePropertiesAdapter +import org.yuzu.yuzu_emu.databinding.FragmentGamePropertiesBinding +import org.yuzu.yuzu_emu.features.settings.model.Settings +import org.yuzu.yuzu_emu.model.DriverViewModel +import org.yuzu.yuzu_emu.model.GameProperty +import org.yuzu.yuzu_emu.model.GamesViewModel +import org.yuzu.yuzu_emu.model.HomeViewModel +import org.yuzu.yuzu_emu.model.InstallableProperty +import org.yuzu.yuzu_emu.model.SubmenuProperty +import org.yuzu.yuzu_emu.model.TaskState +import org.yuzu.yuzu_emu.utils.DirectoryInitialization +import org.yuzu.yuzu_emu.utils.FileUtil +import org.yuzu.yuzu_emu.utils.GameIconUtils +import org.yuzu.yuzu_emu.utils.GpuDriverHelper +import org.yuzu.yuzu_emu.utils.MemoryUtil +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.File + +class GamePropertiesFragment : Fragment() { + private var _binding: FragmentGamePropertiesBinding? = null + private val binding get() = _binding!! + + private val homeViewModel: HomeViewModel by activityViewModels() + private val gamesViewModel: GamesViewModel by activityViewModels() + private val driverViewModel: DriverViewModel by activityViewModels() + + private val args by navArgs() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enterTransition = MaterialSharedAxis(MaterialSharedAxis.Y, true) + returnTransition = MaterialSharedAxis(MaterialSharedAxis.Y, false) + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentGamePropertiesBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + homeViewModel.setNavigationVisibility(visible = false, animated = true) + homeViewModel.setStatusBarShadeVisibility(true) + + binding.buttonBack.setOnClickListener { + view.findNavController().popBackStack() + } + + GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen) + binding.title.text = args.game.title + binding.title.postDelayed( + { + binding.title.ellipsize = TextUtils.TruncateAt.MARQUEE + binding.title.isSelected = true + }, + 3000 + ) + + binding.buttonStart.setOnClickListener { + LaunchGameDialogFragment.newInstance(args.game) + .show(childFragmentManager, LaunchGameDialogFragment.TAG) + } + + reloadList() + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + homeViewModel.openImportSaves.collect { + if (it) { + importSaves.launch(arrayOf("application/zip")) + homeViewModel.setOpenImportSaves(false) + } + } + } + } + + setInsets() + } + + override fun onDestroy() { + super.onDestroy() + gamesViewModel.reloadGames(true) + } + + private fun reloadList() { + _binding ?: return + + driverViewModel.updateDriverNameForGame(args.game) + val properties = mutableListOf().apply { + add( + SubmenuProperty( + R.string.info, + R.string.info_description, + R.drawable.ic_info_outline + ) { + val action = GamePropertiesFragmentDirections + .actionPerGamePropertiesFragmentToGameInfoFragment(args.game) + binding.root.findNavController().navigate(action) + } + ) + add( + SubmenuProperty( + R.string.preferences_settings, + R.string.per_game_settings_description, + R.drawable.ic_settings + ) { + val action = HomeNavigationDirections.actionGlobalSettingsActivity( + args.game, + Settings.MenuTag.SECTION_ROOT + ) + binding.root.findNavController().navigate(action) + } + ) + + if (!args.game.isHomebrew) { + add( + SubmenuProperty( + R.string.add_ons, + R.string.add_ons_description, + R.drawable.ic_edit + ) { + val action = GamePropertiesFragmentDirections + .actionPerGamePropertiesFragmentToAddonsFragment(args.game) + binding.root.findNavController().navigate(action) + } + ) + add( + InstallableProperty( + R.string.save_data, + R.string.save_data_description, + { + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.import_save_warning, + descriptionId = R.string.import_save_warning_description, + positiveAction = { homeViewModel.setOpenImportSaves(true) } + ).show(parentFragmentManager, MessageDialogFragment.TAG) + }, + if (File(args.game.saveDir).exists()) { + { exportSaves.launch(args.game.saveZipName) } + } else { + null + } + ) + ) + + val saveDirFile = File(args.game.saveDir) + if (saveDirFile.exists()) { + add( + SubmenuProperty( + R.string.delete_save_data, + R.string.delete_save_data_description, + R.drawable.ic_delete, + action = { + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.delete_save_data, + descriptionId = R.string.delete_save_data_warning_description, + positiveAction = { + File(args.game.saveDir).deleteRecursively() + Toast.makeText( + YuzuApplication.appContext, + R.string.save_data_deleted_successfully, + Toast.LENGTH_SHORT + ).show() + reloadList() + } + ).show(parentFragmentManager, MessageDialogFragment.TAG) + } + ) + ) + } + + val shaderCacheDir = File( + DirectoryInitialization.userDirectory + + "/shader/" + args.game.settingsName.lowercase() + ) + if (shaderCacheDir.exists()) { + add( + SubmenuProperty( + R.string.clear_shader_cache, + R.string.clear_shader_cache_description, + R.drawable.ic_delete, + { + if (shaderCacheDir.exists()) { + val bytes = shaderCacheDir.walkTopDown().filter { it.isFile } + .map { it.length() }.sum() + MemoryUtil.bytesToSizeUnit(bytes.toFloat()) + } else { + MemoryUtil.bytesToSizeUnit(0f) + } + } + ) { + shaderCacheDir.deleteRecursively() + Toast.makeText( + YuzuApplication.appContext, + R.string.cleared_shaders_successfully, + Toast.LENGTH_SHORT + ).show() + reloadList() + } + ) + } + } + } + binding.listProperties.apply { + layoutManager = + GridLayoutManager(requireContext(), resources.getInteger(R.integer.grid_columns)) + adapter = GamePropertiesAdapter(viewLifecycleOwner, properties) + } + } + + override fun onResume() { + super.onResume() + driverViewModel.updateDriverNameForGame(args.game) + } + + private fun setInsets() = + ViewCompat.setOnApplyWindowInsetsListener( + binding.root + ) { _: View, windowInsets: WindowInsetsCompat -> + val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout()) + + val leftInsets = barInsets.left + cutoutInsets.left + val rightInsets = barInsets.right + cutoutInsets.right + + val smallLayout = resources.getBoolean(R.bool.small_layout) + if (smallLayout) { + val mlpListAll = + binding.listAll.layoutParams as ViewGroup.MarginLayoutParams + mlpListAll.leftMargin = leftInsets + mlpListAll.rightMargin = rightInsets + binding.listAll.layoutParams = mlpListAll + } else { + if (ViewCompat.getLayoutDirection(binding.root) == + ViewCompat.LAYOUT_DIRECTION_LTR + ) { + val mlpListAll = + binding.listAll.layoutParams as ViewGroup.MarginLayoutParams + mlpListAll.rightMargin = rightInsets + binding.listAll.layoutParams = mlpListAll + + val mlpIconLayout = + binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams + mlpIconLayout.topMargin = barInsets.top + mlpIconLayout.leftMargin = leftInsets + binding.iconLayout!!.layoutParams = mlpIconLayout + } else { + val mlpListAll = + binding.listAll.layoutParams as ViewGroup.MarginLayoutParams + mlpListAll.leftMargin = leftInsets + binding.listAll.layoutParams = mlpListAll + + val mlpIconLayout = + binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams + mlpIconLayout.topMargin = barInsets.top + mlpIconLayout.rightMargin = rightInsets + binding.iconLayout!!.layoutParams = mlpIconLayout + } + } + + val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab) + val mlpFab = + binding.buttonStart.layoutParams as ViewGroup.MarginLayoutParams + mlpFab.leftMargin = leftInsets + fabSpacing + mlpFab.rightMargin = rightInsets + fabSpacing + mlpFab.bottomMargin = barInsets.bottom + fabSpacing + binding.buttonStart.layoutParams = mlpFab + + binding.layoutAll.updatePadding( + top = barInsets.top, + bottom = barInsets.bottom + + resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab) + ) + + windowInsets + } + + private val importSaves = + registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> + if (result == null) { + return@registerForActivityResult + } + + val inputZip = requireContext().contentResolver.openInputStream(result) + val savesFolder = File(args.game.saveDir) + val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/") + cacheSaveDir.mkdir() + + if (inputZip == null) { + Toast.makeText( + YuzuApplication.appContext, + getString(R.string.fatal_error), + Toast.LENGTH_LONG + ).show() + return@registerForActivityResult + } + + IndeterminateProgressDialogFragment.newInstance( + requireActivity(), + R.string.save_files_importing, + false + ) { + try { + FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) + val files = cacheSaveDir.listFiles() + var savesFolderFile: File? = null + if (files != null) { + val savesFolderName = args.game.programIdHex + for (file in files) { + if (file.isDirectory && file.name == savesFolderName) { + savesFolderFile = file + break + } + } + } + + if (savesFolderFile != null) { + savesFolder.deleteRecursively() + savesFolder.mkdir() + savesFolderFile.copyRecursively(savesFolder) + savesFolderFile.deleteRecursively() + } + + withContext(Dispatchers.Main) { + if (savesFolderFile == null) { + MessageDialogFragment.newInstance( + requireActivity(), + titleId = R.string.save_file_invalid_zip_structure, + descriptionId = R.string.save_file_invalid_zip_structure_description + ).show(parentFragmentManager, MessageDialogFragment.TAG) + return@withContext + } + Toast.makeText( + YuzuApplication.appContext, + getString(R.string.save_file_imported_success), + Toast.LENGTH_LONG + ).show() + reloadList() + } + + cacheSaveDir.deleteRecursively() + } catch (e: Exception) { + Toast.makeText( + YuzuApplication.appContext, + getString(R.string.fatal_error), + Toast.LENGTH_LONG + ).show() + } + }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) + } + + /** + * Exports the save file located in the given folder path by creating a zip file and opening a + * file picker to save. + */ + private val exportSaves = registerForActivityResult( + ActivityResultContracts.CreateDocument("application/zip") + ) { result -> + if (result == null) { + return@registerForActivityResult + } + + IndeterminateProgressDialogFragment.newInstance( + requireActivity(), + R.string.save_files_exporting, + false + ) { + val saveLocation = args.game.saveDir + val zipResult = FileUtil.zipFromInternalStorage( + File(saveLocation), + saveLocation.replaceAfterLast("/", ""), + BufferedOutputStream(requireContext().contentResolver.openOutputStream(result)) + ) + return@newInstance when (zipResult) { + TaskState.Completed -> getString(R.string.export_success) + TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) + } + }.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG) + } +} diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt index 7e467814d9..8847e5531a 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() { activity: FragmentActivity, titleId: Int, cancellable: Boolean = false, - task: () -> Any + task: suspend () -> Any ): IndeterminateProgressDialogFragment { val dialog = IndeterminateProgressDialogFragment() val args = Bundle() 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 0000000000..f653826a61 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/LaunchGameDialogFragment.kt @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.fragments + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import androidx.navigation.fragment.findNavController +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.yuzu.yuzu_emu.HomeNavigationDirections +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.model.Game +import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable + +class LaunchGameDialogFragment : DialogFragment() { + private var selectedItem = 0 + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val game = requireArguments().parcelable(GAME) + val launchOptions = arrayOf(getString(R.string.global), getString(R.string.custom)) + + if (savedInstanceState != null) { + selectedItem = savedInstanceState.getInt(SELECTED_ITEM) + } + + return MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.launch_options) + .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> + val action = HomeNavigationDirections + .actionGlobalEmulationActivity(game, selectedItem != 0) + requireParentFragment().findNavController().navigate(action) + } + .setSingleChoiceItems(launchOptions, 0) { _: DialogInterface, i: Int -> + selectedItem = i + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putInt(SELECTED_ITEM, selectedItem) + } + + companion object { + const val TAG = "LaunchGameDialogFragment" + + const val GAME = "Game" + const val SELECTED_ITEM = "SelectedItem" + + fun newInstance(game: Game): LaunchGameDialogFragment { + val args = Bundle() + args.putParcelable(GAME, game) + val fragment = LaunchGameDialogFragment() + fragment.arguments = args + return fragment + } + } +} 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 a6183d19eb..32062b6fee 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() { val descriptionString = requireArguments().getString(DESCRIPTION_STRING)!! val helpLinkId = requireArguments().getInt(HELP_LINK) - val dialog = MaterialAlertDialogBuilder(requireContext()) - .setPositiveButton(R.string.close, null) + val builder = MaterialAlertDialogBuilder(requireContext()) - if (titleId != 0) dialog.setTitle(titleId) - if (titleString.isNotEmpty()) dialog.setTitle(titleString) + if (messageDialogViewModel.positiveAction == null) { + builder.setPositiveButton(R.string.close, null) + } else { + builder.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> + messageDialogViewModel.positiveAction?.invoke() + }.setNegativeButton(android.R.string.cancel, null) + } + + if (titleId != 0) builder.setTitle(titleId) + if (titleString.isNotEmpty()) builder.setTitle(titleString) if (descriptionId != 0) { - dialog.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY)) + builder.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY)) } - if (descriptionString.isNotEmpty()) dialog.setMessage(descriptionString) + if (descriptionString.isNotEmpty()) builder.setMessage(descriptionString) if (helpLinkId != 0) { - dialog.setNeutralButton(R.string.learn_more) { _, _ -> + builder.setNeutralButton(R.string.learn_more) { _, _ -> openLink(getString(helpLinkId)) } } - return dialog.show() - } - - override fun onDismiss(dialog: DialogInterface) { - super.onDismiss(dialog) - messageDialogViewModel.dismissAction.invoke() - messageDialogViewModel.clear() + return builder.show() } private fun openLink(link: String) { @@ -74,7 +75,7 @@ class MessageDialogFragment : DialogFragment() { descriptionId: Int = 0, descriptionString: String = "", helpLinkId: Int = 0, - dismissAction: () -> Unit = {} + positiveAction: (() -> Unit)? = null ): MessageDialogFragment { val dialog = MessageDialogFragment() val bundle = Bundle() @@ -85,8 +86,10 @@ class MessageDialogFragment : DialogFragment() { putString(DESCRIPTION_STRING, descriptionString) putInt(HELP_LINK, helpLinkId) } - ViewModelProvider(activity)[MessageDialogViewModel::class.java].dismissAction = - dismissAction + ViewModelProvider(activity)[MessageDialogViewModel::class.java].apply { + clear() + this.positiveAction = positiveAction + } dialog.arguments = bundle return dialog } 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 2dbca76a59..3ac054d8fa 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 @@ -60,7 +60,9 @@ class SearchFragment : Fragment() { // This is using the correct scope, lint is just acting up @SuppressLint("UnsafeRepeatOnLifecycleDetector") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - homeViewModel.setNavigationVisibility(visible = true, animated = false) + super.onViewCreated(view, savedInstanceState) + homeViewModel.setNavigationVisibility(visible = true, animated = true) + homeViewModel.setStatusBarShadeVisibility(true) preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) if (savedInstanceState != 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 0000000000..ed79a8b028 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/Addon.kt @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +data class Addon( + var enabled: Boolean, + val title: String, + val version: String +) 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 0000000000..075252f5bc --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/AddonViewModel.kt @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.utils.NativeConfig +import java.util.concurrent.atomic.AtomicBoolean + +class AddonViewModel : ViewModel() { + private val _addonList = MutableStateFlow(mutableListOf()) + val addonList get() = _addonList.asStateFlow() + + private val _showModInstallPicker = MutableStateFlow(false) + val showModInstallPicker get() = _showModInstallPicker.asStateFlow() + + private val _showModNoticeDialog = MutableStateFlow(false) + val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow() + + var game: Game? = null + + private val isRefreshing = AtomicBoolean(false) + + fun onOpenAddons(game: Game) { + this.game = game + refreshAddons() + } + + fun refreshAddons() { + if (isRefreshing.get() || game == null) { + return + } + isRefreshing.set(true) + viewModelScope.launch { + withContext(Dispatchers.IO) { + val addonList = mutableListOf() + val disabledAddons = NativeConfig.getDisabledAddons(game!!.programId) + NativeLibrary.getAddonsForFile(game!!.path, game!!.programId)?.forEach { + val name = it.first.replace("[D] ", "") + addonList.add(Addon(!disabledAddons.contains(name), name, it.second)) + } + addonList.sortBy { it.title } + _addonList.value = addonList + isRefreshing.set(false) + } + } + } + + fun onCloseAddons() { + if (_addonList.value.isEmpty()) { + return + } + + NativeConfig.setDisabledAddons( + game!!.programId, + _addonList.value.mapNotNull { + if (it.enabled) { + null + } else { + it.title + } + }.toTypedArray() + ) + NativeConfig.saveGlobalConfig() + _addonList.value.clear() + game = null + } + + fun showModInstallPicker(install: Boolean) { + _showModInstallPicker.value = install + } + + fun showModNoticeDialog(show: Boolean) { + _showModNoticeDialog.value = show + } +} 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 2fa3ab31bb..ac642c16e4 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 @@ package org.yuzu.yuzu_emu.model +import android.net.Uri import android.os.Parcelable import java.util.HashSet import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable +import org.yuzu.yuzu_emu.NativeLibrary +import org.yuzu.yuzu_emu.R +import org.yuzu.yuzu_emu.YuzuApplication +import org.yuzu.yuzu_emu.utils.DirectoryInitialization +import org.yuzu.yuzu_emu.utils.FileUtil +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter @Parcelize @Serializable @@ -15,12 +23,44 @@ class Game( val path: String, val programId: String = "", val developer: String = "", - val version: String = "", + var version: String = "", val isHomebrew: Boolean = false ) : Parcelable { val keyAddedToLibraryTime get() = "${path}_AddedToLibraryTime" val keyLastPlayedTime get() = "${path}_LastPlayed" + val settingsName: String + get() { + val programIdLong = programId.toLong() + return if (programIdLong == 0L) { + FileUtil.getFilename(Uri.parse(path)) + } else { + "0" + programIdLong.toString(16).uppercase() + } + } + + val programIdHex: String + get() { + val programIdLong = programId.toLong() + return if (programIdLong == 0L) { + "0" + } else { + "0" + programIdLong.toString(16).uppercase() + } + } + + val saveZipName: String + get() = "$title ${YuzuApplication.appContext.getString(R.string.save_data).lowercase()} - ${ + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")) + }.zip" + + val saveDir: String + get() = DirectoryInitialization.userDirectory + "/nand" + + NativeLibrary.getSavePath(programId) + + val addonDir: String + get() = DirectoryInitialization.userDirectory + "/load/" + programIdHex + "/" + override fun equals(other: Any?): Boolean { if (other !is Game) { return false 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 0000000000..bb3df5bd00 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GameProperties.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.model + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import kotlinx.coroutines.flow.StateFlow + +interface GameProperty { + @get:StringRes + val titleId: Int + get() = -1 + + @get:StringRes + val descriptionId: Int + get() = -1 +} + +data class SubmenuProperty( + override val titleId: Int, + override val descriptionId: Int, + @DrawableRes val iconId: Int, + val details: (() -> String)? = null, + val detailsFlow: StateFlow? = null, + val action: () -> Unit +) : GameProperty + +data class InstallableProperty( + override val titleId: Int, + override val descriptionId: Int, + val install: (() -> Unit)? = null, + val export: (() -> Unit)? = null +) : GameProperty 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 07e65b028d..d801db1054 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 @@ package org.yuzu.yuzu_emu.model +import android.net.Uri import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -21,6 +22,12 @@ class HomeViewModel : ViewModel() { private val _gamesDirSelected = MutableStateFlow(false) val gamesDirSelected get() = _gamesDirSelected.asStateFlow() + private val _openImportSaves = MutableStateFlow(false) + val openImportSaves get() = _openImportSaves.asStateFlow() + + private val _contentToInstall = MutableStateFlow?>(null) + val contentToInstall get() = _contentToInstall.asStateFlow() + var navigatedToSetup = false fun setNavigationVisibility(visible: Boolean, animated: Boolean) { @@ -44,4 +51,12 @@ class HomeViewModel : ViewModel() { fun setGamesDirSelected(selected: Boolean) { _gamesDirSelected.value = selected } + + fun setOpenImportSaves(import: Boolean) { + _openImportSaves.value = import + } + + fun setContentToInstall(documents: List?) { + _contentToInstall.value = documents + } } 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 36ffd08d28..641c5cb177 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 import androidx.lifecycle.ViewModel class MessageDialogViewModel : ViewModel() { - var dismissAction: () -> Unit = {} + var positiveAction: (() -> Unit)? = null fun clear() { - dismissAction = {} + positiveAction = null } } 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 16a794deeb..e59c957335 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() { val cancelled: StateFlow get() = _cancelled private val _cancelled = MutableStateFlow(false) - lateinit var task: () -> Any + lateinit var task: suspend () -> Any fun clear() { _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 805b89b31d..d5acf84791 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 import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.color.MaterialColors -import com.google.android.material.transition.MaterialFadeThrough +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.adapters.GameAdapter @@ -35,11 +35,6 @@ class GamesFragment : Fragment() { private val gamesViewModel: GamesViewModel by activityViewModels() private val homeViewModel: HomeViewModel by activityViewModels() - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enterTransition = MaterialFadeThrough() - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -52,7 +47,9 @@ class GamesFragment : Fragment() { // This is using the correct scope, lint is just acting up @SuppressLint("UnsafeRepeatOnLifecycleDetector") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - homeViewModel.setNavigationVisibility(visible = true, animated = false) + super.onViewCreated(view, savedInstanceState) + homeViewModel.setNavigationVisibility(visible = true, animated = true) + homeViewModel.setStatusBarShadeVisibility(true) binding.gridGames.apply { layoutManager = AutofitGridLayoutManager( 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 16323a316a..09ddd1bbd0 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 @@ -43,7 +43,7 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment import org.yuzu.yuzu_emu.fragments.MessageDialogFragment -import org.yuzu.yuzu_emu.getPublicFilesDir +import org.yuzu.yuzu_emu.model.AddonViewModel import org.yuzu.yuzu_emu.model.GamesViewModel import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.model.TaskState @@ -60,15 +60,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider { private val homeViewModel: HomeViewModel by viewModels() private val gamesViewModel: GamesViewModel by viewModels() private val taskViewModel: TaskViewModel by viewModels() + private val addonViewModel: AddonViewModel by viewModels() override var themeId: Int = 0 - private val savesFolder - get() = "${getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000" - - // Get first subfolder in saves folder (should be the user folder) - val savesFolderRoot get() = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: "" - override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen() splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady } @@ -145,6 +140,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider { homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) } } } + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + homeViewModel.contentToInstall.collect { + if (it != null) { + installContent(it) + homeViewModel.setContentToInstall(null) + } + } + } + } } // Dismiss previous notifications (should not happen unless a crash occurred) @@ -468,110 +473,150 @@ class MainActivity : AppCompatActivity(), ThemeProvider { val installGameUpdate = registerForActivityResult( ActivityResultContracts.OpenMultipleDocuments() ) { documents: List -> - if (documents.isNotEmpty()) { - IndeterminateProgressDialogFragment.newInstance( - this@MainActivity, - R.string.installing_game_content - ) { - var installSuccess = 0 - var installOverwrite = 0 - var errorBaseGame = 0 - var errorExtension = 0 - var errorOther = 0 - documents.forEach { - when ( - NativeLibrary.installFileToNand( - it.toString(), - FileUtil.getExtension(it) - ) - ) { - NativeLibrary.InstallFileToNandResult.Success -> { - installSuccess += 1 - } - - NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> { - installOverwrite += 1 - } - - NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> { - errorBaseGame += 1 - } - - NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> { - errorExtension += 1 - } - - else -> { - errorOther += 1 - } - } - } - - val separator = System.getProperty("line.separator") ?: "\n" - val installResult = StringBuilder() - if (installSuccess > 0) { - installResult.append( - getString( - R.string.install_game_content_success_install, - installSuccess - ) - ) - installResult.append(separator) - } - if (installOverwrite > 0) { - installResult.append( - getString( - R.string.install_game_content_success_overwrite, - installOverwrite - ) - ) - installResult.append(separator) - } - val errorTotal: Int = errorBaseGame + errorExtension + errorOther - if (errorTotal > 0) { - installResult.append(separator) - installResult.append( - getString( - R.string.install_game_content_failed_count, - errorTotal - ) - ) - installResult.append(separator) - if (errorBaseGame > 0) { - installResult.append(separator) - installResult.append( - getString(R.string.install_game_content_failure_base) - ) - installResult.append(separator) - } - if (errorExtension > 0) { - installResult.append(separator) - installResult.append( - getString(R.string.install_game_content_failure_file_extension) - ) - installResult.append(separator) - } - if (errorOther > 0) { - installResult.append( - getString(R.string.install_game_content_failure_description) - ) - installResult.append(separator) - } - return@newInstance MessageDialogFragment.newInstance( - this, - titleId = R.string.install_game_content_failure, - descriptionString = installResult.toString().trim(), - helpLinkId = R.string.install_game_content_help_link - ) - } else { - return@newInstance MessageDialogFragment.newInstance( - this, - titleId = R.string.install_game_content_success, - descriptionString = installResult.toString().trim() - ) - } - }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) + if (documents.isEmpty()) { + return@registerForActivityResult } + + if (addonViewModel.game == null) { + installContent(documents) + return@registerForActivityResult + } + + IndeterminateProgressDialogFragment.newInstance( + this@MainActivity, + R.string.verifying_content, + false + ) { + var updatesMatchProgram = true + for (document in documents) { + val valid = NativeLibrary.doesUpdateMatchProgram( + addonViewModel.game!!.programId, + document.toString() + ) + if (!valid) { + updatesMatchProgram = false + break + } + } + + if (updatesMatchProgram) { + homeViewModel.setContentToInstall(documents) + } else { + MessageDialogFragment.newInstance( + this@MainActivity, + titleId = R.string.content_install_notice, + descriptionId = R.string.content_install_notice_description, + positiveAction = { homeViewModel.setContentToInstall(documents) } + ) + } + }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) + } + + private fun installContent(documents: List) { + IndeterminateProgressDialogFragment.newInstance( + this@MainActivity, + R.string.installing_game_content + ) { + var installSuccess = 0 + var installOverwrite = 0 + var errorBaseGame = 0 + var errorExtension = 0 + var errorOther = 0 + documents.forEach { + when ( + NativeLibrary.installFileToNand( + it.toString(), + FileUtil.getExtension(it) + ) + ) { + NativeLibrary.InstallFileToNandResult.Success -> { + installSuccess += 1 + } + + NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> { + installOverwrite += 1 + } + + NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> { + errorBaseGame += 1 + } + + NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> { + errorExtension += 1 + } + + else -> { + errorOther += 1 + } + } + } + + addonViewModel.refreshAddons() + + val separator = System.getProperty("line.separator") ?: "\n" + val installResult = StringBuilder() + if (installSuccess > 0) { + installResult.append( + getString( + R.string.install_game_content_success_install, + installSuccess + ) + ) + installResult.append(separator) + } + if (installOverwrite > 0) { + installResult.append( + getString( + R.string.install_game_content_success_overwrite, + installOverwrite + ) + ) + installResult.append(separator) + } + val errorTotal: Int = errorBaseGame + errorExtension + errorOther + if (errorTotal > 0) { + installResult.append(separator) + installResult.append( + getString( + R.string.install_game_content_failed_count, + errorTotal + ) + ) + installResult.append(separator) + if (errorBaseGame > 0) { + installResult.append(separator) + installResult.append( + getString(R.string.install_game_content_failure_base) + ) + installResult.append(separator) + } + if (errorExtension > 0) { + installResult.append(separator) + installResult.append( + getString(R.string.install_game_content_failure_file_extension) + ) + installResult.append(separator) + } + if (errorOther > 0) { + installResult.append( + getString(R.string.install_game_content_failure_description) + ) + installResult.append(separator) + } + return@newInstance MessageDialogFragment.newInstance( + this, + titleId = R.string.install_game_content_failure, + descriptionString = installResult.toString().trim(), + helpLinkId = R.string.install_game_content_help_link + ) + } else { + return@newInstance MessageDialogFragment.newInstance( + this, + titleId = R.string.install_game_content_success, + descriptionString = installResult.toString().trim() + ) + } + }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) } val exportUserData = registerForActivityResult( @@ -657,102 +702,4 @@ class MainActivity : AppCompatActivity(), ThemeProvider { return@newInstance getString(R.string.user_data_import_success) }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) } - - /** - * Exports the save file located in the given folder path by creating a zip file and sharing it via intent. - */ - val exportSaves = registerForActivityResult( - ActivityResultContracts.CreateDocument("application/zip") - ) { result -> - if (result == null) { - return@registerForActivityResult - } - - IndeterminateProgressDialogFragment.newInstance( - this, - R.string.save_files_exporting, - false - ) { - val zipResult = FileUtil.zipFromInternalStorage( - File(savesFolderRoot), - savesFolderRoot, - BufferedOutputStream(contentResolver.openOutputStream(result)) - ) - return@newInstance when (zipResult) { - TaskState.Completed -> getString(R.string.export_success) - TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed) - } - }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) - } - - private val startForResultExportSave = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ -> - File(getPublicFilesDir().canonicalPath, "temp").deleteRecursively() - } - - val importSaves = - registerForActivityResult(ActivityResultContracts.OpenDocument()) { result -> - if (result == null) { - return@registerForActivityResult - } - - NativeLibrary.initializeEmptyUserDirectory() - - val inputZip = contentResolver.openInputStream(result) - // A zip needs to have at least one subfolder named after a TitleId in order to be considered valid. - var validZip = false - val savesFolder = File(savesFolderRoot) - val cacheSaveDir = File("${applicationContext.cacheDir.path}/saves/") - cacheSaveDir.mkdir() - - if (inputZip == null) { - Toast.makeText( - applicationContext, - getString(R.string.fatal_error), - Toast.LENGTH_LONG - ).show() - return@registerForActivityResult - } - - val filterTitleId = - FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) } - - try { - CoroutineScope(Dispatchers.IO).launch { - FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir) - cacheSaveDir.list(filterTitleId)?.forEach { savePath -> - File(savesFolder, savePath).deleteRecursively() - File(cacheSaveDir, savePath).copyRecursively( - File(savesFolder, savePath), - true - ) - validZip = true - } - - withContext(Dispatchers.Main) { - if (!validZip) { - MessageDialogFragment.newInstance( - this@MainActivity, - titleId = R.string.save_file_invalid_zip_structure, - descriptionId = R.string.save_file_invalid_zip_structure_description - ).show(supportFragmentManager, MessageDialogFragment.TAG) - return@withContext - } - Toast.makeText( - applicationContext, - getString(R.string.save_file_imported_success), - Toast.LENGTH_LONG - ).show() - } - - cacheSaveDir.deleteRecursively() - } - } catch (e: Exception) { - Toast.makeText( - applicationContext, - getString(R.string.fatal_error), - Toast.LENGTH_LONG - ).show() - } - } } 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 0000000000..8cc5ea71f2 --- /dev/null +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/AddonUtil.kt @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2023 yuzu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.yuzu.yuzu_emu.utils + +object AddonUtil { + val validAddonDirectories = listOf("cheats", "exefs", "romfs") +} 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 bbe7bfa922..00c6bf90e7 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 import java.lang.NullPointerException import java.nio.charset.StandardCharsets import java.util.zip.ZipOutputStream +import kotlin.IllegalStateException object FileUtil { const val PATH_TREE = "tree" @@ -342,6 +343,37 @@ object FileUtil { return TaskState.Completed } + /** + * Helper function that copies the contents of a DocumentFile folder into a [File] + * @param file [File] representation of the folder to copy into + * @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa + */ + fun DocumentFile.copyFilesTo(file: File) { + file.mkdirs() + if (!this.isDirectory || !file.isDirectory) { + throw IllegalStateException( + "[FileUtil] Tried to copy a folder into a file or vice versa" + ) + } + + this.listFiles().forEach { + val newFile = File(file, it.name!!) + if (it.isDirectory) { + newFile.mkdirs() + DocumentFile.fromTreeUri(YuzuApplication.appContext, it.uri)?.copyFilesTo(newFile) + } else { + val inputStream = + YuzuApplication.appContext.contentResolver.openInputStream(it.uri) + BufferedInputStream(inputStream).use { bos -> + if (!newFile.exists()) { + newFile.createNewFile() + } + newFile.outputStream().use { os -> bos.copyTo(os) } + } + } + } + } + fun isRootTreeUri(uri: Uri): Boolean { val paths = uri.pathSegments return paths.size == 2 && PATH_TREE == paths[0] 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 4c7316ba39..7d629b7d54 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 @@ -105,4 +105,23 @@ object NativeConfig { */ @Synchronized external fun addGameDir(dir: GameDir) + + /** + * Gets an array of the addons that are disabled for a given game + * + * @param programId String representation of a game's program ID + * @return An array of disabled addons + */ + @Synchronized + external fun getDisabledAddons(programId: String): Array + + /** + * Clears the disabled addons array corresponding to [programId] and replaces them + * with [disabledAddons] + * + * @param programId String representation of a game's program ID + * @param disabledAddons Replacement array of disabled addons + */ + @Synchronized + external fun setDisabledAddons(programId: String, disabledAddons: Array) } diff --git a/src/android/app/src/main/jni/id_cache.cpp b/src/android/app/src/main/jni/id_cache.cpp index a56ed56629..df89351783 100644 --- a/src/android/app/src/main/jni/id_cache.cpp +++ b/src/android/app/src/main/jni/id_cache.cpp @@ -20,6 +20,12 @@ static jmethodID s_disk_cache_load_progress; static jmethodID s_on_emulation_started; static jmethodID s_on_emulation_stopped; +static jclass s_string_class; +static jclass s_pair_class; +static jmethodID s_pair_constructor; +static jfieldID s_pair_first_field; +static jfieldID s_pair_second_field; + static constexpr jint JNI_VERSION = JNI_VERSION_1_6; namespace IDCache { @@ -79,6 +85,26 @@ jmethodID GetOnEmulationStopped() { return s_on_emulation_stopped; } +jclass GetStringClass() { + return s_string_class; +} + +jclass GetPairClass() { + return s_pair_class; +} + +jmethodID GetPairConstructor() { + return s_pair_constructor; +} + +jfieldID GetPairFirstField() { + return s_pair_first_field; +} + +jfieldID GetPairSecondField() { + return s_pair_second_field; +} + } // namespace IDCache #ifdef __cplusplus @@ -115,6 +141,18 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) { s_on_emulation_stopped = env->GetStaticMethodID(s_native_library_class, "onEmulationStopped", "(I)V"); + const jclass string_class = env->FindClass("java/lang/String"); + s_string_class = reinterpret_cast(env->NewGlobalRef(string_class)); + env->DeleteLocalRef(string_class); + + const jclass pair_class = env->FindClass("kotlin/Pair"); + s_pair_class = reinterpret_cast(env->NewGlobalRef(pair_class)); + s_pair_constructor = + env->GetMethodID(pair_class, "", "(Ljava/lang/Object;Ljava/lang/Object;)V"); + s_pair_first_field = env->GetFieldID(pair_class, "first", "Ljava/lang/Object;"); + s_pair_second_field = env->GetFieldID(pair_class, "second", "Ljava/lang/Object;"); + env->DeleteLocalRef(pair_class); + // Initialize Android Storage Common::FS::Android::RegisterCallbacks(env, s_native_library_class); @@ -136,6 +174,8 @@ void JNI_OnUnload(JavaVM* vm, void* reserved) { env->DeleteGlobalRef(s_disk_cache_progress_class); env->DeleteGlobalRef(s_load_callback_stage_class); env->DeleteGlobalRef(s_game_dir_class); + env->DeleteGlobalRef(s_string_class); + env->DeleteGlobalRef(s_pair_class); // UnInitialize applets 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 855649efaf..36233423eb 100644 --- a/src/android/app/src/main/jni/id_cache.h +++ b/src/android/app/src/main/jni/id_cache.h @@ -20,4 +20,10 @@ jmethodID GetDiskCacheLoadProgress(); jmethodID GetOnEmulationStarted(); jmethodID GetOnEmulationStopped(); +jclass GetStringClass(); +jclass GetPairClass(); +jmethodID GetPairConstructor(); +jfieldID GetPairFirstField(); +jfieldID GetPairSecondField(); + } // namespace IDCache diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index e5d3158c83..ce570b811a 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -79,6 +80,10 @@ Core::System& EmulationSession::System() { return m_system; } +FileSys::ManualContentProvider* EmulationSession::ContentProvider() { + return m_manual_provider.get(); +} + const EmuWindow_Android& EmulationSession::Window() const { return *m_window; } @@ -455,6 +460,15 @@ void EmulationSession::OnEmulationStopped(Core::SystemResultStatus result) { static_cast(result)); } +u64 EmulationSession::GetProgramId(JNIEnv* env, jstring jprogramId) { + auto program_id_string = GetJString(env, jprogramId); + try { + return std::stoull(program_id_string); + } catch (...) { + return 0; + } +} + static Core::SystemResultStatus RunEmulation(const std::string& filepath) { MicroProfileOnThreadCreate("EmuThread"); SCOPE_EXIT({ MicroProfileShutdown(); }); @@ -504,6 +518,27 @@ int Java_org_yuzu_yuzu_1emu_NativeLibrary_installFileToNand(JNIEnv* env, jobject GetJString(env, j_file_extension)); } +jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_doesUpdateMatchProgram(JNIEnv* env, jobject jobj, + jstring jprogramId, + jstring jupdatePath) { + u64 program_id = EmulationSession::GetProgramId(env, jprogramId); + std::string updatePath = GetJString(env, jupdatePath); + std::shared_ptr nsp = std::make_shared( + EmulationSession::GetInstance().System().GetFilesystem()->OpenFile(updatePath, + FileSys::Mode::Read)); + for (const auto& item : nsp->GetNCAs()) { + for (const auto& nca_details : item.second) { + if (nca_details.second->GetName().ends_with(".cnmt.nca")) { + auto update_id = nca_details.second->GetTitleId() & ~0xFFFULL; + if (update_id == program_id) { + return true; + } + } + } + } + return false; +} + void JNICALL Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeGpuDriver(JNIEnv* env, jclass clazz, jstring hook_lib_dir, jstring custom_driver_dir, @@ -665,13 +700,6 @@ void Java_org_yuzu_yuzu_1emu_NativeLibrary_initializeSystem(JNIEnv* env, jclass EmulationSession::GetInstance().InitializeSystem(reload); } -jint Java_org_yuzu_yuzu_1emu_NativeLibrary_defaultCPUCore(JNIEnv* env, jclass clazz) { - return {}; -} - -void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2Ljava_lang_String_2Z( - JNIEnv* env, jclass clazz, jstring j_file, jstring j_savestate, jboolean j_delete_savestate) {} - jdoubleArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getPerfStats(JNIEnv* env, jclass clazz) { jdoubleArray j_stats = env->NewDoubleArray(4); @@ -696,9 +724,13 @@ jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getCpuBackend(JNIEnv* env, jclass return ToJString(env, "JIT"); } -void Java_org_yuzu_yuzu_1emu_utils_DirectoryInitialization_setSysDirectory(JNIEnv* env, - jclass clazz, - jstring j_path) {} +void Java_org_yuzu_yuzu_1emu_NativeLibrary_applySettings(JNIEnv* env, jobject jobj) { + EmulationSession::GetInstance().System().ApplySettings(); +} + +void Java_org_yuzu_yuzu_1emu_NativeLibrary_logSettings(JNIEnv* env, jobject jobj) { + Settings::LogSettings(); +} void Java_org_yuzu_yuzu_1emu_NativeLibrary_run__Ljava_lang_String_2(JNIEnv* env, jclass clazz, jstring j_path) { @@ -792,4 +824,60 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isFirmwareAvailable(JNIEnv* env, return true; } +jobjectArray Java_org_yuzu_yuzu_1emu_NativeLibrary_getAddonsForFile(JNIEnv* env, jobject jobj, + jstring jpath, + jstring jprogramId) { + const auto path = GetJString(env, jpath); + const auto vFile = + Core::GetGameFileFromPath(EmulationSession::GetInstance().System().GetFilesystem(), path); + if (vFile == nullptr) { + return nullptr; + } + + auto& system = EmulationSession::GetInstance().System(); + auto program_id = EmulationSession::GetProgramId(env, jprogramId); + const FileSys::PatchManager pm{program_id, system.GetFileSystemController(), + system.GetContentProvider()}; + const auto loader = Loader::GetLoader(system, vFile); + + FileSys::VirtualFile update_raw; + loader->ReadUpdateRaw(update_raw); + + auto addons = pm.GetPatchVersionNames(update_raw); + auto jemptyString = ToJString(env, ""); + auto jemptyStringPair = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(), + jemptyString, jemptyString); + jobjectArray jaddonsArray = + env->NewObjectArray(addons.size(), IDCache::GetPairClass(), jemptyStringPair); + int i = 0; + for (const auto& addon : addons) { + jobject jaddon = env->NewObject(IDCache::GetPairClass(), IDCache::GetPairConstructor(), + ToJString(env, addon.first), ToJString(env, addon.second)); + env->SetObjectArrayElement(jaddonsArray, i, jaddon); + ++i; + } + return jaddonsArray; +} + +jstring Java_org_yuzu_yuzu_1emu_NativeLibrary_getSavePath(JNIEnv* env, jobject jobj, + jstring jprogramId) { + auto program_id = EmulationSession::GetProgramId(env, jprogramId); + + auto& system = EmulationSession::GetInstance().System(); + + Service::Account::ProfileManager manager; + // TODO: Pass in a selected user once we get the relevant UI working + const auto user_id = manager.GetUser(static_cast(0)); + ASSERT(user_id); + + const auto nandDir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::NANDDir); + auto vfsNandDir = system.GetFilesystem()->OpenDirectory(Common::FS::PathToUTF8String(nandDir), + FileSys::Mode::Read); + + const auto user_save_data_path = FileSys::SaveDataFactory::GetFullPath( + system, vfsNandDir, FileSys::SaveDataSpaceId::NandUser, FileSys::SaveDataType::SaveData, + program_id, user_id->AsU128(), 0); + return ToJString(env, user_save_data_path); +} + } // extern "C" diff --git a/src/android/app/src/main/jni/native.h b/src/android/app/src/main/jni/native.h index f1457bd1f6..96c22d52b3 100644 --- a/src/android/app/src/main/jni/native.h +++ b/src/android/app/src/main/jni/native.h @@ -54,6 +54,8 @@ public: static void OnEmulationStarted(); + static u64 GetProgramId(JNIEnv* env, jstring jprogramId); + private: static void LoadDiskCacheProgress(VideoCore::LoadCallbackStage stage, int progress, int max); 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 9439d11e14..7f2485720a 100644 --- a/src/android/app/src/main/jni/native_config.cpp +++ b/src/android/app/src/main/jni/native_config.cpp @@ -283,4 +283,30 @@ void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_addGameDir(JNIEnv* env, jobject AndroidSettings::GameDir{uriString, static_cast(jdeepScanBoolean)}); } +jobjectArray Java_org_yuzu_yuzu_1emu_utils_NativeConfig_getDisabledAddons(JNIEnv* env, jobject obj, + jstring jprogramId) { + auto program_id = EmulationSession::GetProgramId(env, jprogramId); + auto& disabledAddons = Settings::values.disabled_addons[program_id]; + jobjectArray jdisabledAddonsArray = + env->NewObjectArray(disabledAddons.size(), IDCache::GetStringClass(), ToJString(env, "")); + for (size_t i = 0; i < disabledAddons.size(); ++i) { + env->SetObjectArrayElement(jdisabledAddonsArray, i, ToJString(env, disabledAddons[i])); + } + return jdisabledAddonsArray; +} + +void Java_org_yuzu_yuzu_1emu_utils_NativeConfig_setDisabledAddons(JNIEnv* env, jobject obj, + jstring jprogramId, + jobjectArray jdisabledAddons) { + auto program_id = EmulationSession::GetProgramId(env, jprogramId); + Settings::values.disabled_addons[program_id].clear(); + std::vector disabled_addons; + const int size = env->GetArrayLength(jdisabledAddons); + for (int i = 0; i < size; ++i) { + auto jaddon = static_cast(env->GetObjectArrayElement(jdisabledAddons, i)); + disabled_addons.push_back(GetJString(env, jaddon)); + } + Settings::values.disabled_addons[program_id] = disabled_addons; +} + } // extern "C" 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 0000000000..0b96338556 --- /dev/null +++ b/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + +