From 70c3d36536ad98a5569d51f1d0f68a6f01890f11 Mon Sep 17 00:00:00 2001 From: t895 Date: Sun, 10 Dec 2023 20:10:36 -0500 Subject: 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/yuzu_emu/activities/EmulationActivity.kt | 20 ++-- .../settings/model/AbstractBooleanSetting.kt | 3 +- .../features/settings/model/AbstractByteSetting.kt | 3 +- .../settings/model/AbstractFloatSetting.kt | 3 +- .../features/settings/model/AbstractIntSetting.kt | 3 +- .../features/settings/model/AbstractLongSetting.kt | 3 +- .../features/settings/model/AbstractSetting.kt | 14 ++- .../settings/model/AbstractShortSetting.kt | 3 +- .../settings/model/AbstractStringSetting.kt | 3 +- .../features/settings/model/BooleanSetting.kt | 54 +++++---- .../features/settings/model/ByteSetting.kt | 22 ++-- .../features/settings/model/FloatSetting.kt | 22 ++-- .../yuzu_emu/features/settings/model/IntSetting.kt | 53 ++++----- .../features/settings/model/LongSetting.kt | 22 ++-- .../yuzu_emu/features/settings/model/Settings.kt | 51 --------- .../features/settings/model/ShortSetting.kt | 22 ++-- .../features/settings/model/StringSetting.kt | 23 ++-- .../settings/model/view/DateTimeSetting.kt | 5 +- .../features/settings/model/view/SettingsItem.kt | 24 +++- .../settings/model/view/SingleChoiceSetting.kt | 17 +-- .../features/settings/model/view/SliderSetting.kt | 30 +++-- .../model/view/StringSingleChoiceSetting.kt | 7 +- .../features/settings/model/view/SwitchSetting.kt | 24 ++-- .../features/settings/ui/SettingsAdapter.kt | 11 +- .../settings/ui/SettingsFragmentPresenter.kt | 29 +++-- .../settings/ui/viewholder/DateTimeViewHolder.kt | 2 +- .../ui/viewholder/SingleChoiceViewHolder.kt | 4 +- .../settings/ui/viewholder/SliderViewHolder.kt | 2 +- .../ui/viewholder/SwitchSettingViewHolder.kt | 2 +- .../yuzu/yuzu_emu/fragments/EmulationFragment.kt | 6 +- .../yuzu_emu/fragments/SettingsDialogFragment.kt | 12 +- .../java/org/yuzu/yuzu_emu/utils/NativeConfig.kt | 47 ++++++-- src/android/app/src/main/jni/native_config.cpp | 126 +++++++++------------ 33 files changed, 320 insertions(+), 352 deletions(-) (limited to 'src/android/app') diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt index f41d7bdbf..9b08f008d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt @@ -172,7 +172,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { 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 aeda8d222..0ba465356 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractBooleanSetting.kt @@ -4,7 +4,6 @@ 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 606519ad8..cf6300535 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractByteSetting.kt @@ -4,7 +4,6 @@ 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 974925eed..c6c0bcf34 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractFloatSetting.kt @@ -4,7 +4,6 @@ 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 89b285b10..826402c34 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractIntSetting.kt @@ -4,7 +4,6 @@ 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 4873942db..2b62cc06b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractLongSetting.kt @@ -4,7 +4,6 @@ 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 8b6d29fe5..e384c78c2 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 91407ccbb..8bfa81e4a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractShortSetting.kt @@ -4,7 +4,6 @@ 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 c8935cc48..6ff8fd3f9 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/AbstractStringSetting.kt @@ -4,7 +4,6 @@ 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 8476ce867..16f06cd0a 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt @@ -5,36 +5,34 @@ package org.yuzu.yuzu_emu.features.settings.model 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); - - override val boolean: Boolean - get() = NativeConfig.getBoolean(key, false) - - override fun setBoolean(value: Boolean) = NativeConfig.setBoolean(key, value) - - override val defaultValue: Boolean by lazy { - androidDefault ?: NativeConfig.getBoolean(key, true) +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 fun getBoolean(needsGlobal: Boolean): Boolean = + NativeConfig.getBoolean(key, needsGlobal) + + 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 6ec0a765e..7b7fac211 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ByteSetting.kt @@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model 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 0181d06f2..4644824d8 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/FloatSetting.kt @@ -5,22 +5,22 @@ package org.yuzu.yuzu_emu.features.settings.model 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 ef10b209f..21e4e1afd 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt @@ -5,36 +5,33 @@ package org.yuzu.yuzu_emu.features.settings.model 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); - - override val int: Int - get() = NativeConfig.getInt(key, false) - - override fun setInt(value: Int) = NativeConfig.setInt(key, value) - - override val defaultValue: Int by lazy { - androidDefault ?: NativeConfig.getInt(key, true) +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 fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal) + + 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 c526fc4cf..e3efd516c 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/LongSetting.kt @@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model 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 e3cd66185..9551fc05e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt @@ -6,62 +6,11 @@ package org.yuzu.yuzu_emu.features.settings.model 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 c9a0c664c..16eb4ffdd 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/ShortSetting.kt @@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model 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 9bb3e66d4..a0d8cfede 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/StringSetting.kt @@ -5,22 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model 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 8bc164197..1d81f5f2b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/DateTimeSetting.kt @@ -12,7 +12,6 @@ class DateTimeSetting( ) : 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 e198b18a0..384527294 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 705527a73..97a5a9e59 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SingleChoiceSetting.kt @@ -15,16 +15,11 @@ class SingleChoiceSetting( ) : 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 c3b5df02c..b9b709bf7 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SliderSetting.kt @@ -20,22 +20,20 @@ class SliderSetting( ) : 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 871dab4f3..ba7920f50 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/StringSingleChoiceSetting.kt @@ -17,14 +17,13 @@ class StringSingleChoiceSetting( 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 416967e64..44d47dd69 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SwitchSetting.kt @@ -14,18 +14,18 @@ class SwitchSetting( ) : 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 af2c1e582..3f23c064e 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 7425728c6..12a389b37 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 525f013f8..4e159a799 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 80d1b22c1..28c4d1777 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 b83c90100..67432f88e 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 57fdeaa20..98ed888cb 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 734c1d5ca..b09df7db3 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 b88d2c038..60e029f34 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsDialogFragment.kt @@ -70,7 +70,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener 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 f4e1bb13f..4c7316ba3 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 763b2164c..9439d11e1 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(); -- cgit v1.2.3 From e975f3cde9d4dcb1d9e2bbce116f9a9ba99bf03f Mon Sep 17 00:00:00 2001 From: t895 Date: Sun, 10 Dec 2023 20:27:50 -0500 Subject: 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 --- .../main/java/org/yuzu/yuzu_emu/NativeLibrary.kt | 35 +- .../org/yuzu/yuzu_emu/adapters/AddonAdapter.kt | 52 +++ .../org/yuzu/yuzu_emu/adapters/AppletAdapter.kt | 6 +- .../java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt | 16 +- .../yuzu_emu/adapters/GamePropertiesAdapter.kt | 133 +++++++ .../org/yuzu/yuzu_emu/fragments/AddonsFragment.kt | 214 +++++++++++ .../ContentTypeSelectionDialogFragment.kt | 68 ++++ .../yuzu/yuzu_emu/fragments/GameInfoFragment.kt | 148 ++++++++ .../yuzu_emu/fragments/GamePropertiesFragment.kt | 418 +++++++++++++++++++++ .../IndeterminateProgressDialogFragment.kt | 2 +- .../yuzu_emu/fragments/LaunchGameDialogFragment.kt | 61 +++ .../yuzu_emu/fragments/MessageDialogFragment.kt | 37 +- .../org/yuzu/yuzu_emu/fragments/SearchFragment.kt | 4 +- .../src/main/java/org/yuzu/yuzu_emu/model/Addon.kt | 10 + .../java/org/yuzu/yuzu_emu/model/AddonViewModel.kt | 83 ++++ .../src/main/java/org/yuzu/yuzu_emu/model/Game.kt | 42 ++- .../java/org/yuzu/yuzu_emu/model/GameProperties.kt | 34 ++ .../java/org/yuzu/yuzu_emu/model/HomeViewModel.kt | 15 + .../yuzu/yuzu_emu/model/MessageDialogViewModel.kt | 4 +- .../java/org/yuzu/yuzu_emu/model/TaskViewModel.kt | 2 +- .../java/org/yuzu/yuzu_emu/ui/GamesFragment.kt | 11 +- .../java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt | 327 +++++++--------- .../main/java/org/yuzu/yuzu_emu/utils/AddonUtil.kt | 8 + .../main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt | 32 ++ .../java/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 + src/android/app/src/main/jni/native_config.cpp | 26 ++ .../res/layout-w600dp/fragment_game_properties.xml | 99 +++++ .../app/src/main/res/layout/card_applet_option.xml | 57 --- .../app/src/main/res/layout/card_installable.xml | 3 +- .../src/main/res/layout/card_simple_outlined.xml | 75 ++++ .../app/src/main/res/layout/fragment_addons.xml | 47 +++ .../app/src/main/res/layout/fragment_game_info.xml | 125 ++++++ .../main/res/layout/fragment_game_properties.xml | 86 +++++ .../app/src/main/res/layout/list_item_addon.xml | 57 +++ .../src/main/res/navigation/home_navigation.xml | 33 ++ src/android/app/src/main/res/values/dimens.xml | 2 +- src/android/app/src/main/res/values/strings.xml | 45 +++ 41 files changed, 2283 insertions(+), 309 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 delete mode 100644 src/android/app/src/main/res/layout/card_applet_option.xml create mode 100644 src/android/app/src/main/res/layout/card_simple_outlined.xml 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 (limited to 'src/android/app') diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index e0f01127c..95b98798d 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 000000000..15c7ca3c9 --- /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 a21a705c1..4a05c5be9 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/AppletAdapter.kt @@ -15,7 +15,7 @@ import org.yuzu.yuzu_emu.HomeNavigationDirections 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 2ef638559..928bfe5a7 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,8 +126,15 @@ 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) : diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GamePropertiesAdapter.kt new file mode 100644 index 000000000..ff6270fa8 --- /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 000000000..0dce8ad8d --- /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 000000000..c1d8b9ea5 --- /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 000000000..fa2a4c9f9 --- /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 000000000..485989e2e --- /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 7e467814d..8847e5531 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/IndeterminateProgressDialogFragment.kt @@ -122,7 +122,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() { 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 000000000..f653826a6 --- /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 a6183d19e..32062b6fe 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/MessageDialogFragment.kt @@ -27,30 +27,31 @@ class MessageDialogFragment : DialogFragment() { 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 2dbca76a5..3ac054d8f 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 000000000..ed79a8b02 --- /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 000000000..075252f5b --- /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 2fa3ab31b..ac642c16e 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 000000000..bb3df5bd0 --- /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 07e65b028..d801db105 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 36ffd08d2..641c5cb17 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/MessageDialogViewModel.kt @@ -6,9 +6,9 @@ package org.yuzu.yuzu_emu.model 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 16a794dee..e59c95733 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/TaskViewModel.kt @@ -23,7 +23,7 @@ class TaskViewModel : ViewModel() { 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 805b89b31..d5acf8479 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 16323a316..09ddd1bbd 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 - } + if (documents.isEmpty()) { + return@registerForActivityResult + } - NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> { - installOverwrite += 1 - } + if (addonViewModel.game == null) { + installContent(documents) + return@registerForActivityResult + } - NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> { - errorBaseGame += 1 - } + 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 + } + } - NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> { - errorExtension += 1 - } + 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) + } - else -> { - errorOther += 1 - } + 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 } } + } - val separator = System.getProperty("line.separator") ?: "\n" - val installResult = StringBuilder() - if (installSuccess > 0) { - installResult.append( - getString( - R.string.install_game_content_success_install, - installSuccess - ) + 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) - } - if (installOverwrite > 0) { installResult.append( - getString( - R.string.install_game_content_success_overwrite, - installOverwrite - ) + getString(R.string.install_game_content_failure_base) ) installResult.append(separator) } - val errorTotal: Int = errorBaseGame + errorExtension + errorOther - if (errorTotal > 0) { + if (errorExtension > 0) { installResult.append(separator) installResult.append( - getString( - R.string.install_game_content_failed_count, - errorTotal - ) + getString(R.string.install_game_content_failure_file_extension) ) 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() + } + if (errorOther > 0) { + installResult.append( + getString(R.string.install_game_content_failure_description) ) + installResult.append(separator) } - }.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG) - } + 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 000000000..8cc5ea71f --- /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 bbe7bfa92..00c6bf90e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/FileUtil.kt @@ -22,6 +22,7 @@ import java.io.BufferedOutputStream 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 4c7316ba3..7d629b7d5 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 a56ed5662..df8935178 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 855649efa..36233423e 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 e5d3158c8..ce570b811 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 f1457bd1f..96c22d52b 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 9439d11e1..7f2485720 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 000000000..0b9633855 --- /dev/null +++ b/src/android/app/src/main/res/layout-w600dp/fragment_game_properties.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + +