diff options
Diffstat (limited to 'src/android/app')
31 files changed, 458 insertions, 335 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt index c8706d7a6..21f67f32a 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 @@ -308,21 +308,6 @@ object NativeLibrary { external fun isPaused(): Boolean /** - * Mutes emulation sound - */ - external fun muteAudio(): Boolean - - /** - * Unmutes emulation sound - */ - external fun unmuteAudio(): Boolean - - /** - * Returns true if emulation audio is muted. - */ - external fun isMuted(): Boolean - - /** * Returns the performance stats for the current game */ external fun getPerfStats(): DoubleArray 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 bbd328c71..d4ae39661 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 @@ -332,7 +332,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { pictureInPictureActions.add(pauseRemoteAction) } - if (NativeLibrary.isMuted()) { + if (BooleanSetting.AUDIO_MUTED.boolean) { val unmuteIcon = Icon.createWithResource( this@EmulationActivity, R.drawable.ic_pip_unmute @@ -389,9 +389,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation() } if (intent.action == actionUnmute) { - if (NativeLibrary.isMuted()) NativeLibrary.unmuteAudio() + if (BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(false) } else if (intent.action == actionMute) { - if (!NativeLibrary.isMuted()) NativeLibrary.muteAudio() + if (!BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(true) } buildPictureInPictureParams() } @@ -417,7 +417,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { } catch (ignored: Exception) { } // Always resume audio, since there is no UI button - if (NativeLibrary.isMuted()) NativeLibrary.unmuteAudio() + if (BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(false) } } 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 0013e8512..f9f88a1d2 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 @@ -4,7 +4,8 @@ package org.yuzu.yuzu_emu.adapters import android.content.Intent -import android.graphics.drawable.BitmapDrawable +import android.graphics.Bitmap +import android.graphics.drawable.LayerDrawable import android.net.Uri import android.text.TextUtils import android.view.LayoutInflater @@ -15,7 +16,10 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.drawable.IconCompat +import androidx.core.graphics.drawable.toBitmap +import androidx.core.graphics.drawable.toDrawable import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModelProvider import androidx.navigation.findNavController @@ -87,11 +91,24 @@ class GameAdapter(private val activity: AppCompatActivity) : action = Intent.ACTION_VIEW data = Uri.parse(holder.game.path) } + + val layerDrawable = ResourcesCompat.getDrawable( + YuzuApplication.appContext.resources, + R.drawable.shortcut, + null + ) as LayerDrawable + layerDrawable.setDrawableByLayerId( + R.id.shortcut_foreground, + GameIconUtils.getGameIcon(holder.game).toDrawable(YuzuApplication.appContext.resources) + ) + val inset = YuzuApplication.appContext.resources + .getDimensionPixelSize(R.dimen.icon_inset) + layerDrawable.setLayerInset(1, inset, inset, inset, inset) val shortcut = ShortcutInfoCompat.Builder(YuzuApplication.appContext, holder.game.path) .setShortLabel(holder.game.title) .setIcon( - IconCompat.createWithBitmap( - (holder.binding.imageGameScreen.drawable as BitmapDrawable).bitmap + IconCompat.createWithAdaptiveBitmap( + layerDrawable.toBitmap(config = Bitmap.Config.ARGB_8888) ) ) .setIntent(openIntent) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt index 8d87d3bd7..1675627a1 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/HomeSettingAdapter.kt @@ -10,8 +10,12 @@ import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat 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.R import org.yuzu.yuzu_emu.databinding.CardHomeOptionBinding import org.yuzu.yuzu_emu.fragments.MessageDialogFragment @@ -86,7 +90,11 @@ class HomeSettingAdapter( binding.optionIcon.alpha = 0.5f } - option.details.observe(viewLifecycle) { updateOptionDetails(it) } + viewLifecycle.lifecycleScope.launch { + viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) { + option.details.collect { updateOptionDetails(it) } + } + } binding.optionDetail.postDelayed( { binding.optionDetail.ellipsize = TextUtils.TruncateAt.MARQUEE 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 e0c0538c7..8476ce867 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 @@ -10,6 +10,7 @@ enum class BooleanSetting( 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), 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 0702236e8..08e2a973d 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 @@ -80,6 +80,17 @@ object Settings { const val SECTION_THEME = "Theme" const val SECTION_DEBUG = "Debug" + enum class MenuTag(val titleId: Int) { + SECTION_ROOT(R.string.advanced_settings), + SECTION_GENERAL(R.string.preferences_general), + 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); + } + const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown" const val PREF_OVERLAY_VERSION = "OverlayVersion" diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt index 91c273964..b343e527e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/view/SubmenuSetting.kt @@ -3,10 +3,12 @@ package org.yuzu.yuzu_emu.features.settings.model.view +import org.yuzu.yuzu_emu.features.settings.model.Settings + class SubmenuSetting( titleId: Int, descriptionId: Int, - val menuKey: String + val menuKey: Settings.MenuTag ) : SettingsItem(emptySetting, titleId, descriptionId) { override val type = TYPE_SUBMENU } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt index 908c01265..4d2f2f604 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt @@ -13,9 +13,14 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.NavHostFragment import androidx.navigation.navArgs import com.google.android.material.color.MaterialColors +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import java.io.IOException import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding @@ -66,25 +71,39 @@ class SettingsActivity : AppCompatActivity() { ) } - settingsViewModel.shouldRecreate.observe(this) { - if (it) { - settingsViewModel.setShouldRecreate(false) - recreate() + lifecycleScope.apply { + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + settingsViewModel.shouldRecreate.collectLatest { + if (it) { + settingsViewModel.setShouldRecreate(false) + recreate() + } + } + } } - } - settingsViewModel.shouldNavigateBack.observe(this) { - if (it) { - settingsViewModel.setShouldNavigateBack(false) - navigateBack() + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + settingsViewModel.shouldNavigateBack.collectLatest { + if (it) { + settingsViewModel.setShouldNavigateBack(false) + navigateBack() + } + } + } } - } - settingsViewModel.shouldShowResetSettingsDialog.observe(this) { - if (it) { - settingsViewModel.setShouldShowResetSettingsDialog(false) - ResetSettingsDialogFragment().show( - supportFragmentManager, - ResetSettingsDialogFragment.TAG - ) + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + settingsViewModel.shouldShowResetSettingsDialog.collectLatest { + if (it) { + settingsViewModel.setShouldShowResetSettingsDialog(false) + ResetSettingsDialogFragment().show( + supportFragmentManager, + ResetSettingsDialogFragment.TAG + ) + } + } + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt index bc319714c..70d8ec14b 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragment.kt @@ -3,6 +3,7 @@ package org.yuzu.yuzu_emu.features.settings.ui +import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -13,14 +14,19 @@ 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.LinearLayoutManager import com.google.android.material.divider.MaterialDividerItemDecoration import com.google.android.material.transition.MaterialSharedAxis +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.FragmentSettingsBinding -import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile +import org.yuzu.yuzu_emu.features.settings.model.Settings import org.yuzu.yuzu_emu.model.SettingsViewModel class SettingsFragment : Fragment() { @@ -51,15 +57,17 @@ class SettingsFragment : Fragment() { return binding.root } + // This is using the correct scope, lint is just acting up + @SuppressLint("UnsafeRepeatOnLifecycleDetector") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { settingsAdapter = SettingsAdapter(this, requireContext()) presenter = SettingsFragmentPresenter( settingsViewModel, settingsAdapter!!, - args.menuTag, - args.game?.gameId ?: "" + args.menuTag ) + binding.toolbarSettingsLayout.title = getString(args.menuTag.titleId) val dividerDecoration = MaterialDividerItemDecoration( requireContext(), LinearLayoutManager.VERTICAL @@ -75,28 +83,31 @@ class SettingsFragment : Fragment() { settingsViewModel.setShouldNavigateBack(true) } - settingsViewModel.toolbarTitle.observe(viewLifecycleOwner) { - if (it.isNotEmpty()) binding.toolbarSettingsLayout.title = it - } - - settingsViewModel.shouldReloadSettingsList.observe(viewLifecycleOwner) { - if (it) { - settingsViewModel.setShouldReloadSettingsList(false) - presenter.loadSettingsList() + viewLifecycleOwner.lifecycleScope.apply { + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + settingsViewModel.shouldReloadSettingsList.collectLatest { + if (it) { + settingsViewModel.setShouldReloadSettingsList(false) + presenter.loadSettingsList() + } + } + } } - } - - settingsViewModel.isUsingSearch.observe(viewLifecycleOwner) { - if (it) { - reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) - exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) - } else { - reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) - exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + launch { + settingsViewModel.isUsingSearch.collectLatest { + if (it) { + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) + exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) + } else { + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) + exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) + } + } } } - if (args.menuTag == SettingsFile.FILE_NAME_CONFIG) { + if (args.menuTag == Settings.MenuTag.SECTION_ROOT) { binding.toolbarSettings.inflateMenu(R.menu.menu_settings) binding.toolbarSettings.setOnMenuItemClickListener { when (it.itemId) { 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 22a529b1b..766414a6c 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 @@ -6,7 +6,6 @@ package org.yuzu.yuzu_emu.features.settings.ui import android.content.Context import android.content.SharedPreferences import android.os.Build -import android.text.TextUtils import android.widget.Toast import androidx.preference.PreferenceManager import org.yuzu.yuzu_emu.R @@ -20,15 +19,13 @@ 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 import org.yuzu.yuzu_emu.features.settings.model.view.* -import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.model.SettingsViewModel import org.yuzu.yuzu_emu.utils.NativeConfig class SettingsFragmentPresenter( private val settingsViewModel: SettingsViewModel, private val adapter: SettingsAdapter, - private var menuTag: String, - private var gameId: String + private var menuTag: Settings.MenuTag ) { private var settingsList = ArrayList<SettingsItem>() @@ -53,24 +50,15 @@ class SettingsFragmentPresenter( } fun loadSettingsList() { - if (!TextUtils.isEmpty(gameId)) { - settingsViewModel.setToolbarTitle( - context.getString( - R.string.advanced_settings_game, - gameId - ) - ) - } - val sl = ArrayList<SettingsItem>() when (menuTag) { - SettingsFile.FILE_NAME_CONFIG -> addConfigSettings(sl) - Settings.SECTION_GENERAL -> addGeneralSettings(sl) - Settings.SECTION_SYSTEM -> addSystemSettings(sl) - Settings.SECTION_RENDERER -> addGraphicsSettings(sl) - Settings.SECTION_AUDIO -> addAudioSettings(sl) - Settings.SECTION_THEME -> addThemeSettings(sl) - Settings.SECTION_DEBUG -> addDebugSettings(sl) + Settings.MenuTag.SECTION_ROOT -> addConfigSettings(sl) + Settings.MenuTag.SECTION_GENERAL -> addGeneralSettings(sl) + Settings.MenuTag.SECTION_SYSTEM -> addSystemSettings(sl) + Settings.MenuTag.SECTION_RENDERER -> addGraphicsSettings(sl) + Settings.MenuTag.SECTION_AUDIO -> addAudioSettings(sl) + Settings.MenuTag.SECTION_THEME -> addThemeSettings(sl) + Settings.MenuTag.SECTION_DEBUG -> addDebugSettings(sl) else -> { val context = YuzuApplication.appContext Toast.makeText( @@ -86,13 +74,12 @@ class SettingsFragmentPresenter( } private fun addConfigSettings(sl: ArrayList<SettingsItem>) { - settingsViewModel.setToolbarTitle(context.getString(R.string.advanced_settings)) sl.apply { - add(SubmenuSetting(R.string.preferences_general, 0, Settings.SECTION_GENERAL)) - add(SubmenuSetting(R.string.preferences_system, 0, Settings.SECTION_SYSTEM)) - add(SubmenuSetting(R.string.preferences_graphics, 0, Settings.SECTION_RENDERER)) - add(SubmenuSetting(R.string.preferences_audio, 0, Settings.SECTION_AUDIO)) - add(SubmenuSetting(R.string.preferences_debug, 0, Settings.SECTION_DEBUG)) + add(SubmenuSetting(R.string.preferences_general, 0, Settings.MenuTag.SECTION_GENERAL)) + add(SubmenuSetting(R.string.preferences_system, 0, Settings.MenuTag.SECTION_SYSTEM)) + add(SubmenuSetting(R.string.preferences_graphics, 0, Settings.MenuTag.SECTION_RENDERER)) + add(SubmenuSetting(R.string.preferences_audio, 0, Settings.MenuTag.SECTION_AUDIO)) + add(SubmenuSetting(R.string.preferences_debug, 0, Settings.MenuTag.SECTION_DEBUG)) add( RunnableSetting(R.string.reset_to_default, 0, false) { settingsViewModel.setShouldShowResetSettingsDialog(true) @@ -102,7 +89,6 @@ class SettingsFragmentPresenter( } private fun addGeneralSettings(sl: ArrayList<SettingsItem>) { - settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_general)) sl.apply { add(BooleanSetting.RENDERER_USE_SPEED_LIMIT.key) add(ShortSetting.RENDERER_SPEED_LIMIT.key) @@ -112,7 +98,6 @@ class SettingsFragmentPresenter( } private fun addSystemSettings(sl: ArrayList<SettingsItem>) { - settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_system)) sl.apply { add(BooleanSetting.USE_DOCKED_MODE.key) add(IntSetting.REGION_INDEX.key) @@ -123,7 +108,6 @@ class SettingsFragmentPresenter( } private fun addGraphicsSettings(sl: ArrayList<SettingsItem>) { - settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_graphics)) sl.apply { add(IntSetting.RENDERER_ACCURACY.key) add(IntSetting.RENDERER_RESOLUTION.key) @@ -140,7 +124,6 @@ class SettingsFragmentPresenter( } private fun addAudioSettings(sl: ArrayList<SettingsItem>) { - settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_audio)) sl.apply { add(IntSetting.AUDIO_OUTPUT_ENGINE.key) add(ByteSetting.AUDIO_VOLUME.key) @@ -148,7 +131,6 @@ class SettingsFragmentPresenter( } private fun addThemeSettings(sl: ArrayList<SettingsItem>) { - settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_theme)) sl.apply { val theme: AbstractIntSetting = object : AbstractIntSetting { override val int: Int @@ -261,7 +243,6 @@ class SettingsFragmentPresenter( } private fun addDebugSettings(sl: ArrayList<SettingsItem>) { - settingsViewModel.setToolbarTitle(context.getString(R.string.preferences_debug)) sl.apply { add(HeaderSetting(R.string.gpu)) add(IntSetting.RENDERER_BACKEND.key) 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 944ae652e..3e6c157c7 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 @@ -39,6 +39,7 @@ import androidx.window.layout.WindowLayoutInfo import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.slider.Slider import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.HomeNavigationDirections import org.yuzu.yuzu_emu.NativeLibrary @@ -49,7 +50,6 @@ import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding import org.yuzu.yuzu_emu.features.settings.model.IntSetting import org.yuzu.yuzu_emu.features.settings.model.Settings -import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.model.Game import org.yuzu.yuzu_emu.model.EmulationViewModel import org.yuzu.yuzu_emu.overlay.InputOverlay @@ -129,6 +129,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { return binding.root } + // This is using the correct scope, lint is just acting up + @SuppressLint("UnsafeRepeatOnLifecycleDetector") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { binding.surfaceEmulation.holder.addCallback(this) binding.showFpsText.setTextColor(Color.YELLOW) @@ -163,7 +165,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { R.id.menu_settings -> { val action = HomeNavigationDirections.actionGlobalSettingsActivity( null, - SettingsFile.FILE_NAME_CONFIG + Settings.MenuTag.SECTION_ROOT ) binding.root.findNavController().navigate(action) true @@ -205,59 +207,80 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } ) - viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) { - lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - WindowInfoTracker.getOrCreate(requireContext()) - .windowLayoutInfo(requireActivity()) - .collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) } - } - } - GameIconUtils.loadGameIcon(game, binding.loadingImage) binding.loadingTitle.text = game.title binding.loadingTitle.isSelected = true binding.loadingText.isSelected = true - emulationViewModel.shaderProgress.observe(viewLifecycleOwner) { - if (it > 0 && it != emulationViewModel.totalShaders.value!!) { - binding.loadingProgressIndicator.isIndeterminate = false - - if (it < binding.loadingProgressIndicator.max) { - binding.loadingProgressIndicator.progress = it + viewLifecycleOwner.lifecycleScope.apply { + launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + WindowInfoTracker.getOrCreate(requireContext()) + .windowLayoutInfo(requireActivity()) + .collect { + updateFoldableLayout(requireActivity() as EmulationActivity, it) + } } } + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + emulationViewModel.shaderProgress.collectLatest { + if (it > 0 && it != emulationViewModel.totalShaders.value) { + binding.loadingProgressIndicator.isIndeterminate = false + + if (it < binding.loadingProgressIndicator.max) { + binding.loadingProgressIndicator.progress = it + } + } - if (it == emulationViewModel.totalShaders.value!!) { - binding.loadingText.setText(R.string.loading) - binding.loadingProgressIndicator.isIndeterminate = true + if (it == emulationViewModel.totalShaders.value) { + binding.loadingText.setText(R.string.loading) + binding.loadingProgressIndicator.isIndeterminate = true + } + } + } } - } - emulationViewModel.totalShaders.observe(viewLifecycleOwner) { - binding.loadingProgressIndicator.max = it - } - emulationViewModel.shaderMessage.observe(viewLifecycleOwner) { - if (it.isNotEmpty()) { - binding.loadingText.text = it + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + emulationViewModel.totalShaders.collectLatest { + binding.loadingProgressIndicator.max = it + } + } } - } - - emulationViewModel.emulationStarted.observe(viewLifecycleOwner) { started -> - if (started) { - binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) - ViewUtils.showView(binding.surfaceInputOverlay) - ViewUtils.hideView(binding.loadingIndicator) - - // Setup overlay - updateShowFpsOverlay() + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + emulationViewModel.shaderMessage.collectLatest { + if (it.isNotEmpty()) { + binding.loadingText.text = it + } + } + } } - } + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + emulationViewModel.emulationStarted.collectLatest { + if (it) { + binding.drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED) + ViewUtils.showView(binding.surfaceInputOverlay) + ViewUtils.hideView(binding.loadingIndicator) - emulationViewModel.isEmulationStopping.observe(viewLifecycleOwner) { - if (it) { - binding.loadingText.setText(R.string.shutting_down) - ViewUtils.showView(binding.loadingIndicator) - ViewUtils.hideView(binding.inputContainer) - ViewUtils.hideView(binding.showFpsText) + // Setup overlay + updateShowFpsOverlay() + } + } + } + } + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + emulationViewModel.isEmulationStopping.collectLatest { + if (it) { + binding.loadingText.setText(R.string.shutting_down) + ViewUtils.showView(binding.loadingIndicator) + ViewUtils.hideView(binding.inputContainer) + ViewUtils.hideView(binding.showFpsText) + } + } + } } } } @@ -274,9 +297,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { } } } else { - if (EmulationMenuSettings.showOverlay && - emulationViewModel.emulationStarted.value == true - ) { + if (EmulationMenuSettings.showOverlay && emulationViewModel.emulationStarted.value) { binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.visibility = View.VISIBLE } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt index cbbe14d22..c119e69c9 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/HomeSettingsFragment.kt @@ -37,7 +37,6 @@ import org.yuzu.yuzu_emu.adapters.HomeSettingAdapter import org.yuzu.yuzu_emu.databinding.FragmentHomeSettingsBinding import org.yuzu.yuzu_emu.features.DocumentProvider import org.yuzu.yuzu_emu.features.settings.model.Settings -import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.model.HomeSetting import org.yuzu.yuzu_emu.model.HomeViewModel import org.yuzu.yuzu_emu.ui.main.MainActivity @@ -78,7 +77,7 @@ class HomeSettingsFragment : Fragment() { { val action = HomeNavigationDirections.actionGlobalSettingsActivity( null, - SettingsFile.FILE_NAME_CONFIG + Settings.MenuTag.SECTION_ROOT ) binding.root.findNavController().navigate(action) } @@ -100,7 +99,7 @@ class HomeSettingsFragment : Fragment() { { val action = HomeNavigationDirections.actionGlobalSettingsActivity( null, - Settings.SECTION_THEME + Settings.MenuTag.SECTION_THEME ) binding.root.findNavController().navigate(action) } 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 181bd983a..ea8eb073a 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 @@ -9,8 +9,12 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding import org.yuzu.yuzu_emu.model.TaskViewModel @@ -28,21 +32,27 @@ class IndeterminateProgressDialogFragment : DialogFragment() { .create() dialog.setCanceledOnTouchOutside(false) - taskViewModel.isComplete.observe(this) { complete -> - if (complete) { - dialog.dismiss() - when (val result = taskViewModel.result.value) { - is String -> Toast.makeText(requireContext(), result, Toast.LENGTH_LONG).show() - is MessageDialogFragment -> result.show( - requireActivity().supportFragmentManager, - MessageDialogFragment.TAG - ) + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + taskViewModel.isComplete.collect { + if (it) { + dialog.dismiss() + when (val result = taskViewModel.result.value) { + is String -> Toast.makeText(requireContext(), result, Toast.LENGTH_LONG) + .show() + + is MessageDialogFragment -> result.show( + requireActivity().supportFragmentManager, + MessageDialogFragment.TAG + ) + } + taskViewModel.clear() + } } - taskViewModel.clear() } } - if (taskViewModel.isRunning.value == false) { + if (!taskViewModel.isRunning.value) { taskViewModel.runTask() } 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 f54dccc69..2dbca76a5 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 @@ -3,6 +3,7 @@ package org.yuzu.yuzu_emu.fragments +import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences import android.os.Bundle @@ -17,9 +18,13 @@ import androidx.core.view.updatePadding import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.preference.PreferenceManager import info.debatty.java.stringsimilarity.Jaccard import info.debatty.java.stringsimilarity.JaroWinkler +import kotlinx.coroutines.launch import java.util.Locale import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication @@ -52,6 +57,8 @@ class SearchFragment : Fragment() { return binding.root } + // 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) preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) @@ -79,21 +86,32 @@ class SearchFragment : Fragment() { filterAndSearch() } - gamesViewModel.apply { - searchFocused.observe(viewLifecycleOwner) { searchFocused -> - if (searchFocused) { - focusSearch() - gamesViewModel.setSearchFocused(false) + viewLifecycleOwner.lifecycleScope.apply { + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + gamesViewModel.searchFocused.collect { + if (it) { + focusSearch() + gamesViewModel.setSearchFocused(false) + } + } } } - - games.observe(viewLifecycleOwner) { filterAndSearch() } - searchedGames.observe(viewLifecycleOwner) { - (binding.gridGamesSearch.adapter as GameAdapter).submitList(it) - if (it.isEmpty()) { - binding.noResultsView.visibility = View.VISIBLE - } else { - binding.noResultsView.visibility = View.GONE + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + gamesViewModel.games.collect { filterAndSearch() } + } + } + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + gamesViewModel.searchedGames.collect { + (binding.gridGamesSearch.adapter as GameAdapter).submitList(it) + if (it.isEmpty()) { + binding.noResultsView.visibility = View.VISIBLE + } else { + binding.noResultsView.visibility = View.GONE + } + } } } } @@ -109,7 +127,7 @@ class SearchFragment : Fragment() { private inner class ScoredGame(val score: Double, val item: Game) private fun filterAndSearch() { - val baseList = gamesViewModel.games.value!! + val baseList = gamesViewModel.games.value val filteredList: List<Game> = when (binding.chipGroup.checkedChipId) { R.id.chip_recently_played -> { baseList.filter { diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt index 55b6a0367..9d0594c6e 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SettingsSearchFragment.kt @@ -15,10 +15,14 @@ import androidx.core.view.updatePadding import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.divider.MaterialDividerItemDecoration import com.google.android.material.transition.MaterialSharedAxis import info.debatty.java.stringsimilarity.Cosine +import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.databinding.FragmentSettingsSearchBinding import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem @@ -79,10 +83,14 @@ class SettingsSearchFragment : Fragment() { search() binding.settingsList.smoothScrollToPosition(0) } - settingsViewModel.shouldReloadSettingsList.observe(viewLifecycleOwner) { - if (it) { - settingsViewModel.setShouldReloadSettingsList(false) - search() + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + settingsViewModel.shouldReloadSettingsList.collect { + if (it) { + settingsViewModel.setShouldReloadSettingsList(false) + search() + } + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt index d50c421a0..fbb2f6e18 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/SetupFragment.kt @@ -22,10 +22,14 @@ import androidx.core.view.isVisible 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.preference.PreferenceManager import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import com.google.android.material.transition.MaterialFadeThrough +import kotlinx.coroutines.launch import java.io.File import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication @@ -206,10 +210,14 @@ class SetupFragment : Fragment() { ) } - homeViewModel.shouldPageForward.observe(viewLifecycleOwner) { - if (it) { - pageForward() - homeViewModel.setShouldPageForward(false) + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + homeViewModel.shouldPageForward.collect { + if (it) { + pageForward() + homeViewModel.setShouldPageForward(false) + } + } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/EmulationViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/EmulationViewModel.kt index e35f51bc3..f34870c2d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/EmulationViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/EmulationViewModel.kt @@ -3,28 +3,28 @@ package org.yuzu.yuzu_emu.model -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow class EmulationViewModel : ViewModel() { - private val _emulationStarted = MutableLiveData(false) - val emulationStarted: LiveData<Boolean> get() = _emulationStarted + val emulationStarted: StateFlow<Boolean> get() = _emulationStarted + private val _emulationStarted = MutableStateFlow(false) - private val _isEmulationStopping = MutableLiveData(false) - val isEmulationStopping: LiveData<Boolean> get() = _isEmulationStopping + val isEmulationStopping: StateFlow<Boolean> get() = _isEmulationStopping + private val _isEmulationStopping = MutableStateFlow(false) - private val _shaderProgress = MutableLiveData(0) - val shaderProgress: LiveData<Int> get() = _shaderProgress + val shaderProgress: StateFlow<Int> get() = _shaderProgress + private val _shaderProgress = MutableStateFlow(0) - private val _totalShaders = MutableLiveData(0) - val totalShaders: LiveData<Int> get() = _totalShaders + val totalShaders: StateFlow<Int> get() = _totalShaders + private val _totalShaders = MutableStateFlow(0) - private val _shaderMessage = MutableLiveData("") - val shaderMessage: LiveData<String> get() = _shaderMessage + val shaderMessage: StateFlow<String> get() = _shaderMessage + private val _shaderMessage = MutableStateFlow("") fun setEmulationStarted(started: Boolean) { - _emulationStarted.postValue(started) + _emulationStarted.value = started } fun setIsEmulationStopping(value: Boolean) { @@ -50,10 +50,18 @@ class EmulationViewModel : ViewModel() { } fun clear() { - _emulationStarted.value = false - _isEmulationStopping.value = false - _shaderProgress.value = 0 - _totalShaders.value = 0 - _shaderMessage.value = "" + setEmulationStarted(false) + setIsEmulationStopping(false) + setShaderProgress(0) + setTotalShaders(0) + setShaderMessage("") + } + + companion object { + const val KEY_EMULATION_STARTED = "EmulationStarted" + const val KEY_IS_EMULATION_STOPPING = "IsEmulationStarting" + const val KEY_SHADER_PROGRESS = "ShaderProgress" + const val KEY_TOTAL_SHADERS = "TotalShaders" + const val KEY_SHADER_MESSAGE = "ShaderMessage" } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt index 1fe42f922..6e09fa81d 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/GamesViewModel.kt @@ -5,13 +5,13 @@ package org.yuzu.yuzu_emu.model import android.net.Uri import androidx.documentfile.provider.DocumentFile -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.preference.PreferenceManager import java.util.Locale import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi @@ -24,23 +24,23 @@ import org.yuzu.yuzu_emu.utils.GameHelper @OptIn(ExperimentalSerializationApi::class) class GamesViewModel : ViewModel() { - private val _games = MutableLiveData<List<Game>>(emptyList()) - val games: LiveData<List<Game>> get() = _games + val games: StateFlow<List<Game>> get() = _games + private val _games = MutableStateFlow(emptyList<Game>()) - private val _searchedGames = MutableLiveData<List<Game>>(emptyList()) - val searchedGames: LiveData<List<Game>> get() = _searchedGames + val searchedGames: StateFlow<List<Game>> get() = _searchedGames + private val _searchedGames = MutableStateFlow(emptyList<Game>()) - private val _isReloading = MutableLiveData(false) - val isReloading: LiveData<Boolean> get() = _isReloading + val isReloading: StateFlow<Boolean> get() = _isReloading + private val _isReloading = MutableStateFlow(false) - private val _shouldSwapData = MutableLiveData(false) - val shouldSwapData: LiveData<Boolean> get() = _shouldSwapData + val shouldSwapData: StateFlow<Boolean> get() = _shouldSwapData + private val _shouldSwapData = MutableStateFlow(false) - private val _shouldScrollToTop = MutableLiveData(false) - val shouldScrollToTop: LiveData<Boolean> get() = _shouldScrollToTop + val shouldScrollToTop: StateFlow<Boolean> get() = _shouldScrollToTop + private val _shouldScrollToTop = MutableStateFlow(false) - private val _searchFocused = MutableLiveData(false) - val searchFocused: LiveData<Boolean> get() = _searchFocused + val searchFocused: StateFlow<Boolean> get() = _searchFocused + private val _searchFocused = MutableStateFlow(false) init { // Ensure keys are loaded so that ROM metadata can be decrypted. @@ -79,36 +79,36 @@ class GamesViewModel : ViewModel() { ) ) - _games.postValue(sortedList) + _games.value = sortedList } fun setSearchedGames(games: List<Game>) { - _searchedGames.postValue(games) + _searchedGames.value = games } fun setShouldSwapData(shouldSwap: Boolean) { - _shouldSwapData.postValue(shouldSwap) + _shouldSwapData.value = shouldSwap } fun setShouldScrollToTop(shouldScroll: Boolean) { - _shouldScrollToTop.postValue(shouldScroll) + _shouldScrollToTop.value = shouldScroll } fun setSearchFocused(searchFocused: Boolean) { - _searchFocused.postValue(searchFocused) + _searchFocused.value = searchFocused } fun reloadGames(directoryChanged: Boolean) { - if (isReloading.value == true) { + if (isReloading.value) { return } - _isReloading.postValue(true) + _isReloading.value = true viewModelScope.launch { withContext(Dispatchers.IO) { NativeLibrary.resetRomMetadata() setGames(GameHelper.getGames()) - _isReloading.postValue(false) + _isReloading.value = false if (directoryChanged) { setShouldSwapData(true) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeSetting.kt index 498c222fa..b32e19373 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeSetting.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/HomeSetting.kt @@ -3,8 +3,8 @@ package org.yuzu.yuzu_emu.model -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow data class HomeSetting( val titleId: Int, @@ -14,5 +14,5 @@ data class HomeSetting( val isEnabled: () -> Boolean = { true }, val disabledTitleId: Int = 0, val disabledMessageId: Int = 0, - val details: LiveData<String> = MutableLiveData("") + val details: StateFlow<String> = MutableStateFlow("") ) 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 a48ef7a88..756f76721 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 @@ -5,47 +5,43 @@ package org.yuzu.yuzu_emu.model import android.net.Uri import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.preference.PreferenceManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.utils.GameHelper class HomeViewModel : ViewModel() { - private val _navigationVisible = MutableLiveData<Pair<Boolean, Boolean>>() - val navigationVisible: LiveData<Pair<Boolean, Boolean>> get() = _navigationVisible + val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible + private val _navigationVisible = MutableStateFlow(Pair(false, false)) - private val _statusBarShadeVisible = MutableLiveData(true) - val statusBarShadeVisible: LiveData<Boolean> get() = _statusBarShadeVisible + val statusBarShadeVisible: StateFlow<Boolean> get() = _statusBarShadeVisible + private val _statusBarShadeVisible = MutableStateFlow(true) - private val _shouldPageForward = MutableLiveData(false) - val shouldPageForward: LiveData<Boolean> get() = _shouldPageForward + val shouldPageForward: StateFlow<Boolean> get() = _shouldPageForward + private val _shouldPageForward = MutableStateFlow(false) - private val _gamesDir = MutableLiveData( + val gamesDir: StateFlow<String> get() = _gamesDir + private val _gamesDir = MutableStateFlow( Uri.parse( PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) .getString(GameHelper.KEY_GAME_PATH, "") ).path ?: "" ) - val gamesDir: LiveData<String> get() = _gamesDir var navigatedToSetup = false - init { - _navigationVisible.value = Pair(false, false) - } - fun setNavigationVisibility(visible: Boolean, animated: Boolean) { - if (_navigationVisible.value?.first == visible) { + if (navigationVisible.value.first == visible) { return } _navigationVisible.value = Pair(visible, animated) } fun setStatusBarShadeVisibility(visible: Boolean) { - if (_statusBarShadeVisible.value == visible) { + if (statusBarShadeVisible.value == visible) { return } _statusBarShadeVisible.value = visible diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt index d16d15fa6..53fa7a8de 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/model/SettingsViewModel.kt @@ -3,48 +3,43 @@ package org.yuzu.yuzu_emu.model -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.YuzuApplication import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem -class SettingsViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { +class SettingsViewModel : ViewModel() { var game: Game? = null var shouldSave = false var clickedItem: SettingsItem? = null - private val _toolbarTitle = MutableLiveData("") - val toolbarTitle: LiveData<String> get() = _toolbarTitle + val shouldRecreate: StateFlow<Boolean> get() = _shouldRecreate + private val _shouldRecreate = MutableStateFlow(false) - private val _shouldRecreate = MutableLiveData(false) - val shouldRecreate: LiveData<Boolean> get() = _shouldRecreate + val shouldNavigateBack: StateFlow<Boolean> get() = _shouldNavigateBack + private val _shouldNavigateBack = MutableStateFlow(false) - private val _shouldNavigateBack = MutableLiveData(false) - val shouldNavigateBack: LiveData<Boolean> get() = _shouldNavigateBack + val shouldShowResetSettingsDialog: StateFlow<Boolean> get() = _shouldShowResetSettingsDialog + private val _shouldShowResetSettingsDialog = MutableStateFlow(false) - private val _shouldShowResetSettingsDialog = MutableLiveData(false) - val shouldShowResetSettingsDialog: LiveData<Boolean> get() = _shouldShowResetSettingsDialog + val shouldReloadSettingsList: StateFlow<Boolean> get() = _shouldReloadSettingsList + private val _shouldReloadSettingsList = MutableStateFlow(false) - private val _shouldReloadSettingsList = MutableLiveData(false) - val shouldReloadSettingsList: LiveData<Boolean> get() = _shouldReloadSettingsList + val isUsingSearch: StateFlow<Boolean> get() = _isUsingSearch + private val _isUsingSearch = MutableStateFlow(false) - private val _isUsingSearch = MutableLiveData(false) - val isUsingSearch: LiveData<Boolean> get() = _isUsingSearch + val sliderProgress: StateFlow<Int> get() = _sliderProgress + private val _sliderProgress = MutableStateFlow(-1) - val sliderProgress = savedStateHandle.getStateFlow(KEY_SLIDER_PROGRESS, -1) + val sliderTextValue: StateFlow<String> get() = _sliderTextValue + private val _sliderTextValue = MutableStateFlow("") - val sliderTextValue = savedStateHandle.getStateFlow(KEY_SLIDER_TEXT_VALUE, "") - - val adapterItemChanged = savedStateHandle.getStateFlow(KEY_ADAPTER_ITEM_CHANGED, -1) - - fun setToolbarTitle(value: String) { - _toolbarTitle.value = value - } + val adapterItemChanged: StateFlow<Int> get() = _adapterItemChanged + private val _adapterItemChanged = MutableStateFlow(-1) fun setShouldRecreate(value: Boolean) { _shouldRecreate.value = value @@ -67,8 +62,8 @@ class SettingsViewModel(private val savedStateHandle: SavedStateHandle) : ViewMo } fun setSliderTextValue(value: Float, units: String) { - savedStateHandle[KEY_SLIDER_PROGRESS] = value - savedStateHandle[KEY_SLIDER_TEXT_VALUE] = String.format( + _sliderProgress.value = value.toInt() + _sliderTextValue.value = String.format( YuzuApplication.appContext.getString(R.string.value_with_units), value.toInt().toString(), units @@ -76,21 +71,15 @@ class SettingsViewModel(private val savedStateHandle: SavedStateHandle) : ViewMo } fun setSliderProgress(value: Float) { - savedStateHandle[KEY_SLIDER_PROGRESS] = value + _sliderProgress.value = value.toInt() } fun setAdapterItemChanged(value: Int) { - savedStateHandle[KEY_ADAPTER_ITEM_CHANGED] = value + _adapterItemChanged.value = value } fun clear() { game = null shouldSave = false } - - companion object { - const val KEY_SLIDER_TEXT_VALUE = "SliderTextValue" - const val KEY_SLIDER_PROGRESS = "SliderProgress" - const val KEY_ADAPTER_ITEM_CHANGED = "AdapterItemChanged" - } } 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 27ea725a5..531c2aaf0 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 @@ -3,29 +3,25 @@ package org.yuzu.yuzu_emu.model -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch class TaskViewModel : ViewModel() { - private val _result = MutableLiveData<Any>() - val result: LiveData<Any> = _result + val result: StateFlow<Any> get() = _result + private val _result = MutableStateFlow(Any()) - private val _isComplete = MutableLiveData<Boolean>() - val isComplete: LiveData<Boolean> = _isComplete + val isComplete: StateFlow<Boolean> get() = _isComplete + private val _isComplete = MutableStateFlow(false) - private val _isRunning = MutableLiveData<Boolean>() - val isRunning: LiveData<Boolean> = _isRunning + val isRunning: StateFlow<Boolean> get() = _isRunning + private val _isRunning = MutableStateFlow(false) lateinit var task: () -> Any - init { - clear() - } - fun clear() { _result.value = Any() _isComplete.value = false @@ -33,15 +29,16 @@ class TaskViewModel : ViewModel() { } fun runTask() { - if (_isRunning.value == true) { + if (isRunning.value) { return } _isRunning.value = true viewModelScope.launch(Dispatchers.IO) { val res = task() - _result.postValue(res) - _isComplete.postValue(true) + _result.value = res + _isComplete.value = true + _isRunning.value = false } } } 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 b0156dca5..805b89b31 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 @@ -3,6 +3,7 @@ package org.yuzu.yuzu_emu.ui +import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -14,8 +15,12 @@ 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 com.google.android.material.color.MaterialColors import com.google.android.material.transition.MaterialFadeThrough +import kotlinx.coroutines.launch import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.adapters.GameAdapter import org.yuzu.yuzu_emu.databinding.FragmentGamesBinding @@ -44,6 +49,8 @@ class GamesFragment : Fragment() { return binding.root } + // 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) @@ -80,37 +87,48 @@ class GamesFragment : Fragment() { if (_binding == null) { return@post } - binding.swipeRefresh.isRefreshing = gamesViewModel.isReloading.value!! + binding.swipeRefresh.isRefreshing = gamesViewModel.isReloading.value } } - gamesViewModel.apply { - // Watch for when we get updates to any of our games lists - isReloading.observe(viewLifecycleOwner) { isReloading -> - binding.swipeRefresh.isRefreshing = isReloading + viewLifecycleOwner.lifecycleScope.apply { + launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + gamesViewModel.isReloading.collect { binding.swipeRefresh.isRefreshing = it } + } } - games.observe(viewLifecycleOwner) { - (binding.gridGames.adapter as GameAdapter).submitList(it) - if (it.isEmpty()) { - binding.noticeText.visibility = View.VISIBLE - } else { - binding.noticeText.visibility = View.GONE + launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + gamesViewModel.games.collect { + (binding.gridGames.adapter as GameAdapter).submitList(it) + if (it.isEmpty()) { + binding.noticeText.visibility = View.VISIBLE + } else { + binding.noticeText.visibility = View.GONE + } + } } } - shouldSwapData.observe(viewLifecycleOwner) { shouldSwapData -> - if (shouldSwapData) { - (binding.gridGames.adapter as GameAdapter).submitList( - gamesViewModel.games.value!! - ) - gamesViewModel.setShouldSwapData(false) + launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + gamesViewModel.shouldSwapData.collect { + if (it) { + (binding.gridGames.adapter as GameAdapter).submitList( + gamesViewModel.games.value + ) + gamesViewModel.setShouldSwapData(false) + } + } } } - - // Check if the user reselected the games menu item and then scroll to top of the list - shouldScrollToTop.observe(viewLifecycleOwner) { shouldScroll -> - if (shouldScroll) { - scrollToTop() - gamesViewModel.setShouldScrollToTop(false) + launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + gamesViewModel.shouldScrollToTop.collect { + if (it) { + scrollToTop() + gamesViewModel.setShouldScrollToTop(false) + } + } } } } diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/ui/main/MainActivity.kt index 7d8e06ad8..b6b6c6c17 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 @@ -19,7 +19,9 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController @@ -40,7 +42,6 @@ import org.yuzu.yuzu_emu.activities.EmulationActivity import org.yuzu.yuzu_emu.databinding.ActivityMainBinding import org.yuzu.yuzu_emu.databinding.DialogProgressBarBinding import org.yuzu.yuzu_emu.features.settings.model.Settings -import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment import org.yuzu.yuzu_emu.fragments.MessageDialogFragment import org.yuzu.yuzu_emu.model.GamesViewModel @@ -107,7 +108,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider { R.id.homeSettingsFragment -> { val action = HomeNavigationDirections.actionGlobalSettingsActivity( null, - SettingsFile.FILE_NAME_CONFIG + Settings.MenuTag.SECTION_ROOT ) navHostFragment.navController.navigate(action) } @@ -115,16 +116,22 @@ class MainActivity : AppCompatActivity(), ThemeProvider { } // Prevents navigation from being drawn for a short time on recreation if set to hidden - if (!homeViewModel.navigationVisible.value?.first!!) { + if (!homeViewModel.navigationVisible.value.first) { binding.navigationView.visibility = View.INVISIBLE binding.statusBarShade.visibility = View.INVISIBLE } - homeViewModel.navigationVisible.observe(this) { - showNavigation(it.first, it.second) - } - homeViewModel.statusBarShadeVisible.observe(this) { visible -> - showStatusBarShade(visible) + lifecycleScope.apply { + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + homeViewModel.navigationVisible.collect { showNavigation(it.first, it.second) } + } + } + launch { + repeatOnLifecycle(Lifecycle.State.CREATED) { + homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) } + } + } } // Dismiss previous notifications (should not happen unless a crash occurred) diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconUtils.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconUtils.kt index c0fe596d7..9fe99fab1 100644 --- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconUtils.kt +++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/GameIconUtils.kt @@ -6,9 +6,11 @@ package org.yuzu.yuzu_emu.utils import android.graphics.Bitmap import android.graphics.BitmapFactory import android.widget.ImageView +import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toDrawable import coil.ImageLoader import coil.decode.DataSource +import coil.executeBlocking import coil.fetch.DrawableResult import coil.fetch.FetchResult import coil.fetch.Fetcher @@ -74,4 +76,13 @@ object GameIconUtils { .build() imageLoader.enqueue(request) } + + fun getGameIcon(game: Game): Bitmap { + val request = ImageRequest.Builder(YuzuApplication.appContext) + .data(game) + .error(R.drawable.default_icon) + .build() + return imageLoader.executeBlocking(request) + .drawable!!.toBitmap(config = Bitmap.Config.ARGB_8888) + } } diff --git a/src/android/app/src/main/jni/native.cpp b/src/android/app/src/main/jni/native.cpp index b9ecefa74..f31fe054b 100644 --- a/src/android/app/src/main/jni/native.cpp +++ b/src/android/app/src/main/jni/native.cpp @@ -262,9 +262,6 @@ public: Core::SystemResultStatus InitializeEmulation(const std::string& filepath) { std::scoped_lock lock(m_mutex); - // Loads the configuration. - Config{}; - // Create the render window. m_window = std::make_unique<EmuWindow_Android>(&m_input_subsystem, m_native_window, m_vulkan_library); @@ -330,12 +327,13 @@ public: m_system.ShutdownMainProcess(); m_detached_tasks.WaitForAllTasks(); m_load_result = Core::SystemResultStatus::ErrorNotInitialized; + m_window.reset(); + OnEmulationStopped(Core::SystemResultStatus::Success); + return; } // Tear down the render window. m_window.reset(); - - OnEmulationStopped(m_load_result); } void PauseEmulation() { @@ -672,18 +670,6 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused(JNIEnv* env, jclass claz return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused()); } -void Java_org_yuzu_yuzu_1emu_NativeLibrary_muteAduio(JNIEnv* env, jclass clazz) { - Settings::values.audio_muted = true; -} - -void Java_org_yuzu_yuzu_1emu_NativeLibrary_unmuteAudio(JNIEnv* env, jclass clazz) { - Settings::values.audio_muted = false; -} - -jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isMuted(JNIEnv* env, jclass clazz) { - return static_cast<jboolean>(Settings::values.audio_muted.GetValue()); -} - jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly(JNIEnv* env, jclass clazz) { return EmulationSession::GetInstance().IsHandheldOnly(); } diff --git a/src/android/app/src/main/res/drawable/shortcut.xml b/src/android/app/src/main/res/drawable/shortcut.xml new file mode 100644 index 000000000..c749e5d72 --- /dev/null +++ b/src/android/app/src/main/res/drawable/shortcut.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + + <item> + <color android:color="@android:color/white" /> + </item> + <item android:id="@+id/shortcut_foreground"> + <bitmap android:src="@drawable/default_icon" /> + </item> + +</layer-list> diff --git a/src/android/app/src/main/res/navigation/emulation_navigation.xml b/src/android/app/src/main/res/navigation/emulation_navigation.xml index c7be37f9b..cfc494b3f 100644 --- a/src/android/app/src/main/res/navigation/emulation_navigation.xml +++ b/src/android/app/src/main/res/navigation/emulation_navigation.xml @@ -27,7 +27,7 @@ app:nullable="true" /> <argument android:name="menuTag" - app:argType="string" /> + app:argType="org.yuzu.yuzu_emu.features.settings.model.Settings$MenuTag" /> </activity> <action diff --git a/src/android/app/src/main/res/navigation/home_navigation.xml b/src/android/app/src/main/res/navigation/home_navigation.xml index 2085430bf..2e0ce7a3d 100644 --- a/src/android/app/src/main/res/navigation/home_navigation.xml +++ b/src/android/app/src/main/res/navigation/home_navigation.xml @@ -82,7 +82,7 @@ app:nullable="true" /> <argument android:name="menuTag" - app:argType="string" /> + app:argType="org.yuzu.yuzu_emu.features.settings.model.Settings$MenuTag" /> </activity> <action diff --git a/src/android/app/src/main/res/navigation/settings_navigation.xml b/src/android/app/src/main/res/navigation/settings_navigation.xml index 88e1b4587..1d87d36b3 100644 --- a/src/android/app/src/main/res/navigation/settings_navigation.xml +++ b/src/android/app/src/main/res/navigation/settings_navigation.xml @@ -10,7 +10,7 @@ android:label="SettingsFragment"> <argument android:name="menuTag" - app:argType="string" /> + app:argType="org.yuzu.yuzu_emu.features.settings.model.Settings$MenuTag" /> <argument android:name="game" app:argType="org.yuzu.yuzu_emu.model.Game" diff --git a/src/android/app/src/main/res/values/dimens.xml b/src/android/app/src/main/res/values/dimens.xml index 00757e5e8..7b2296d95 100644 --- a/src/android/app/src/main/res/values/dimens.xml +++ b/src/android/app/src/main/res/values/dimens.xml @@ -12,6 +12,7 @@ <dimen name="spacing_refresh_end">72dp</dimen> <dimen name="menu_width">256dp</dimen> <dimen name="card_width">165dp</dimen> + <dimen name="icon_inset">24dp</dimen> <dimen name="dialog_margin">20dp</dimen> <dimen name="elevated_app_bar">3dp</dimen> |