Merge pull request #10639 from 8bitDream/pictureinpicture
android: Support for Picture in Picture / Portrait
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -26,6 +26,8 @@ CMakeSettings.json | |||||||
| # OSX global filetypes | # OSX global filetypes | ||||||
| # Created by Finder or Spotlight in directories for various OS functionality (indexing, etc) | # Created by Finder or Spotlight in directories for various OS functionality (indexing, etc) | ||||||
| .DS_Store | .DS_Store | ||||||
|  | .DS_Store? | ||||||
|  | ._* | ||||||
| .AppleDouble | .AppleDouble | ||||||
| .LSOverride | .LSOverride | ||||||
| .Spotlight-V100 | .Spotlight-V100 | ||||||
|   | |||||||
| @@ -53,7 +53,8 @@ SPDX-License-Identifier: GPL-3.0-or-later | |||||||
|         <activity |         <activity | ||||||
|             android:name="org.yuzu.yuzu_emu.activities.EmulationActivity" |             android:name="org.yuzu.yuzu_emu.activities.EmulationActivity" | ||||||
|             android:theme="@style/Theme.Yuzu.Main" |             android:theme="@style/Theme.Yuzu.Main" | ||||||
|             android:screenOrientation="userLandscape" |             android:supportsPictureInPicture="true" | ||||||
|  |             android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode" | ||||||
|             android:exported="true"> |             android:exported="true"> | ||||||
|  |  | ||||||
|             <intent-filter> |             <intent-filter> | ||||||
|   | |||||||
| @@ -282,6 +282,11 @@ object NativeLibrary { | |||||||
|      */ |      */ | ||||||
|     external fun isRunning(): Boolean |     external fun isRunning(): Boolean | ||||||
|  |  | ||||||
|  |     /** | ||||||
|  |      * Returns true if emulation is paused. | ||||||
|  |      */ | ||||||
|  |     external fun isPaused(): Boolean | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
|      * Returns the performance stats for the current game |      * Returns the performance stats for the current game | ||||||
|      */ |      */ | ||||||
|   | |||||||
| @@ -4,14 +4,23 @@ | |||||||
| package org.yuzu.yuzu_emu.activities | package org.yuzu.yuzu_emu.activities | ||||||
|  |  | ||||||
| import android.app.Activity | import android.app.Activity | ||||||
|  | import android.app.PendingIntent | ||||||
|  | import android.app.PictureInPictureParams | ||||||
|  | import android.app.RemoteAction | ||||||
|  | import android.content.BroadcastReceiver | ||||||
| import android.content.Context | import android.content.Context | ||||||
| import android.content.Intent | import android.content.Intent | ||||||
|  | import android.content.IntentFilter | ||||||
|  | import android.content.res.Configuration | ||||||
| import android.graphics.Rect | import android.graphics.Rect | ||||||
|  | import android.graphics.drawable.Icon | ||||||
| import android.hardware.Sensor | import android.hardware.Sensor | ||||||
| import android.hardware.SensorEvent | import android.hardware.SensorEvent | ||||||
| import android.hardware.SensorEventListener | import android.hardware.SensorEventListener | ||||||
| import android.hardware.SensorManager | import android.hardware.SensorManager | ||||||
|  | import android.os.Build | ||||||
| import android.os.Bundle | import android.os.Bundle | ||||||
|  | import android.util.Rational | ||||||
| import android.view.InputDevice | import android.view.InputDevice | ||||||
| import android.view.KeyEvent | import android.view.KeyEvent | ||||||
| import android.view.MotionEvent | import android.view.MotionEvent | ||||||
| @@ -27,6 +36,8 @@ import androidx.navigation.fragment.NavHostFragment | |||||||
| import org.yuzu.yuzu_emu.NativeLibrary | import org.yuzu.yuzu_emu.NativeLibrary | ||||||
| import org.yuzu.yuzu_emu.R | import org.yuzu.yuzu_emu.R | ||||||
| import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding | import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding | ||||||
|  | import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting | ||||||
|  | import org.yuzu.yuzu_emu.features.settings.model.IntSetting | ||||||
| import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel | import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel | ||||||
| import org.yuzu.yuzu_emu.model.Game | import org.yuzu.yuzu_emu.model.Game | ||||||
| import org.yuzu.yuzu_emu.utils.ControllerMappingHelper | import org.yuzu.yuzu_emu.utils.ControllerMappingHelper | ||||||
| @@ -50,6 +61,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||||||
|     private var motionTimestamp: Long = 0 |     private var motionTimestamp: Long = 0 | ||||||
|     private var flipMotionOrientation: Boolean = false |     private var flipMotionOrientation: Boolean = false | ||||||
|  |  | ||||||
|  |     private val actionPause = "ACTION_EMULATOR_PAUSE" | ||||||
|  |     private val actionPlay = "ACTION_EMULATOR_PLAY" | ||||||
|  |  | ||||||
|     private val settingsViewModel: SettingsViewModel by viewModels() |     private val settingsViewModel: SettingsViewModel by viewModels() | ||||||
|  |  | ||||||
|     override fun onDestroy() { |     override fun onDestroy() { | ||||||
| @@ -120,6 +134,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||||||
|         super.onResume() |         super.onResume() | ||||||
|         nfcReader.startScanning() |         nfcReader.startScanning() | ||||||
|         startMotionSensorListener() |         startMotionSensorListener() | ||||||
|  |  | ||||||
|  |         buildPictureInPictureParams() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override fun onPause() { |     override fun onPause() { | ||||||
| @@ -128,6 +144,16 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||||||
|         stopMotionSensorListener() |         stopMotionSensorListener() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     override fun onUserLeaveHint() { | ||||||
|  |         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { | ||||||
|  |             if (BooleanSetting.PICTURE_IN_PICTURE.boolean && !isInPictureInPictureMode) { | ||||||
|  |                 val pictureInPictureParamsBuilder = PictureInPictureParams.Builder() | ||||||
|  |                     .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder() | ||||||
|  |                 enterPictureInPictureMode(pictureInPictureParamsBuilder.build()) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     override fun onNewIntent(intent: Intent) { |     override fun onNewIntent(intent: Intent) { | ||||||
|         super.onNewIntent(intent) |         super.onNewIntent(intent) | ||||||
|         setIntent(intent) |         setIntent(intent) | ||||||
| @@ -230,6 +256,96 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder(): PictureInPictureParams.Builder { | ||||||
|  |         val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.int) { | ||||||
|  |             0 -> Rational(16, 9) | ||||||
|  |             1 -> Rational(4, 3) | ||||||
|  |             2 -> Rational(21, 9) | ||||||
|  |             3 -> Rational(16, 10) | ||||||
|  |             else -> null // Best fit | ||||||
|  |         } | ||||||
|  |         return this.apply { aspectRatio?.let { setAspectRatio(it) } } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun PictureInPictureParams.Builder.getPictureInPictureActionsBuilder(): PictureInPictureParams.Builder { | ||||||
|  |         val pictureInPictureActions: MutableList<RemoteAction> = mutableListOf() | ||||||
|  |         val pendingFlags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE | ||||||
|  |  | ||||||
|  |         if (NativeLibrary.isPaused()) { | ||||||
|  |             val playIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_play) | ||||||
|  |             val playPendingIntent = PendingIntent.getBroadcast( | ||||||
|  |                 this@EmulationActivity, | ||||||
|  |                 R.drawable.ic_pip_play, | ||||||
|  |                 Intent(actionPlay), | ||||||
|  |                 pendingFlags | ||||||
|  |             ) | ||||||
|  |             val playRemoteAction = RemoteAction( | ||||||
|  |                 playIcon, | ||||||
|  |                 getString(R.string.play), | ||||||
|  |                 getString(R.string.play), | ||||||
|  |                 playPendingIntent | ||||||
|  |             ) | ||||||
|  |             pictureInPictureActions.add(playRemoteAction) | ||||||
|  |         } else { | ||||||
|  |             val pauseIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_pause) | ||||||
|  |             val pausePendingIntent = PendingIntent.getBroadcast( | ||||||
|  |                 this@EmulationActivity, | ||||||
|  |                 R.drawable.ic_pip_pause, | ||||||
|  |                 Intent(actionPause), | ||||||
|  |                 pendingFlags | ||||||
|  |             ) | ||||||
|  |             val pauseRemoteAction = RemoteAction( | ||||||
|  |                 pauseIcon, | ||||||
|  |                 getString(R.string.pause), | ||||||
|  |                 getString(R.string.pause), | ||||||
|  |                 pausePendingIntent | ||||||
|  |             ) | ||||||
|  |             pictureInPictureActions.add(pauseRemoteAction) | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return this.apply { setActions(pictureInPictureActions) } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     fun buildPictureInPictureParams() { | ||||||
|  |         val pictureInPictureParamsBuilder = PictureInPictureParams.Builder() | ||||||
|  |             .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder() | ||||||
|  |         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||||||
|  |             pictureInPictureParamsBuilder.setAutoEnterEnabled(BooleanSetting.PICTURE_IN_PICTURE.boolean) | ||||||
|  |         } | ||||||
|  |         setPictureInPictureParams(pictureInPictureParamsBuilder.build()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private var pictureInPictureReceiver = object : BroadcastReceiver() { | ||||||
|  |         override fun onReceive(context: Context?, intent: Intent) { | ||||||
|  |             if (intent.action == actionPlay) { | ||||||
|  |                 if (NativeLibrary.isPaused()) NativeLibrary.unPauseEmulation() | ||||||
|  |             } else if (intent.action == actionPause) { | ||||||
|  |                 if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation() | ||||||
|  |             } | ||||||
|  |             buildPictureInPictureParams() | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onPictureInPictureModeChanged( | ||||||
|  |         isInPictureInPictureMode: Boolean, | ||||||
|  |         newConfig: Configuration | ||||||
|  |     ) { | ||||||
|  |         super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) | ||||||
|  |         if (isInPictureInPictureMode) { | ||||||
|  |             IntentFilter().apply { | ||||||
|  |                 addAction(actionPause) | ||||||
|  |                 addAction(actionPlay) | ||||||
|  |             }.also { | ||||||
|  |                 registerReceiver(pictureInPictureReceiver, it) | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             try { | ||||||
|  |                 unregisterReceiver(pictureInPictureReceiver) | ||||||
|  |             } catch (ignored : Exception) { | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     private fun startMotionSensorListener() { |     private fun startMotionSensorListener() { | ||||||
|         val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager |         val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager | ||||||
|         val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) |         val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ enum class BooleanSetting( | |||||||
|     override val section: String, |     override val section: String, | ||||||
|     override val defaultValue: Boolean |     override val defaultValue: Boolean | ||||||
| ) : AbstractBooleanSetting { | ) : AbstractBooleanSetting { | ||||||
|  |     PICTURE_IN_PICTURE("picture_in_picture", Settings.SECTION_GENERAL, true), | ||||||
|     USE_CUSTOM_RTC("custom_rtc_enabled", Settings.SECTION_SYSTEM, false); |     USE_CUSTOM_RTC("custom_rtc_enabled", Settings.SECTION_SYSTEM, false); | ||||||
|  |  | ||||||
|     override var boolean: Boolean = defaultValue |     override var boolean: Boolean = defaultValue | ||||||
| @@ -27,6 +28,7 @@ enum class BooleanSetting( | |||||||
|  |  | ||||||
|     companion object { |     companion object { | ||||||
|         private val NOT_RUNTIME_EDITABLE = listOf( |         private val NOT_RUNTIME_EDITABLE = listOf( | ||||||
|  |             PICTURE_IN_PICTURE, | ||||||
|             USE_CUSTOM_RTC |             USE_CUSTOM_RTC | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -93,6 +93,11 @@ enum class IntSetting( | |||||||
|         Settings.SECTION_RENDERER, |         Settings.SECTION_RENDERER, | ||||||
|         0 |         0 | ||||||
|     ), |     ), | ||||||
|  |     RENDERER_SCREEN_LAYOUT( | ||||||
|  |         "screen_layout", | ||||||
|  |         Settings.SECTION_RENDERER, | ||||||
|  |         Settings.LayoutOption_MobileLandscape | ||||||
|  |     ), | ||||||
|     RENDERER_ASPECT_RATIO( |     RENDERER_ASPECT_RATIO( | ||||||
|         "aspect_ratio", |         "aspect_ratio", | ||||||
|         Settings.SECTION_RENDERER, |         Settings.SECTION_RENDERER, | ||||||
|   | |||||||
| @@ -133,7 +133,6 @@ class Settings { | |||||||
|         const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter" |         const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter" | ||||||
|         const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable" |         const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable" | ||||||
|         const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics" |         const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics" | ||||||
|         const val PREF_MENU_SETTINGS_LANDSCAPE = "EmulationMenuSettings_LandscapeScreenLayout" |  | ||||||
|         const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps" |         const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps" | ||||||
|         const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay" |         const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay" | ||||||
|  |  | ||||||
| @@ -144,6 +143,10 @@ class Settings { | |||||||
|  |  | ||||||
|         private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap() |         private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap() | ||||||
|  |  | ||||||
|  |         const val LayoutOption_Unspecified = 0 | ||||||
|  |         const val LayoutOption_MobilePortrait = 4 | ||||||
|  |         const val LayoutOption_MobileLandscape = 5 | ||||||
|  |  | ||||||
|         init { |         init { | ||||||
|             configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] = |             configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] = | ||||||
|                 listOf( |                 listOf( | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ import androidx.core.view.WindowCompat | |||||||
| import androidx.core.view.WindowInsetsCompat | import androidx.core.view.WindowInsetsCompat | ||||||
| import android.view.ViewGroup.MarginLayoutParams | import android.view.ViewGroup.MarginLayoutParams | ||||||
| import androidx.activity.OnBackPressedCallback | import androidx.activity.OnBackPressedCallback | ||||||
|  | import androidx.activity.result.ActivityResultLauncher | ||||||
| import androidx.core.view.updatePadding | import androidx.core.view.updatePadding | ||||||
| import com.google.android.material.color.MaterialColors | import com.google.android.material.color.MaterialColors | ||||||
| import org.yuzu.yuzu_emu.NativeLibrary | import org.yuzu.yuzu_emu.NativeLibrary | ||||||
| @@ -239,5 +240,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView { | |||||||
|             settings.putExtra(ARG_GAME_ID, gameId) |             settings.putExtra(ARG_GAME_ID, gameId) | ||||||
|             context.startActivity(settings) |             context.startActivity(settings) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         fun launch( | ||||||
|  |             context: Context, | ||||||
|  |             launcher: ActivityResultLauncher<Intent>, | ||||||
|  |             menuTag: String?, | ||||||
|  |             gameId: String? | ||||||
|  |         ) { | ||||||
|  |             val settings = Intent(context, SettingsActivity::class.java) | ||||||
|  |             settings.putExtra(ARG_MENU_TAG, menuTag) | ||||||
|  |             settings.putExtra(ARG_GAME_ID, gameId) | ||||||
|  |             launcher.launch(settings) | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -166,6 +166,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) | |||||||
|                     IntSetting.CPU_ACCURACY.defaultValue |                     IntSetting.CPU_ACCURACY.defaultValue | ||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|  |             add( | ||||||
|  |                 SwitchSetting( | ||||||
|  |                     BooleanSetting.PICTURE_IN_PICTURE, | ||||||
|  |                     R.string.picture_in_picture, | ||||||
|  |                     R.string.picture_in_picture_description, | ||||||
|  |                     BooleanSetting.PICTURE_IN_PICTURE.key, | ||||||
|  |                     BooleanSetting.PICTURE_IN_PICTURE.defaultValue | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -283,6 +292,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) | |||||||
|                     IntSetting.RENDERER_ANTI_ALIASING.defaultValue |                     IntSetting.RENDERER_ANTI_ALIASING.defaultValue | ||||||
|                 ) |                 ) | ||||||
|             ) |             ) | ||||||
|  |             add( | ||||||
|  |                 SingleChoiceSetting( | ||||||
|  |                     IntSetting.RENDERER_SCREEN_LAYOUT, | ||||||
|  |                     R.string.renderer_screen_layout, | ||||||
|  |                     0, | ||||||
|  |                     R.array.rendererScreenLayoutNames, | ||||||
|  |                     R.array.rendererScreenLayoutValues, | ||||||
|  |                     IntSetting.RENDERER_SCREEN_LAYOUT.key, | ||||||
|  |                     IntSetting.RENDERER_SCREEN_LAYOUT.defaultValue | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|             add( |             add( | ||||||
|                 SingleChoiceSetting( |                 SingleChoiceSetting( | ||||||
|                     IntSetting.RENDERER_ASPECT_RATIO, |                     IntSetting.RENDERER_ASPECT_RATIO, | ||||||
|   | |||||||
| @@ -7,24 +7,26 @@ import android.annotation.SuppressLint | |||||||
| import android.app.AlertDialog | import android.app.AlertDialog | ||||||
| import android.content.Context | import android.content.Context | ||||||
| import android.content.DialogInterface | import android.content.DialogInterface | ||||||
|  | import android.content.Intent | ||||||
| import android.content.SharedPreferences | import android.content.SharedPreferences | ||||||
| import android.content.pm.ActivityInfo | import android.content.pm.ActivityInfo | ||||||
| import android.content.res.Resources | import android.content.res.Configuration | ||||||
| import android.graphics.Color | import android.graphics.Color | ||||||
| import android.os.Bundle | import android.os.Bundle | ||||||
| import android.os.Handler | import android.os.Handler | ||||||
| import android.os.Looper | import android.os.Looper | ||||||
| import android.util.Rational | import android.util.Rational | ||||||
| import android.util.TypedValue |  | ||||||
| import android.view.* | import android.view.* | ||||||
| import android.widget.TextView | import android.widget.TextView | ||||||
| import androidx.activity.OnBackPressedCallback | import androidx.activity.OnBackPressedCallback | ||||||
|  | import androidx.activity.result.ActivityResultLauncher | ||||||
|  | import androidx.activity.result.contract.ActivityResultContracts | ||||||
| import androidx.appcompat.widget.PopupMenu | import androidx.appcompat.widget.PopupMenu | ||||||
| import androidx.core.content.res.ResourcesCompat | import androidx.core.content.res.ResourcesCompat | ||||||
| import androidx.core.graphics.Insets | import androidx.core.graphics.Insets | ||||||
| import androidx.core.view.ViewCompat | import androidx.core.view.ViewCompat | ||||||
| import androidx.core.view.WindowInsetsCompat | import androidx.core.view.WindowInsetsCompat | ||||||
| import androidx.core.view.updatePadding | import androidx.core.view.isVisible | ||||||
| import androidx.fragment.app.Fragment | import androidx.fragment.app.Fragment | ||||||
| import androidx.lifecycle.Lifecycle | import androidx.lifecycle.Lifecycle | ||||||
| import androidx.lifecycle.lifecycleScope | import androidx.lifecycle.lifecycleScope | ||||||
| @@ -48,6 +50,7 @@ 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.model.Settings | ||||||
| import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity | import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity | ||||||
| import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile | import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile | ||||||
|  | import org.yuzu.yuzu_emu.overlay.InputOverlay | ||||||
| import org.yuzu.yuzu_emu.utils.* | import org.yuzu.yuzu_emu.utils.* | ||||||
|  |  | ||||||
| class EmulationFragment : Fragment(), SurfaceHolder.Callback { | class EmulationFragment : Fragment(), SurfaceHolder.Callback { | ||||||
| @@ -61,11 +64,40 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||||||
|  |  | ||||||
|     val args by navArgs<EmulationFragmentArgs>() |     val args by navArgs<EmulationFragmentArgs>() | ||||||
|  |  | ||||||
|  |     private var isInFoldableLayout = false | ||||||
|  |  | ||||||
|  |     private lateinit var onReturnFromSettings: ActivityResultLauncher<Intent> | ||||||
|  |  | ||||||
|     override fun onAttach(context: Context) { |     override fun onAttach(context: Context) { | ||||||
|         super.onAttach(context) |         super.onAttach(context) | ||||||
|         if (context is EmulationActivity) { |         if (context is EmulationActivity) { | ||||||
|             emulationActivity = context |             emulationActivity = context | ||||||
|             NativeLibrary.setEmulationActivity(context) |             NativeLibrary.setEmulationActivity(context) | ||||||
|  |  | ||||||
|  |             lifecycleScope.launch(Dispatchers.Main) { | ||||||
|  |                 lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { | ||||||
|  |                     WindowInfoTracker.getOrCreate(context) | ||||||
|  |                         .windowLayoutInfo(context) | ||||||
|  |                         .collect { updateFoldableLayout(context, it) } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             onReturnFromSettings = context.activityResultRegistry.register( | ||||||
|  |                 "SettingsResult", ActivityResultContracts.StartActivityForResult() | ||||||
|  |             ) { | ||||||
|  |                 binding.surfaceEmulation.setAspectRatio( | ||||||
|  |                     when (IntSetting.RENDERER_ASPECT_RATIO.int) { | ||||||
|  |                         0 -> Rational(16, 9) | ||||||
|  |                         1 -> Rational(4, 3) | ||||||
|  |                         2 -> Rational(21, 9) | ||||||
|  |                         3 -> Rational(16, 10) | ||||||
|  |                         4 -> null // Stretch | ||||||
|  |                         else -> Rational(16, 9) | ||||||
|  |                     } | ||||||
|  |                 ) | ||||||
|  |                 emulationActivity?.buildPictureInPictureParams() | ||||||
|  |                 updateScreenLayout() | ||||||
|  |             } | ||||||
|         } else { |         } else { | ||||||
|             throw IllegalStateException("EmulationFragment must have EmulationActivity parent") |             throw IllegalStateException("EmulationFragment must have EmulationActivity parent") | ||||||
|         } |         } | ||||||
| @@ -129,7 +161,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 R.id.menu_settings -> { |                 R.id.menu_settings -> { | ||||||
|                     SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") |                     SettingsActivity.launch( | ||||||
|  |                         requireContext(), | ||||||
|  |                         onReturnFromSettings, | ||||||
|  |                         SettingsFile.FILE_NAME_CONFIG, | ||||||
|  |                         "" | ||||||
|  |                     ) | ||||||
|                     true |                     true | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
| @@ -162,7 +199,33 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||||||
|             lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { |             lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { | ||||||
|                 WindowInfoTracker.getOrCreate(requireContext()) |                 WindowInfoTracker.getOrCreate(requireContext()) | ||||||
|                     .windowLayoutInfo(requireActivity()) |                     .windowLayoutInfo(requireActivity()) | ||||||
|                     .collect { updateCurrentLayout(requireActivity() as EmulationActivity, it) } |                     .collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     override fun onConfigurationChanged(newConfig: Configuration) { | ||||||
|  |         super.onConfigurationChanged(newConfig) | ||||||
|  |         if (emulationActivity?.isInPictureInPictureMode == true) { | ||||||
|  |             if (binding.drawerLayout.isOpen) { | ||||||
|  |                 binding.drawerLayout.close() | ||||||
|  |             } | ||||||
|  |             if (EmulationMenuSettings.showOverlay) { | ||||||
|  |                 binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = false } | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             if (EmulationMenuSettings.showOverlay) { | ||||||
|  |                 binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = true } | ||||||
|  |             } | ||||||
|  |             if (!isInFoldableLayout) { | ||||||
|  |                 if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) { | ||||||
|  |                     binding.surfaceInputOverlay.orientation = InputOverlay.PORTRAIT | ||||||
|  |                 } else { | ||||||
|  |                     binding.surfaceInputOverlay.orientation = InputOverlay.LANDSCAPE | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             if (!binding.surfaceInputOverlay.isInEditMode) { | ||||||
|  |                 refreshInputOverlay() | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -184,6 +247,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |         updateScreenLayout() | ||||||
|  |  | ||||||
|         emulationState.run(emulationActivity!!.isActivityRecreated) |         emulationState.run(emulationActivity!!.isActivityRecreated) | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -243,31 +308,50 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private val Number.toPx get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics).toInt() |     @SuppressLint("SourceLockedOrientationActivity") | ||||||
|  |     private fun updateScreenLayout() { | ||||||
|  |         emulationActivity?.let { | ||||||
|  |             it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) { | ||||||
|  |                 Settings.LayoutOption_MobileLandscape -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE | ||||||
|  |                 Settings.LayoutOption_MobilePortrait -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT | ||||||
|  |                 Settings.LayoutOption_Unspecified -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED | ||||||
|  |                 else -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         onConfigurationChanged(resources.configuration) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     fun updateCurrentLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) { |     private fun updateFoldableLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) { | ||||||
|         val isFolding = (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let { |         val isFolding = (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let { | ||||||
|             if (it.isSeparating) { |             if (it.isSeparating) { | ||||||
|                 emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED |                 emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED | ||||||
|                 if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) { |                 if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) { | ||||||
|                     binding.surfaceEmulation.layoutParams.height = it.bounds.top |                     // Restrict emulation and overlays to the top of the screen | ||||||
|  |                     binding.emulationContainer.layoutParams.height = it.bounds.top | ||||||
|  |                     binding.overlayContainer.layoutParams.height = it.bounds.top | ||||||
|  |                     // Restrict input and menu drawer to the bottom of the screen | ||||||
|  |                     binding.inputContainer.layoutParams.height = it.bounds.bottom | ||||||
|                     binding.inGameMenu.layoutParams.height = it.bounds.bottom |                     binding.inGameMenu.layoutParams.height = it.bounds.bottom | ||||||
|                     binding.overlayContainer.layoutParams.height = it.bounds.bottom - 48.toPx |  | ||||||
|                     binding.overlayContainer.updatePadding(0, 0, 0, 24.toPx) |                     isInFoldableLayout = true | ||||||
|  |                     binding.surfaceInputOverlay.orientation = InputOverlay.FOLDABLE | ||||||
|  |                     refreshInputOverlay() | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             it.isSeparating |             it.isSeparating | ||||||
|         } ?: false |         } ?: false | ||||||
|         if (!isFolding) { |         if (!isFolding) { | ||||||
|             binding.surfaceEmulation.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT |             binding.emulationContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT | ||||||
|             binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT |             binding.inputContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT | ||||||
|             binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT |             binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT | ||||||
|             binding.overlayContainer.updatePadding(0, 0, 0, 0) |             binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT | ||||||
|             emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE |             isInFoldableLayout = false | ||||||
|  |             updateScreenLayout() | ||||||
|         } |         } | ||||||
|         binding.surfaceInputOverlay.requestLayout() |         binding.emulationContainer.requestLayout() | ||||||
|         binding.inGameMenu.requestLayout() |         binding.inputContainer.requestLayout() | ||||||
|         binding.overlayContainer.requestLayout() |         binding.overlayContainer.requestLayout() | ||||||
|  |         binding.inGameMenu.requestLayout() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     override fun surfaceCreated(holder: SurfaceHolder) { |     override fun surfaceCreated(holder: SurfaceHolder) { | ||||||
| @@ -397,7 +481,19 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||||||
|         popup.show() |         popup.show() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     @SuppressLint("SourceLockedOrientationActivity") | ||||||
|     private fun startConfiguringControls() { |     private fun startConfiguringControls() { | ||||||
|  |         // Lock the current orientation to prevent editing inconsistencies | ||||||
|  |         if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) { | ||||||
|  |             emulationActivity?.let { | ||||||
|  |                 it.requestedOrientation = | ||||||
|  |                     if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { | ||||||
|  |                         ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT | ||||||
|  |                     } else { | ||||||
|  |                         ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE | ||||||
|  |                     } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|         binding.doneControlConfig.visibility = View.VISIBLE |         binding.doneControlConfig.visibility = View.VISIBLE | ||||||
|         binding.surfaceInputOverlay.setIsInEditMode(true) |         binding.surfaceInputOverlay.setIsInEditMode(true) | ||||||
|     } |     } | ||||||
| @@ -405,6 +501,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { | |||||||
|     private fun stopConfiguringControls() { |     private fun stopConfiguringControls() { | ||||||
|         binding.doneControlConfig.visibility = View.GONE |         binding.doneControlConfig.visibility = View.GONE | ||||||
|         binding.surfaceInputOverlay.setIsInEditMode(false) |         binding.surfaceInputOverlay.setIsInEditMode(false) | ||||||
|  |         // Unlock the orientation if it was locked for editing | ||||||
|  |         if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) { | ||||||
|  |             emulationActivity?.let { | ||||||
|  |                 it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @SuppressLint("SetTextI18n") |     @SuppressLint("SetTextI18n") | ||||||
|   | |||||||
| @@ -51,12 +51,14 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context | |||||||
|  |  | ||||||
|     private lateinit var windowInsets: WindowInsets |     private lateinit var windowInsets: WindowInsets | ||||||
|  |  | ||||||
|  |     var orientation = LANDSCAPE | ||||||
|  |  | ||||||
|     override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { |     override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { | ||||||
|         super.onLayout(changed, left, top, right, bottom) |         super.onLayout(changed, left, top, right, bottom) | ||||||
|  |  | ||||||
|         windowInsets = rootWindowInsets |         windowInsets = rootWindowInsets | ||||||
|  |  | ||||||
|         if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) { |         if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) { | ||||||
|             defaultOverlay() |             defaultOverlay() | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -233,10 +235,6 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context | |||||||
|         val fingerPositionX = event.getX(pointerIndex).toInt() |         val fingerPositionX = event.getX(pointerIndex).toInt() | ||||||
|         val fingerPositionY = event.getY(pointerIndex).toInt() |         val fingerPositionY = event.getY(pointerIndex).toInt() | ||||||
|  |  | ||||||
|         // TODO: Provide support for portrait layout |  | ||||||
|         //val orientation = |  | ||||||
|         //    if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else "" |  | ||||||
|  |  | ||||||
|         for (button in overlayButtons) { |         for (button in overlayButtons) { | ||||||
|             // Determine the button state to apply based on the MotionEvent action flag. |             // Determine the button state to apply based on the MotionEvent action flag. | ||||||
|             when (event.action and MotionEvent.ACTION_MASK) { |             when (event.action and MotionEvent.ACTION_MASK) { | ||||||
| @@ -266,7 +264,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context | |||||||
|                         buttonBeingConfigured!!.buttonId, |                         buttonBeingConfigured!!.buttonId, | ||||||
|                         buttonBeingConfigured!!.bounds.centerX(), |                         buttonBeingConfigured!!.bounds.centerX(), | ||||||
|                         buttonBeingConfigured!!.bounds.centerY(), |                         buttonBeingConfigured!!.bounds.centerY(), | ||||||
|                         "" |                         orientation | ||||||
|                     ) |                     ) | ||||||
|                     buttonBeingConfigured = null |                     buttonBeingConfigured = null | ||||||
|                 } |                 } | ||||||
| @@ -299,7 +297,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context | |||||||
|                         dpadBeingConfigured!!.upId, |                         dpadBeingConfigured!!.upId, | ||||||
|                         dpadBeingConfigured!!.bounds.centerX(), |                         dpadBeingConfigured!!.bounds.centerX(), | ||||||
|                         dpadBeingConfigured!!.bounds.centerY(), |                         dpadBeingConfigured!!.bounds.centerY(), | ||||||
|                         "" |                         orientation | ||||||
|                     ) |                     ) | ||||||
|                     dpadBeingConfigured = null |                     dpadBeingConfigured = null | ||||||
|                 } |                 } | ||||||
| @@ -330,7 +328,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context | |||||||
|                         joystickBeingConfigured!!.buttonId, |                         joystickBeingConfigured!!.buttonId, | ||||||
|                         joystickBeingConfigured!!.bounds.centerX(), |                         joystickBeingConfigured!!.bounds.centerX(), | ||||||
|                         joystickBeingConfigured!!.bounds.centerY(), |                         joystickBeingConfigured!!.bounds.centerY(), | ||||||
|                         "" |                         orientation | ||||||
|                     ) |                     ) | ||||||
|                     joystickBeingConfigured = null |                     joystickBeingConfigured = null | ||||||
|                 } |                 } | ||||||
| @@ -533,8 +531,6 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context | |||||||
|         overlayButtons.clear() |         overlayButtons.clear() | ||||||
|         overlayDpads.clear() |         overlayDpads.clear() | ||||||
|         overlayJoysticks.clear() |         overlayJoysticks.clear() | ||||||
|         val orientation = |  | ||||||
|             if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else "" |  | ||||||
|  |  | ||||||
|         // Add all the enabled overlay items back to the HashSet. |         // Add all the enabled overlay items back to the HashSet. | ||||||
|         if (EmulationMenuSettings.showOverlay) { |         if (EmulationMenuSettings.showOverlay) { | ||||||
| @@ -548,8 +544,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context | |||||||
|         val min = windowSize.first |         val min = windowSize.first | ||||||
|         val max = windowSize.second |         val max = windowSize.second | ||||||
|         PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit() |         PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit() | ||||||
|             .putFloat("$sharedPrefsId$orientation-X", (x - min.x).toFloat() / max.x) |             .putFloat("$sharedPrefsId-X$orientation", (x - min.x).toFloat() / max.x) | ||||||
|             .putFloat("$sharedPrefsId$orientation-Y", (y - min.y).toFloat() / max.y) |             .putFloat("$sharedPrefsId-Y$orientation", (y - min.y).toFloat() / max.y) | ||||||
|             .apply() |             .apply() | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -558,145 +554,250 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     private fun defaultOverlay() { |     private fun defaultOverlay() { | ||||||
|         if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) { |         if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) { | ||||||
|             defaultOverlayLandscape() |             defaultOverlayByLayout(orientation) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         resetButtonPlacement() |         resetButtonPlacement() | ||||||
|         preferences.edit() |         preferences.edit() | ||||||
|             .putBoolean(Settings.PREF_OVERLAY_INIT, true) |             .putBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", true) | ||||||
|             .apply() |             .apply() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     fun resetButtonPlacement() { |     fun resetButtonPlacement() { | ||||||
|         defaultOverlayLandscape() |         defaultOverlayByLayout(orientation) | ||||||
|         refreshControls() |         refreshControls() | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     private fun defaultOverlayLandscape() { |     private val landscapeResources = arrayOf( | ||||||
|  |         R.integer.SWITCH_BUTTON_A_X, | ||||||
|  |         R.integer.SWITCH_BUTTON_A_Y, | ||||||
|  |         R.integer.SWITCH_BUTTON_B_X, | ||||||
|  |         R.integer.SWITCH_BUTTON_B_Y, | ||||||
|  |         R.integer.SWITCH_BUTTON_X_X, | ||||||
|  |         R.integer.SWITCH_BUTTON_X_Y, | ||||||
|  |         R.integer.SWITCH_BUTTON_Y_X, | ||||||
|  |         R.integer.SWITCH_BUTTON_Y_Y, | ||||||
|  |         R.integer.SWITCH_TRIGGER_ZL_X, | ||||||
|  |         R.integer.SWITCH_TRIGGER_ZL_Y, | ||||||
|  |         R.integer.SWITCH_TRIGGER_ZR_X, | ||||||
|  |         R.integer.SWITCH_TRIGGER_ZR_Y, | ||||||
|  |         R.integer.SWITCH_BUTTON_DPAD_X, | ||||||
|  |         R.integer.SWITCH_BUTTON_DPAD_Y, | ||||||
|  |         R.integer.SWITCH_TRIGGER_L_X, | ||||||
|  |         R.integer.SWITCH_TRIGGER_L_Y, | ||||||
|  |         R.integer.SWITCH_TRIGGER_R_X, | ||||||
|  |         R.integer.SWITCH_TRIGGER_R_Y, | ||||||
|  |         R.integer.SWITCH_BUTTON_PLUS_X, | ||||||
|  |         R.integer.SWITCH_BUTTON_PLUS_Y, | ||||||
|  |         R.integer.SWITCH_BUTTON_MINUS_X, | ||||||
|  |         R.integer.SWITCH_BUTTON_MINUS_Y, | ||||||
|  |         R.integer.SWITCH_BUTTON_HOME_X, | ||||||
|  |         R.integer.SWITCH_BUTTON_HOME_Y, | ||||||
|  |         R.integer.SWITCH_BUTTON_CAPTURE_X, | ||||||
|  |         R.integer.SWITCH_BUTTON_CAPTURE_Y, | ||||||
|  |         R.integer.SWITCH_STICK_R_X, | ||||||
|  |         R.integer.SWITCH_STICK_R_Y, | ||||||
|  |         R.integer.SWITCH_STICK_L_X, | ||||||
|  |         R.integer.SWITCH_STICK_L_Y | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     private val portraitResources = arrayOf( | ||||||
|  |         R.integer.SWITCH_BUTTON_A_X_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_BUTTON_A_Y_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_BUTTON_B_X_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_BUTTON_B_Y_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_BUTTON_X_X_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_BUTTON_X_Y_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_BUTTON_Y_X_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_BUTTON_Y_Y_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_TRIGGER_ZL_X_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_TRIGGER_ZL_Y_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_TRIGGER_ZR_X_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_TRIGGER_ZR_Y_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_BUTTON_DPAD_X_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_BUTTON_DPAD_Y_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_TRIGGER_L_X_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_TRIGGER_L_Y_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_TRIGGER_R_X_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_TRIGGER_R_Y_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_BUTTON_PLUS_X_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_BUTTON_PLUS_Y_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_BUTTON_MINUS_X_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_BUTTON_MINUS_Y_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_BUTTON_HOME_X_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_BUTTON_HOME_Y_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_BUTTON_CAPTURE_X_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_BUTTON_CAPTURE_Y_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_STICK_R_X_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_STICK_R_Y_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_STICK_L_X_PORTRAIT, | ||||||
|  |         R.integer.SWITCH_STICK_L_Y_PORTRAIT | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     private val foldableResources = arrayOf( | ||||||
|  |         R.integer.SWITCH_BUTTON_A_X_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_BUTTON_A_Y_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_BUTTON_B_X_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_BUTTON_B_Y_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_BUTTON_X_X_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_BUTTON_X_Y_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_BUTTON_Y_X_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_BUTTON_Y_Y_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_TRIGGER_ZL_X_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_TRIGGER_ZL_Y_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_TRIGGER_ZR_X_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_TRIGGER_ZR_Y_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_BUTTON_DPAD_X_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_BUTTON_DPAD_Y_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_TRIGGER_L_X_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_TRIGGER_L_Y_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_TRIGGER_R_X_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_TRIGGER_R_Y_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_BUTTON_PLUS_X_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_BUTTON_PLUS_Y_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_BUTTON_MINUS_X_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_BUTTON_MINUS_Y_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_BUTTON_HOME_X_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_BUTTON_HOME_Y_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_BUTTON_CAPTURE_X_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_BUTTON_CAPTURE_Y_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_STICK_R_X_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_STICK_R_Y_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_STICK_L_X_FOLDABLE, | ||||||
|  |         R.integer.SWITCH_STICK_L_Y_FOLDABLE | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     private fun getResourceValue(orientation: String, position: Int) : Float { | ||||||
|  |         return when (orientation) { | ||||||
|  |             PORTRAIT -> resources.getInteger(portraitResources[position]).toFloat() / 1000 | ||||||
|  |             FOLDABLE -> resources.getInteger(foldableResources[position]).toFloat() / 1000 | ||||||
|  |             else -> resources.getInteger(landscapeResources[position]).toFloat() / 1000 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     private fun defaultOverlayByLayout(orientation: String) { | ||||||
|         // Each value represents the position of the button in relation to the screen size without insets. |         // Each value represents the position of the button in relation to the screen size without insets. | ||||||
|         preferences.edit() |         preferences.edit() | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.BUTTON_A.toString() + "-X", |                 ButtonType.BUTTON_A.toString() + "-X$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_A_X).toFloat() / 1000 |                 getResourceValue(orientation, 0) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.BUTTON_A.toString() + "-Y", |                 ButtonType.BUTTON_A.toString() + "-Y$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_A_Y).toFloat() / 1000 |                 getResourceValue(orientation, 1) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.BUTTON_B.toString() + "-X", |                 ButtonType.BUTTON_B.toString() + "-X$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_B_X).toFloat() / 1000 |                 getResourceValue(orientation, 2) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.BUTTON_B.toString() + "-Y", |                 ButtonType.BUTTON_B.toString() + "-Y$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_B_Y).toFloat() / 1000 |                 getResourceValue(orientation, 3) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.BUTTON_X.toString() + "-X", |                 ButtonType.BUTTON_X.toString() + "-X$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_X_X).toFloat() / 1000 |                 getResourceValue(orientation, 4) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.BUTTON_X.toString() + "-Y", |                 ButtonType.BUTTON_X.toString() + "-Y$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_X_Y).toFloat() / 1000 |                 getResourceValue(orientation, 5) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.BUTTON_Y.toString() + "-X", |                 ButtonType.BUTTON_Y.toString() + "-X$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_Y_X).toFloat() / 1000 |                 getResourceValue(orientation, 6) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.BUTTON_Y.toString() + "-Y", |                 ButtonType.BUTTON_Y.toString() + "-Y$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_Y_Y).toFloat() / 1000 |                 getResourceValue(orientation, 7) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.TRIGGER_ZL.toString() + "-X", |                 ButtonType.TRIGGER_ZL.toString() + "-X$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_X).toFloat() / 1000 |                 getResourceValue(orientation, 8) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.TRIGGER_ZL.toString() + "-Y", |                 ButtonType.TRIGGER_ZL.toString() + "-Y$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y).toFloat() / 1000 |                 getResourceValue(orientation, 9) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.TRIGGER_ZR.toString() + "-X", |                 ButtonType.TRIGGER_ZR.toString() + "-X$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_X).toFloat() / 1000 |                 getResourceValue(orientation, 10) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.TRIGGER_ZR.toString() + "-Y", |                 ButtonType.TRIGGER_ZR.toString() + "-Y$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y).toFloat() / 1000 |                 getResourceValue(orientation, 11) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.DPAD_UP.toString() + "-X", |                 ButtonType.DPAD_UP.toString() + "-X$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_X).toFloat() / 1000 |                 getResourceValue(orientation, 12) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.DPAD_UP.toString() + "-Y", |                 ButtonType.DPAD_UP.toString() + "-Y$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y).toFloat() / 1000 |                 getResourceValue(orientation, 13) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.TRIGGER_L.toString() + "-X", |                 ButtonType.TRIGGER_L.toString() + "-X$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_TRIGGER_L_X).toFloat() / 1000 |                 getResourceValue(orientation, 14) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.TRIGGER_L.toString() + "-Y", |                 ButtonType.TRIGGER_L.toString() + "-Y$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_TRIGGER_L_Y).toFloat() / 1000 |                 getResourceValue(orientation, 15) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.TRIGGER_R.toString() + "-X", |                 ButtonType.TRIGGER_R.toString() + "-X$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_TRIGGER_R_X).toFloat() / 1000 |                 getResourceValue(orientation, 16) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.TRIGGER_R.toString() + "-Y", |                 ButtonType.TRIGGER_R.toString() + "-Y$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_TRIGGER_R_Y).toFloat() / 1000 |                 getResourceValue(orientation, 17) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.BUTTON_PLUS.toString() + "-X", |                 ButtonType.BUTTON_PLUS.toString() + "-X$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_X).toFloat() / 1000 |                 getResourceValue(orientation, 18) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.BUTTON_PLUS.toString() + "-Y", |                 ButtonType.BUTTON_PLUS.toString() + "-Y$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y).toFloat() / 1000 |                 getResourceValue(orientation, 19) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.BUTTON_MINUS.toString() + "-X", |                 ButtonType.BUTTON_MINUS.toString() + "-X$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_X).toFloat() / 1000 |                 getResourceValue(orientation, 20) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.BUTTON_MINUS.toString() + "-Y", |                 ButtonType.BUTTON_MINUS.toString() + "-Y$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y).toFloat() / 1000 |                 getResourceValue(orientation, 21) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.BUTTON_HOME.toString() + "-X", |                 ButtonType.BUTTON_HOME.toString() + "-X$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_HOME_X).toFloat() / 1000 |                 getResourceValue(orientation, 22) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.BUTTON_HOME.toString() + "-Y", |                 ButtonType.BUTTON_HOME.toString() + "-Y$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_HOME_Y).toFloat() / 1000 |                 getResourceValue(orientation, 23) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.BUTTON_CAPTURE.toString() + "-X", |                 ButtonType.BUTTON_CAPTURE.toString() + "-X$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X) |                 getResourceValue(orientation, 24) | ||||||
|                     .toFloat() / 1000 |  | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.BUTTON_CAPTURE.toString() + "-Y", |                 ButtonType.BUTTON_CAPTURE.toString() + "-Y$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y) |                 getResourceValue(orientation, 25) | ||||||
|                     .toFloat() / 1000 |  | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.STICK_R.toString() + "-X", |                 ButtonType.STICK_R.toString() + "-X$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_STICK_R_X).toFloat() / 1000 |                 getResourceValue(orientation, 26) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.STICK_R.toString() + "-Y", |                 ButtonType.STICK_R.toString() + "-Y$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_STICK_R_Y).toFloat() / 1000 |                 getResourceValue(orientation, 27) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.STICK_L.toString() + "-X", |                 ButtonType.STICK_L.toString() + "-X$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_STICK_L_X).toFloat() / 1000 |                 getResourceValue(orientation, 28) | ||||||
|             ) |             ) | ||||||
|             .putFloat( |             .putFloat( | ||||||
|                 ButtonType.STICK_L.toString() + "-Y", |                 ButtonType.STICK_L.toString() + "-Y$orientation", | ||||||
|                 resources.getInteger(R.integer.SWITCH_STICK_L_Y).toFloat() / 1000 |                 getResourceValue(orientation, 29) | ||||||
|             ) |             ) | ||||||
|             .apply() |             .apply() | ||||||
|     } |     } | ||||||
| @@ -709,6 +810,10 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context | |||||||
|         private val preferences: SharedPreferences = |         private val preferences: SharedPreferences = | ||||||
|             PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) |             PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) | ||||||
|  |  | ||||||
|  |         const val LANDSCAPE = "" | ||||||
|  |         const val PORTRAIT = "_Portrait" | ||||||
|  |         const val FOLDABLE = "_Foldable" | ||||||
|  |  | ||||||
|         /** |         /** | ||||||
|          * Resizes a [Bitmap] by a given scale factor |          * Resizes a [Bitmap] by a given scale factor | ||||||
|          * |          * | ||||||
| @@ -754,8 +859,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context | |||||||
|          */ |          */ | ||||||
|         private fun getSafeScreenSize(context: Context): Pair<Point, Point> { |         private fun getSafeScreenSize(context: Context): Pair<Point, Point> { | ||||||
|             // Get screen size |             // Get screen size | ||||||
|             val windowMetrics = |             val windowMetrics = WindowMetricsCalculator.getOrCreate() | ||||||
|                 WindowMetricsCalculator.getOrCreate() |  | ||||||
|                 .computeCurrentWindowMetrics(context as Activity) |                 .computeCurrentWindowMetrics(context as Activity) | ||||||
|             var maxY = windowMetrics.bounds.height().toFloat() |             var maxY = windowMetrics.bounds.height().toFloat() | ||||||
|             var maxX = windowMetrics.bounds.width().toFloat() |             var maxX = windowMetrics.bounds.width().toFloat() | ||||||
| @@ -769,9 +873,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context | |||||||
|                 val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout |                 val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout | ||||||
|                 if (insets != null) { |                 if (insets != null) { | ||||||
|                     if (insets.boundingRectTop.bottom != 0 && insets.boundingRectTop.bottom > maxY / 2) |                     if (insets.boundingRectTop.bottom != 0 && insets.boundingRectTop.bottom > maxY / 2) | ||||||
|                         insets.boundingRectTop.bottom.toFloat() else maxY |                         maxY = insets.boundingRectTop.bottom.toFloat() | ||||||
|                     if (insets.boundingRectRight.left != 0 && insets.boundingRectRight.left > maxX / 2) |                     if (insets.boundingRectRight.left != 0 && insets.boundingRectRight.left > maxX / 2) | ||||||
|                         insets.boundingRectRight.left.toFloat() else maxX |                         maxX = insets.boundingRectRight.left.toFloat() | ||||||
|  |  | ||||||
|                     minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left |                     minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left | ||||||
|                     minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom |                     minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom | ||||||
| @@ -878,8 +982,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context | |||||||
|  |  | ||||||
|             // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. |             // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. | ||||||
|             // These were set in the input overlay configuration menu. |             // These were set in the input overlay configuration menu. | ||||||
|             val xKey = "$buttonId$orientation-X" |             val xKey = "$buttonId-X$orientation" | ||||||
|             val yKey = "$buttonId$orientation-Y" |             val yKey = "$buttonId-Y$orientation" | ||||||
|             val drawableXPercent = sPrefs.getFloat(xKey, 0f) |             val drawableXPercent = sPrefs.getFloat(xKey, 0f) | ||||||
|             val drawableYPercent = sPrefs.getFloat(yKey, 0f) |             val drawableYPercent = sPrefs.getFloat(yKey, 0f) | ||||||
|             val drawableX = (drawableXPercent * max.x + min.x).toInt() |             val drawableX = (drawableXPercent * max.x + min.x).toInt() | ||||||
| @@ -959,8 +1063,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context | |||||||
|  |  | ||||||
|             // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. |             // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. | ||||||
|             // These were set in the input overlay configuration menu. |             // These were set in the input overlay configuration menu. | ||||||
|             val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-X", 0f) |             val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}-X$orientation", 0f) | ||||||
|             val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-Y", 0f) |             val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}-Y$orientation", 0f) | ||||||
|             val drawableX = (drawableXPercent * max.x + min.x).toInt() |             val drawableX = (drawableXPercent * max.x + min.x).toInt() | ||||||
|             val drawableY = (drawableYPercent * max.y + min.y).toInt() |             val drawableY = (drawableYPercent * max.y + min.y).toInt() | ||||||
|             val width = overlayDrawable.width |             val width = overlayDrawable.width | ||||||
| @@ -1026,8 +1130,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context | |||||||
|  |  | ||||||
|             // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. |             // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. | ||||||
|             // These were set in the input overlay configuration menu. |             // These were set in the input overlay configuration menu. | ||||||
|             val drawableXPercent = sPrefs.getFloat("$button$orientation-X", 0f) |             val drawableXPercent = sPrefs.getFloat("$button-X$orientation", 0f) | ||||||
|             val drawableYPercent = sPrefs.getFloat("$button$orientation-Y", 0f) |             val drawableYPercent = sPrefs.getFloat("$button-Y$orientation", 0f) | ||||||
|             val drawableX = (drawableXPercent * max.x + min.x).toInt() |             val drawableX = (drawableXPercent * max.x + min.x).toInt() | ||||||
|             val drawableY = (drawableYPercent * max.y + min.y).toInt() |             val drawableY = (drawableYPercent * max.y + min.y).toInt() | ||||||
|             val outerScale = 1.66f |             val outerScale = 1.66f | ||||||
|   | |||||||
| @@ -11,14 +11,6 @@ object EmulationMenuSettings { | |||||||
|     private val preferences = |     private val preferences = | ||||||
|         PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) |         PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) | ||||||
|  |  | ||||||
|     // These must match what is defined in src/core/settings.h |  | ||||||
|     const val LayoutOption_Default = 0 |  | ||||||
|     const val LayoutOption_SingleScreen = 1 |  | ||||||
|     const val LayoutOption_LargeScreen = 2 |  | ||||||
|     const val LayoutOption_SideScreen = 3 |  | ||||||
|     const val LayoutOption_MobilePortrait = 4 |  | ||||||
|     const val LayoutOption_MobileLandscape = 5 |  | ||||||
|  |  | ||||||
|     var joystickRelCenter: Boolean |     var joystickRelCenter: Boolean | ||||||
|         get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, true) |         get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, true) | ||||||
|         set(value) { |         set(value) { | ||||||
| @@ -41,16 +33,6 @@ object EmulationMenuSettings { | |||||||
|                 .apply() |                 .apply() | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     var landscapeScreenLayout: Int |  | ||||||
|         get() = preferences.getInt( |  | ||||||
|             Settings.PREF_MENU_SETTINGS_LANDSCAPE, |  | ||||||
|             LayoutOption_MobileLandscape |  | ||||||
|         ) |  | ||||||
|         set(value) { |  | ||||||
|             preferences.edit() |  | ||||||
|                 .putInt(Settings.PREF_MENU_SETTINGS_LANDSCAPE, value) |  | ||||||
|                 .apply() |  | ||||||
|         } |  | ||||||
|     var showFps: Boolean |     var showFps: Boolean | ||||||
|         get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, false) |         get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, false) | ||||||
|         set(value) { |         set(value) { | ||||||
|   | |||||||
| @@ -202,6 +202,11 @@ public: | |||||||
|         return m_is_running; |         return m_is_running; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     bool IsPaused() const { | ||||||
|  |         std::scoped_lock lock(m_mutex); | ||||||
|  |         return m_is_running && m_is_paused; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const Core::PerfStatsResults& PerfStats() const { |     const Core::PerfStatsResults& PerfStats() const { | ||||||
|         std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex); |         std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex); | ||||||
|         return m_perf_stats; |         return m_perf_stats; | ||||||
| @@ -287,11 +292,13 @@ public: | |||||||
|     void PauseEmulation() { |     void PauseEmulation() { | ||||||
|         std::scoped_lock lock(m_mutex); |         std::scoped_lock lock(m_mutex); | ||||||
|         m_system.Pause(); |         m_system.Pause(); | ||||||
|  |         m_is_paused = true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     void UnPauseEmulation() { |     void UnPauseEmulation() { | ||||||
|         std::scoped_lock lock(m_mutex); |         std::scoped_lock lock(m_mutex); | ||||||
|         m_system.Run(); |         m_system.Run(); | ||||||
|  |         m_is_paused = false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     void HaltEmulation() { |     void HaltEmulation() { | ||||||
| @@ -473,6 +480,7 @@ private: | |||||||
|     std::shared_ptr<FileSys::VfsFilesystem> m_vfs; |     std::shared_ptr<FileSys::VfsFilesystem> m_vfs; | ||||||
|     Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized}; |     Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized}; | ||||||
|     bool m_is_running{}; |     bool m_is_running{}; | ||||||
|  |     bool m_is_paused{}; | ||||||
|     SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{}; |     SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{}; | ||||||
|     std::unique_ptr<Service::Account::ProfileManager> m_profile_manager; |     std::unique_ptr<Service::Account::ProfileManager> m_profile_manager; | ||||||
|  |  | ||||||
| @@ -583,6 +591,11 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isRunning([[maybe_unused]] JNIEnv | |||||||
|     return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning()); |     return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning()); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused([[maybe_unused]] JNIEnv* env, | ||||||
|  |                                                         [[maybe_unused]] jclass clazz) { | ||||||
|  |     return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused()); | ||||||
|  | } | ||||||
|  |  | ||||||
| jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly([[maybe_unused]] JNIEnv* env, | jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly([[maybe_unused]] JNIEnv* env, | ||||||
|                                                               [[maybe_unused]] jclass clazz) { |                                                               [[maybe_unused]] jclass clazz) { | ||||||
|     return EmulationSession::GetInstance().IsHandheldOnly(); |     return EmulationSession::GetInstance().IsHandheldOnly(); | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_pip_pause.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_pip_pause.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:width="24dp" | ||||||
|  |     android:height="24dp" | ||||||
|  |     android:viewportHeight="24" | ||||||
|  |     android:viewportWidth="24"> | ||||||
|  |     <path | ||||||
|  |         android:fillColor="@android:color/white" | ||||||
|  |         android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" /> | ||||||
|  | </vector> | ||||||
							
								
								
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_pip_play.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/android/app/src/main/res/drawable/ic_pip_play.xml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:width="24dp" | ||||||
|  |     android:height="24dp" | ||||||
|  |     android:viewportHeight="24" | ||||||
|  |     android:viewportWidth="24"> | ||||||
|  |     <path | ||||||
|  |         android:fillColor="@android:color/white" | ||||||
|  |         android:pathData="M8,5v14l11,-7z" /> | ||||||
|  | </vector> | ||||||
| @@ -12,6 +12,11 @@ | |||||||
|         android:layout_width="match_parent" |         android:layout_width="match_parent" | ||||||
|         android:layout_height="match_parent"> |         android:layout_height="match_parent"> | ||||||
|  |  | ||||||
|  |         <FrameLayout | ||||||
|  |             android:id="@+id/emulation_container" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="match_parent"> | ||||||
|  |  | ||||||
|             <!-- This is what everything is rendered to during emulation --> |             <!-- This is what everything is rendered to during emulation --> | ||||||
|             <org.yuzu.yuzu_emu.views.FixedRatioSurfaceView |             <org.yuzu.yuzu_emu.views.FixedRatioSurfaceView | ||||||
|                 android:id="@+id/surface_emulation" |                 android:id="@+id/surface_emulation" | ||||||
| @@ -21,8 +26,10 @@ | |||||||
|                 android:focusable="false" |                 android:focusable="false" | ||||||
|                 android:focusableInTouchMode="false" /> |                 android:focusableInTouchMode="false" /> | ||||||
|  |  | ||||||
|  |         </FrameLayout> | ||||||
|  |  | ||||||
|         <FrameLayout |         <FrameLayout | ||||||
|             android:id="@+id/overlay_container" |             android:id="@+id/input_container" | ||||||
|             android:layout_width="match_parent" |             android:layout_width="match_parent" | ||||||
|             android:layout_height="match_parent" |             android:layout_height="match_parent" | ||||||
|             android:layout_gravity="bottom"> |             android:layout_gravity="bottom"> | ||||||
| @@ -32,9 +39,26 @@ | |||||||
|                 android:id="@+id/surface_input_overlay" |                 android:id="@+id/surface_input_overlay" | ||||||
|                 android:layout_width="match_parent" |                 android:layout_width="match_parent" | ||||||
|                 android:layout_height="match_parent" |                 android:layout_height="match_parent" | ||||||
|  |                 android:layout_gravity="center" | ||||||
|                 android:focusable="true" |                 android:focusable="true" | ||||||
|                 android:focusableInTouchMode="true" /> |                 android:focusableInTouchMode="true" /> | ||||||
|  |  | ||||||
|  |             <Button | ||||||
|  |                 style="@style/Widget.Material3.Button.ElevatedButton" | ||||||
|  |                 android:id="@+id/done_control_config" | ||||||
|  |                 android:layout_width="wrap_content" | ||||||
|  |                 android:layout_height="wrap_content" | ||||||
|  |                 android:layout_gravity="center" | ||||||
|  |                 android:text="@string/emulation_done" | ||||||
|  |                 android:visibility="gone" /> | ||||||
|  |  | ||||||
|  |         </FrameLayout> | ||||||
|  |  | ||||||
|  |         <FrameLayout | ||||||
|  |             android:id="@+id/overlay_container" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="match_parent"> | ||||||
|  |  | ||||||
|             <TextView |             <TextView | ||||||
|                 android:id="@+id/show_fps_text" |                 android:id="@+id/show_fps_text" | ||||||
|                 android:layout_width="wrap_content" |                 android:layout_width="wrap_content" | ||||||
| @@ -47,14 +71,6 @@ | |||||||
|                 android:textSize="12sp" |                 android:textSize="12sp" | ||||||
|                 tools:ignore="RtlHardcoded" /> |                 tools:ignore="RtlHardcoded" /> | ||||||
|  |  | ||||||
|         <Button |  | ||||||
|             style="@style/Widget.Material3.Button.ElevatedButton" |  | ||||||
|             android:id="@+id/done_control_config" |  | ||||||
|             android:layout_width="wrap_content" |  | ||||||
|             android:layout_height="wrap_content" |  | ||||||
|             android:layout_gravity="center" |  | ||||||
|             android:text="@string/emulation_done" |  | ||||||
|             android:visibility="gone" /> |  | ||||||
|         </FrameLayout> |         </FrameLayout> | ||||||
|  |  | ||||||
|     </androidx.coordinatorlayout.widget.CoordinatorLayout> |     </androidx.coordinatorlayout.widget.CoordinatorLayout> | ||||||
|   | |||||||
| @@ -119,6 +119,18 @@ | |||||||
|         <item>3</item> |         <item>3</item> | ||||||
|     </integer-array> |     </integer-array> | ||||||
|  |  | ||||||
|  |     <string-array name="rendererScreenLayoutNames"> | ||||||
|  |         <item>@string/screen_layout_landscape</item> | ||||||
|  |         <item>@string/screen_layout_portrait</item> | ||||||
|  |         <item>@string/screen_layout_auto</item> | ||||||
|  |     </string-array> | ||||||
|  |  | ||||||
|  |     <integer-array name="rendererScreenLayoutValues"> | ||||||
|  |         <item>5</item> | ||||||
|  |         <item>4</item> | ||||||
|  |         <item>0</item> | ||||||
|  |     </integer-array> | ||||||
|  |  | ||||||
|     <string-array name="rendererAspectRatioNames"> |     <string-array name="rendererAspectRatioNames"> | ||||||
|         <item>@string/ratio_default</item> |         <item>@string/ratio_default</item> | ||||||
|         <item>@string/ratio_force_four_three</item> |         <item>@string/ratio_force_four_three</item> | ||||||
|   | |||||||
| @@ -34,4 +34,68 @@ | |||||||
|     <integer name="SWITCH_BUTTON_DPAD_X">260</integer> |     <integer name="SWITCH_BUTTON_DPAD_X">260</integer> | ||||||
|     <integer name="SWITCH_BUTTON_DPAD_Y">790</integer> |     <integer name="SWITCH_BUTTON_DPAD_Y">790</integer> | ||||||
|  |  | ||||||
|  |     <!-- Default SWITCH portrait layout --> | ||||||
|  |     <integer name="SWITCH_BUTTON_A_X_PORTRAIT">840</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_A_Y_PORTRAIT">840</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_B_X_PORTRAIT">740</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_B_Y_PORTRAIT">880</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_X_X_PORTRAIT">740</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_X_Y_PORTRAIT">800</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_Y_X_PORTRAIT">640</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_Y_Y_PORTRAIT">840</integer> | ||||||
|  |     <integer name="SWITCH_STICK_L_X_PORTRAIT">180</integer> | ||||||
|  |     <integer name="SWITCH_STICK_L_Y_PORTRAIT">660</integer> | ||||||
|  |     <integer name="SWITCH_STICK_R_X_PORTRAIT">820</integer> | ||||||
|  |     <integer name="SWITCH_STICK_R_Y_PORTRAIT">660</integer> | ||||||
|  |     <integer name="SWITCH_TRIGGER_L_X_PORTRAIT">140</integer> | ||||||
|  |     <integer name="SWITCH_TRIGGER_L_Y_PORTRAIT">260</integer> | ||||||
|  |     <integer name="SWITCH_TRIGGER_R_X_PORTRAIT">860</integer> | ||||||
|  |     <integer name="SWITCH_TRIGGER_R_Y_PORTRAIT">260</integer> | ||||||
|  |     <integer name="SWITCH_TRIGGER_ZL_X_PORTRAIT">140</integer> | ||||||
|  |     <integer name="SWITCH_TRIGGER_ZL_Y_PORTRAIT">200</integer> | ||||||
|  |     <integer name="SWITCH_TRIGGER_ZR_X_PORTRAIT">860</integer> | ||||||
|  |     <integer name="SWITCH_TRIGGER_ZR_Y_PORTRAIT">200</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_MINUS_X_PORTRAIT">440</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_MINUS_Y_PORTRAIT">950</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_PLUS_X_PORTRAIT">560</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_PLUS_Y_PORTRAIT">950</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_HOME_X_PORTRAIT">680</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_HOME_Y_PORTRAIT">950</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_CAPTURE_X_PORTRAIT">320</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_CAPTURE_Y_PORTRAIT">950</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_DPAD_X_PORTRAIT">240</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_DPAD_Y_PORTRAIT">840</integer> | ||||||
|  |  | ||||||
|  |     <!-- Default SWITCH foldable layout --> | ||||||
|  |     <integer name="SWITCH_BUTTON_A_X_FOLDABLE">840</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_A_Y_FOLDABLE">390</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_B_X_FOLDABLE">740</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_B_Y_FOLDABLE">430</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_X_X_FOLDABLE">740</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_X_Y_FOLDABLE">350</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_Y_X_FOLDABLE">640</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_Y_Y_FOLDABLE">390</integer> | ||||||
|  |     <integer name="SWITCH_STICK_L_X_FOLDABLE">180</integer> | ||||||
|  |     <integer name="SWITCH_STICK_L_Y_FOLDABLE">250</integer> | ||||||
|  |     <integer name="SWITCH_STICK_R_X_FOLDABLE">820</integer> | ||||||
|  |     <integer name="SWITCH_STICK_R_Y_FOLDABLE">250</integer> | ||||||
|  |     <integer name="SWITCH_TRIGGER_L_X_FOLDABLE">140</integer> | ||||||
|  |     <integer name="SWITCH_TRIGGER_L_Y_FOLDABLE">130</integer> | ||||||
|  |     <integer name="SWITCH_TRIGGER_R_X_FOLDABLE">860</integer> | ||||||
|  |     <integer name="SWITCH_TRIGGER_R_Y_FOLDABLE">130</integer> | ||||||
|  |     <integer name="SWITCH_TRIGGER_ZL_X_FOLDABLE">140</integer> | ||||||
|  |     <integer name="SWITCH_TRIGGER_ZL_Y_FOLDABLE">70</integer> | ||||||
|  |     <integer name="SWITCH_TRIGGER_ZR_X_FOLDABLE">860</integer> | ||||||
|  |     <integer name="SWITCH_TRIGGER_ZR_Y_FOLDABLE">70</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_MINUS_X_FOLDABLE">440</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_MINUS_Y_FOLDABLE">470</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_PLUS_X_FOLDABLE">560</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_PLUS_Y_FOLDABLE">470</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_HOME_X_FOLDABLE">680</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_HOME_Y_FOLDABLE">470</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_CAPTURE_X_FOLDABLE">320</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_CAPTURE_Y_FOLDABLE">470</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_DPAD_X_FOLDABLE">240</integer> | ||||||
|  |     <integer name="SWITCH_BUTTON_DPAD_Y_FOLDABLE">390</integer> | ||||||
|  |  | ||||||
| </resources> | </resources> | ||||||
|   | |||||||
| @@ -162,6 +162,7 @@ | |||||||
|     <string name="renderer_accuracy">Accuracy level</string> |     <string name="renderer_accuracy">Accuracy level</string> | ||||||
|     <string name="renderer_resolution">Resolution (Handheld/Docked)</string> |     <string name="renderer_resolution">Resolution (Handheld/Docked)</string> | ||||||
|     <string name="renderer_vsync">VSync mode</string> |     <string name="renderer_vsync">VSync mode</string> | ||||||
|  |     <string name="renderer_screen_layout">Orientation</string> | ||||||
|     <string name="renderer_aspect_ratio">Aspect ratio</string> |     <string name="renderer_aspect_ratio">Aspect ratio</string> | ||||||
|     <string name="renderer_scaling_filter">Window adapting filter</string> |     <string name="renderer_scaling_filter">Window adapting filter</string> | ||||||
|     <string name="renderer_anti_aliasing">Anti-aliasing method</string> |     <string name="renderer_anti_aliasing">Anti-aliasing method</string> | ||||||
| @@ -326,6 +327,11 @@ | |||||||
|     <string name="anti_aliasing_fxaa">FXAA</string> |     <string name="anti_aliasing_fxaa">FXAA</string> | ||||||
|     <string name="anti_aliasing_smaa">SMAA</string> |     <string name="anti_aliasing_smaa">SMAA</string> | ||||||
|  |  | ||||||
|  |     <!-- Screen Layouts --> | ||||||
|  |     <string name="screen_layout_landscape">Landscape</string> | ||||||
|  |     <string name="screen_layout_portrait">Portrait</string> | ||||||
|  |     <string name="screen_layout_auto">Auto</string> | ||||||
|  |  | ||||||
|     <!-- Aspect Ratios --> |     <!-- Aspect Ratios --> | ||||||
|     <string name="ratio_default">Default (16:9)</string> |     <string name="ratio_default">Default (16:9)</string> | ||||||
|     <string name="ratio_force_four_three">Force 4:3</string> |     <string name="ratio_force_four_three">Force 4:3</string> | ||||||
| @@ -364,6 +370,12 @@ | |||||||
|     <string name="use_black_backgrounds">Black backgrounds</string> |     <string name="use_black_backgrounds">Black backgrounds</string> | ||||||
|     <string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string> |     <string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string> | ||||||
|  |  | ||||||
|  |     <!-- Picture-In-Picture --> | ||||||
|  |     <string name="picture_in_picture">Picture in Picture</string> | ||||||
|  |     <string name="picture_in_picture_description">Minimize window when placed in the background</string> | ||||||
|  |     <string name="pause">Pause</string> | ||||||
|  |     <string name="play">Play</string> | ||||||
|  |  | ||||||
|     <!-- Licenses screen strings --> |     <!-- Licenses screen strings --> | ||||||
|     <string name="licenses">Licenses</string> |     <string name="licenses">Licenses</string> | ||||||
|     <string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string> |     <string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user