summaryrefslogtreecommitdiff
path: root/src/android
diff options
context:
space:
mode:
authorGravatar Abandoned Cart2023-06-10 22:42:54 -0400
committerGravatar Abandoned Cart2023-06-14 16:34:14 -0400
commitde9100ea81adec8c008b9bb9376541d2410ef4e8 (patch)
tree5b52ed8094c79f264b62c87950aa48a625e68794 /src/android
parentMerge pull request #10726 from t895/emulation-nav-component (diff)
downloadyuzu-de9100ea81adec8c008b9bb9376541d2410ef4e8.tar.gz
yuzu-de9100ea81adec8c008b9bb9376541d2410ef4e8.tar.xz
yuzu-de9100ea81adec8c008b9bb9376541d2410ef4e8.zip
android: Add Picture in Picture / Orientation
Diffstat (limited to 'src/android')
-rw-r--r--src/android/app/src/main/AndroidManifest.xml2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt99
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/BooleanSetting.kt2
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt5
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/Settings.kt9
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt8
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsFragmentPresenter.kt20
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/fragments/EmulationFragment.kt92
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt28
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt18
-rw-r--r--src/android/app/src/main/res/drawable/ic_pip_pause.xml9
-rw-r--r--src/android/app/src/main/res/drawable/ic_pip_play.xml9
-rw-r--r--src/android/app/src/main/res/layout/fragment_emulation.xml77
-rw-r--r--src/android/app/src/main/res/values/arrays.xml12
-rw-r--r--src/android/app/src/main/res/values/strings.xml12
15 files changed, 336 insertions, 66 deletions
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index b474ddb0b..e31ad69e2 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -54,6 +54,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
54 android:name="org.yuzu.yuzu_emu.activities.EmulationActivity" 54 android:name="org.yuzu.yuzu_emu.activities.EmulationActivity"
55 android:theme="@style/Theme.Yuzu.Main" 55 android:theme="@style/Theme.Yuzu.Main"
56 android:screenOrientation="userLandscape" 56 android:screenOrientation="userLandscape"
57 android:supportsPictureInPicture="true"
58 android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
57 android:exported="true"> 59 android:exported="true">
58 60
59 <intent-filter> 61 <intent-filter>
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 caf660348..e2eab3105 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
@@ -4,14 +4,23 @@
4package org.yuzu.yuzu_emu.activities 4package org.yuzu.yuzu_emu.activities
5 5
6import android.app.Activity 6import android.app.Activity
7import android.app.PendingIntent
8import android.app.PictureInPictureParams
9import android.app.RemoteAction
10import android.content.BroadcastReceiver
7import android.content.Context 11import android.content.Context
8import android.content.Intent 12import android.content.Intent
13import android.content.IntentFilter
14import android.content.res.Configuration
9import android.graphics.Rect 15import android.graphics.Rect
16import android.graphics.drawable.Icon
10import android.hardware.Sensor 17import android.hardware.Sensor
11import android.hardware.SensorEvent 18import android.hardware.SensorEvent
12import android.hardware.SensorEventListener 19import android.hardware.SensorEventListener
13import android.hardware.SensorManager 20import android.hardware.SensorManager
21import android.os.Build
14import android.os.Bundle 22import android.os.Bundle
23import android.util.Rational
15import android.view.InputDevice 24import android.view.InputDevice
16import android.view.KeyEvent 25import android.view.KeyEvent
17import android.view.MotionEvent 26import android.view.MotionEvent
@@ -27,6 +36,8 @@ import androidx.navigation.fragment.NavHostFragment
27import org.yuzu.yuzu_emu.NativeLibrary 36import org.yuzu.yuzu_emu.NativeLibrary
28import org.yuzu.yuzu_emu.R 37import org.yuzu.yuzu_emu.R
29import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding 38import org.yuzu.yuzu_emu.databinding.ActivityEmulationBinding
39import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
40import org.yuzu.yuzu_emu.features.settings.model.IntSetting
30import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel 41import org.yuzu.yuzu_emu.features.settings.model.SettingsViewModel
31import org.yuzu.yuzu_emu.model.Game 42import org.yuzu.yuzu_emu.model.Game
32import org.yuzu.yuzu_emu.utils.ControllerMappingHelper 43import org.yuzu.yuzu_emu.utils.ControllerMappingHelper
@@ -50,6 +61,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
50 private var motionTimestamp: Long = 0 61 private var motionTimestamp: Long = 0
51 private var flipMotionOrientation: Boolean = false 62 private var flipMotionOrientation: Boolean = false
52 63
64 private val actionPause = "ACTION_EMULATOR_PAUSE"
65 private val actionPlay = "ACTION_EMULATOR_PLAY"
66
53 private val settingsViewModel: SettingsViewModel by viewModels() 67 private val settingsViewModel: SettingsViewModel by viewModels()
54 68
55 override fun onDestroy() { 69 override fun onDestroy() {
@@ -120,6 +134,8 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
120 super.onResume() 134 super.onResume()
121 nfcReader.startScanning() 135 nfcReader.startScanning()
122 startMotionSensorListener() 136 startMotionSensorListener()
137
138 buildPictureInPictureParams()
123 } 139 }
124 140
125 override fun onPause() { 141 override fun onPause() {
@@ -128,6 +144,16 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
128 stopMotionSensorListener() 144 stopMotionSensorListener()
129 } 145 }
130 146
147 override fun onUserLeaveHint() {
148 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
149 if (BooleanSetting.PICTURE_IN_PICTURE.boolean && !isInPictureInPictureMode) {
150 val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
151 .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
152 enterPictureInPictureMode(pictureInPictureParamsBuilder.build())
153 }
154 }
155 }
156
131 override fun onNewIntent(intent: Intent) { 157 override fun onNewIntent(intent: Intent) {
132 super.onNewIntent(intent) 158 super.onNewIntent(intent)
133 setIntent(intent) 159 setIntent(intent)
@@ -230,6 +256,79 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
230 } 256 }
231 } 257 }
232 258
259 private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder() : PictureInPictureParams.Builder {
260 val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.int) {
261 0 -> Rational(16, 9)
262 1 -> Rational(4, 3)
263 2 -> Rational(21, 9)
264 3 -> Rational(16, 10)
265 else -> null
266 }
267 return this.apply { aspectRatio?.let { setAspectRatio(it) } }
268 }
269
270 private fun PictureInPictureParams.Builder.getPictureInPictureActionsBuilder() : PictureInPictureParams.Builder {
271 val pictureInPictureActions : MutableList<RemoteAction> = mutableListOf()
272 val pendingFlags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
273
274 val isEmulationPaused = emulationFragment?.isEmulationStatePaused() ?: false
275 if (isEmulationPaused) {
276 val playIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_play)
277 val playPendingIntent = PendingIntent.getBroadcast(
278 this@EmulationActivity, R.drawable.ic_pip_play, Intent(actionPlay), pendingFlags
279 )
280 val playRemoteAction = RemoteAction(playIcon, getString(R.string.play), getString(R.string.play), playPendingIntent)
281 pictureInPictureActions.add(playRemoteAction)
282 } else {
283 val pauseIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_pause)
284 val pausePendingIntent = PendingIntent.getBroadcast(
285 this@EmulationActivity, R.drawable.ic_pip_pause, Intent(actionPause), pendingFlags
286 )
287 val pauseRemoteAction = RemoteAction(pauseIcon, getString(R.string.pause), getString(R.string.pause), pausePendingIntent)
288 pictureInPictureActions.add(pauseRemoteAction)
289 }
290
291 return this.apply { setActions(pictureInPictureActions) }
292 }
293
294 fun buildPictureInPictureParams() {
295 val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
296 .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
297 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
298 pictureInPictureParamsBuilder.setAutoEnterEnabled(BooleanSetting.PICTURE_IN_PICTURE.boolean)
299 }
300 setPictureInPictureParams(pictureInPictureParamsBuilder.build())
301 }
302
303 private var pictureInPictureReceiver = object : BroadcastReceiver() {
304 override fun onReceive(context : Context?, intent : Intent) {
305 if (intent.action == actionPlay) {
306 emulationFragment?.onPictureInPicturePlay()
307 } else if (intent.action == actionPause) {
308 emulationFragment?.onPictureInPicturePause()
309 }
310 buildPictureInPictureParams()
311 }
312 }
313
314 override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
315 super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
316 if (isInPictureInPictureMode) {
317 IntentFilter().apply {
318 addAction(actionPause)
319 addAction(actionPlay)
320 }.also {
321 registerReceiver(pictureInPictureReceiver, it)
322 }
323 emulationFragment?.onPictureInPictureEnter()
324 } else {
325 try {
326 unregisterReceiver(pictureInPictureReceiver)
327 } catch (ignored : Exception) { }
328 emulationFragment?.onPictureInPictureLeave()
329 }
330 }
331
233 private fun startMotionSensorListener() { 332 private fun startMotionSensorListener() {
234 val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager 333 val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
235 val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) 334 val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
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 3dfd66779..63b4df273 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
@@ -8,6 +8,7 @@ enum class BooleanSetting(
8 override val section: String, 8 override val section: String,
9 override val defaultValue: Boolean 9 override val defaultValue: Boolean
10) : AbstractBooleanSetting { 10) : AbstractBooleanSetting {
11 PICTURE_IN_PICTURE("picture_in_picture", Settings.SECTION_GENERAL, true),
11 USE_CUSTOM_RTC("custom_rtc_enabled", Settings.SECTION_SYSTEM, false); 12 USE_CUSTOM_RTC("custom_rtc_enabled", Settings.SECTION_SYSTEM, false);
12 13
13 override var boolean: Boolean = defaultValue 14 override var boolean: Boolean = defaultValue
@@ -27,6 +28,7 @@ enum class BooleanSetting(
27 28
28 companion object { 29 companion object {
29 private val NOT_RUNTIME_EDITABLE = listOf( 30 private val NOT_RUNTIME_EDITABLE = listOf(
31 PICTURE_IN_PICTURE,
30 USE_CUSTOM_RTC 32 USE_CUSTOM_RTC
31 ) 33 )
32 34
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
index fa84f94f5..4427a7d9d 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/model/IntSetting.kt
@@ -93,6 +93,11 @@ enum class IntSetting(
93 Settings.SECTION_RENDERER, 93 Settings.SECTION_RENDERER,
94 0 94 0
95 ), 95 ),
96 RENDERER_SCREEN_LAYOUT(
97 "screen_layout",
98 Settings.SECTION_RENDERER,
99 Settings.LayoutOption_MobileLandscape
100 ),
96 RENDERER_ASPECT_RATIO( 101 RENDERER_ASPECT_RATIO(
97 "aspect_ratio", 102 "aspect_ratio",
98 Settings.SECTION_RENDERER, 103 Settings.SECTION_RENDERER,
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 8df20b928..4f753955b 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
@@ -133,7 +133,6 @@ class Settings {
133 const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter" 133 const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter"
134 const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable" 134 const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable"
135 const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics" 135 const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics"
136 const val PREF_MENU_SETTINGS_LANDSCAPE = "EmulationMenuSettings_LandscapeScreenLayout"
137 const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps" 136 const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps"
138 const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay" 137 const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay"
139 138
@@ -144,6 +143,14 @@ class Settings {
144 143
145 private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap() 144 private val configFileSectionsMap: MutableMap<String, List<String>> = HashMap()
146 145
146 // These must match what is defined in src/core/settings.h
147 const val LayoutOption_Default = 0
148 const val LayoutOption_SingleScreen = 1
149 const val LayoutOption_LargeScreen = 2
150 const val LayoutOption_SideScreen = 3
151 const val LayoutOption_MobilePortrait = 4
152 const val LayoutOption_MobileLandscape = 5
153
147 init { 154 init {
148 configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] = 155 configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] =
149 listOf( 156 listOf(
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 72e2cce2a..3853845ce 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
@@ -16,6 +16,7 @@ import androidx.core.view.WindowCompat
16import androidx.core.view.WindowInsetsCompat 16import androidx.core.view.WindowInsetsCompat
17import android.view.ViewGroup.MarginLayoutParams 17import android.view.ViewGroup.MarginLayoutParams
18import androidx.activity.OnBackPressedCallback 18import androidx.activity.OnBackPressedCallback
19import androidx.activity.result.ActivityResultLauncher
19import androidx.core.view.updatePadding 20import androidx.core.view.updatePadding
20import com.google.android.material.color.MaterialColors 21import com.google.android.material.color.MaterialColors
21import org.yuzu.yuzu_emu.NativeLibrary 22import org.yuzu.yuzu_emu.NativeLibrary
@@ -239,5 +240,12 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
239 settings.putExtra(ARG_GAME_ID, gameId) 240 settings.putExtra(ARG_GAME_ID, gameId)
240 context.startActivity(settings) 241 context.startActivity(settings)
241 } 242 }
243
244 fun launch(context: Context, launcher: ActivityResultLauncher<Intent>, menuTag: String?, gameId: String?) {
245 val settings = Intent(context, SettingsActivity::class.java)
246 settings.putExtra(ARG_MENU_TAG, menuTag)
247 settings.putExtra(ARG_GAME_ID, gameId)
248 launcher.launch(settings)
249 }
242 } 250 }
243} 251}
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 1ceaa6fb4..b611389a1 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
@@ -166,6 +166,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
166 IntSetting.CPU_ACCURACY.defaultValue 166 IntSetting.CPU_ACCURACY.defaultValue
167 ) 167 )
168 ) 168 )
169 add(
170 SwitchSetting(
171 BooleanSetting.PICTURE_IN_PICTURE,
172 R.string.picture_in_picture,
173 R.string.picture_in_picture_description,
174 BooleanSetting.PICTURE_IN_PICTURE.key,
175 BooleanSetting.PICTURE_IN_PICTURE.defaultValue
176 )
177 )
169 } 178 }
170 } 179 }
171 180
@@ -285,6 +294,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
285 ) 294 )
286 add( 295 add(
287 SingleChoiceSetting( 296 SingleChoiceSetting(
297 IntSetting.RENDERER_SCREEN_LAYOUT,
298 R.string.renderer_screen_layout,
299 0,
300 R.array.rendererScreenLayoutNames,
301 R.array.rendererScreenLayoutValues,
302 IntSetting.RENDERER_SCREEN_LAYOUT.key,
303 IntSetting.RENDERER_SCREEN_LAYOUT.defaultValue
304 )
305 )
306 add(
307 SingleChoiceSetting(
288 IntSetting.RENDERER_ASPECT_RATIO, 308 IntSetting.RENDERER_ASPECT_RATIO,
289 R.string.renderer_aspect_ratio, 309 R.string.renderer_aspect_ratio,
290 0, 310 0,
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 02bfcdb1e..6ea5c90f3 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
@@ -7,6 +7,7 @@ import android.annotation.SuppressLint
7import android.app.AlertDialog 7import android.app.AlertDialog
8import android.content.Context 8import android.content.Context
9import android.content.DialogInterface 9import android.content.DialogInterface
10import android.content.Intent
10import android.content.SharedPreferences 11import android.content.SharedPreferences
11import android.content.pm.ActivityInfo 12import android.content.pm.ActivityInfo
12import android.content.res.Resources 13import android.content.res.Resources
@@ -19,11 +20,14 @@ import android.util.TypedValue
19import android.view.* 20import android.view.*
20import android.widget.TextView 21import android.widget.TextView
21import androidx.activity.OnBackPressedCallback 22import androidx.activity.OnBackPressedCallback
23import androidx.activity.result.ActivityResultLauncher
24import androidx.activity.result.contract.ActivityResultContracts
22import androidx.appcompat.widget.PopupMenu 25import androidx.appcompat.widget.PopupMenu
23import androidx.core.content.res.ResourcesCompat 26import androidx.core.content.res.ResourcesCompat
24import androidx.core.graphics.Insets 27import androidx.core.graphics.Insets
25import androidx.core.view.ViewCompat 28import androidx.core.view.ViewCompat
26import androidx.core.view.WindowInsetsCompat 29import androidx.core.view.WindowInsetsCompat
30import androidx.core.view.isVisible
27import androidx.core.view.updatePadding 31import androidx.core.view.updatePadding
28import androidx.fragment.app.Fragment 32import androidx.fragment.app.Fragment
29import androidx.lifecycle.Lifecycle 33import androidx.lifecycle.Lifecycle
@@ -61,11 +65,30 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
61 65
62 val args by navArgs<EmulationFragmentArgs>() 66 val args by navArgs<EmulationFragmentArgs>()
63 67
68 private lateinit var onReturnFromSettings: ActivityResultLauncher<Intent>
69
64 override fun onAttach(context: Context) { 70 override fun onAttach(context: Context) {
65 super.onAttach(context) 71 super.onAttach(context)
66 if (context is EmulationActivity) { 72 if (context is EmulationActivity) {
67 emulationActivity = context 73 emulationActivity = context
68 NativeLibrary.setEmulationActivity(context) 74 NativeLibrary.setEmulationActivity(context)
75
76 onReturnFromSettings = context.activityResultRegistry.register(
77 "SettingsResult", ActivityResultContracts.StartActivityForResult()
78 ) {
79 binding.surfaceEmulation.setAspectRatio(
80 when (IntSetting.RENDERER_ASPECT_RATIO.int) {
81 0 -> Rational(16, 9)
82 1 -> Rational(4, 3)
83 2 -> Rational(21, 9)
84 3 -> Rational(16, 10)
85 4 -> null // Stretch
86 else -> Rational(16, 9)
87 }
88 )
89 emulationActivity?.buildPictureInPictureParams()
90 updateScreenLayout()
91 }
69 } else { 92 } else {
70 throw IllegalStateException("EmulationFragment must have EmulationActivity parent") 93 throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
71 } 94 }
@@ -129,7 +152,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
129 } 152 }
130 153
131 R.id.menu_settings -> { 154 R.id.menu_settings -> {
132 SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") 155 SettingsActivity.launch(
156 requireContext(), onReturnFromSettings, SettingsFile.FILE_NAME_CONFIG, ""
157 )
133 true 158 true
134 } 159 }
135 160
@@ -162,7 +187,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
162 lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { 187 lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
163 WindowInfoTracker.getOrCreate(requireContext()) 188 WindowInfoTracker.getOrCreate(requireContext())
164 .windowLayoutInfo(requireActivity()) 189 .windowLayoutInfo(requireActivity())
165 .collect { updateCurrentLayout(requireActivity() as EmulationActivity, it) } 190 .collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) }
166 } 191 }
167 } 192 }
168 } 193 }
@@ -204,6 +229,37 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
204 super.onDetach() 229 super.onDetach()
205 } 230 }
206 231
232 fun isEmulationStatePaused() : Boolean {
233 return this::emulationState.isInitialized && emulationState.isPaused
234 }
235
236 fun onPictureInPictureEnter() {
237 if (binding.drawerLayout.isOpen) {
238 binding.drawerLayout.close()
239 }
240 if (EmulationMenuSettings.showOverlay) {
241 binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = false }
242 }
243 }
244
245 fun onPictureInPicturePause() {
246 if (!emulationState.isPaused) {
247 emulationState.pause()
248 }
249 }
250
251 fun onPictureInPicturePlay() {
252 if (emulationState.isPaused) {
253 emulationState.run(false)
254 }
255 }
256
257 fun onPictureInPictureLeave() {
258 if (EmulationMenuSettings.showOverlay) {
259 binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = true }
260 }
261 }
262
207 private fun refreshInputOverlay() { 263 private fun refreshInputOverlay() {
208 binding.surfaceInputOverlay.refreshControls() 264 binding.surfaceInputOverlay.refreshControls()
209 } 265 }
@@ -243,15 +299,33 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
243 } 299 }
244 } 300 }
245 301
302 @SuppressLint("SourceLockedOrientationActivity")
303 private fun updateScreenLayout() {
304 emulationActivity?.let {
305 when (IntSetting.RENDERER_SCREEN_LAYOUT.int) {
306 Settings.LayoutOption_MobileLandscape -> {
307 it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
308 }
309 Settings.LayoutOption_MobilePortrait -> {
310 it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
311 }
312 Settings.LayoutOption_Default -> {
313 it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
314 }
315 else -> { it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE }
316 }
317 }
318 }
319
246 private val Number.toPx get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics).toInt() 320 private val Number.toPx get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics).toInt()
247 321
248 fun updateCurrentLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) { 322 fun updateFoldableLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) {
249 val isFolding = (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let { 323 val isFolding = (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let {
250 if (it.isSeparating) { 324 if (it.isSeparating) {
251 emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED 325 emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
252 if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) { 326 if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) {
253 binding.surfaceEmulation.layoutParams.height = it.bounds.top 327 binding.emulationContainer.layoutParams.height = it.bounds.top
254 binding.inGameMenu.layoutParams.height = it.bounds.bottom 328 // Prevent touch regions from being displayed in the hinge
255 binding.overlayContainer.layoutParams.height = it.bounds.bottom - 48.toPx 329 binding.overlayContainer.layoutParams.height = it.bounds.bottom - 48.toPx
256 binding.overlayContainer.updatePadding(0, 0, 0, 24.toPx) 330 binding.overlayContainer.updatePadding(0, 0, 0, 24.toPx)
257 } 331 }
@@ -259,14 +333,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
259 it.isSeparating 333 it.isSeparating
260 } ?: false 334 } ?: false
261 if (!isFolding) { 335 if (!isFolding) {
262 binding.surfaceEmulation.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT 336 binding.emulationContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
263 binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
264 binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT 337 binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
265 binding.overlayContainer.updatePadding(0, 0, 0, 0) 338 binding.overlayContainer.updatePadding(0, 0, 0, 0)
266 emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE 339 updateScreenLayout()
267 } 340 }
268 binding.surfaceInputOverlay.requestLayout() 341 binding.emulationContainer.requestLayout()
269 binding.inGameMenu.requestLayout()
270 binding.overlayContainer.requestLayout() 342 binding.overlayContainer.requestLayout()
271 } 343 }
272 344
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
index aa424c768..724929a04 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
@@ -3,6 +3,7 @@
3 3
4package org.yuzu.yuzu_emu.overlay 4package org.yuzu.yuzu_emu.overlay
5 5
6import android.annotation.SuppressLint
6import android.app.Activity 7import android.app.Activity
7import android.content.Context 8import android.content.Context
8import android.content.SharedPreferences 9import android.content.SharedPreferences
@@ -15,12 +16,14 @@ import android.graphics.drawable.Drawable
15import android.graphics.drawable.VectorDrawable 16import android.graphics.drawable.VectorDrawable
16import android.os.Build 17import android.os.Build
17import android.util.AttributeSet 18import android.util.AttributeSet
19import android.util.Rational
18import android.view.HapticFeedbackConstants 20import android.view.HapticFeedbackConstants
19import android.view.MotionEvent 21import android.view.MotionEvent
20import android.view.SurfaceView 22import android.view.SurfaceView
21import android.view.View 23import android.view.View
22import android.view.View.OnTouchListener 24import android.view.View.OnTouchListener
23import android.view.WindowInsets 25import android.view.WindowInsets
26import android.view.WindowManager
24import androidx.core.content.ContextCompat 27import androidx.core.content.ContextCompat
25import androidx.preference.PreferenceManager 28import androidx.preference.PreferenceManager
26import androidx.window.layout.WindowMetricsCalculator 29import androidx.window.layout.WindowMetricsCalculator
@@ -33,6 +36,7 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings
33import org.yuzu.yuzu_emu.utils.EmulationMenuSettings 36import org.yuzu.yuzu_emu.utils.EmulationMenuSettings
34import kotlin.math.max 37import kotlin.math.max
35import kotlin.math.min 38import kotlin.math.min
39import kotlin.math.roundToInt
36 40
37/** 41/**
38 * Draws the interactive input overlay on top of the 42 * Draws the interactive input overlay on top of the
@@ -73,6 +77,25 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
73 requestFocus() 77 requestFocus()
74 } 78 }
75 79
80 @SuppressLint("DrawAllocation")
81 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
82 super.onMeasure(widthMeasureSpec, heightMeasureSpec)
83 val width = MeasureSpec.getSize(widthMeasureSpec)
84 val height = MeasureSpec.getSize(heightMeasureSpec)
85 if (height > width) {
86 val aspectRatio = with (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager) {
87 val metrics = maximumWindowMetrics.bounds
88 Rational(metrics.height(), metrics.width()).toFloat()
89 }
90 val newWidth: Int = width
91 val newHeight: Int = (width / aspectRatio).roundToInt()
92 setMeasuredDimension(newWidth, newHeight)
93 invalidate()
94 } else {
95 setMeasuredDimension(width, height)
96 }
97 }
98
76 override fun draw(canvas: Canvas) { 99 override fun draw(canvas: Canvas) {
77 super.draw(canvas) 100 super.draw(canvas)
78 for (button in overlayButtons) { 101 for (button in overlayButtons) {
@@ -754,9 +777,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
754 */ 777 */
755 private fun getSafeScreenSize(context: Context): Pair<Point, Point> { 778 private fun getSafeScreenSize(context: Context): Pair<Point, Point> {
756 // Get screen size 779 // Get screen size
757 val windowMetrics = 780 val windowMetrics = WindowMetricsCalculator.getOrCreate()
758 WindowMetricsCalculator.getOrCreate() 781 .computeCurrentWindowMetrics(context as Activity)
759 .computeCurrentWindowMetrics(context as Activity)
760 var maxY = windowMetrics.bounds.height().toFloat() 782 var maxY = windowMetrics.bounds.height().toFloat()
761 var maxX = windowMetrics.bounds.width().toFloat() 783 var maxX = windowMetrics.bounds.width().toFloat()
762 var minY = 0 784 var minY = 0
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt
index e1e7a59d7..7e8f058c1 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt
@@ -11,14 +11,6 @@ object EmulationMenuSettings {
11 private val preferences = 11 private val preferences =
12 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) 12 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
13 13
14 // These must match what is defined in src/core/settings.h
15 const val LayoutOption_Default = 0
16 const val LayoutOption_SingleScreen = 1
17 const val LayoutOption_LargeScreen = 2
18 const val LayoutOption_SideScreen = 3
19 const val LayoutOption_MobilePortrait = 4
20 const val LayoutOption_MobileLandscape = 5
21
22 var joystickRelCenter: Boolean 14 var joystickRelCenter: Boolean
23 get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, true) 15 get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, true)
24 set(value) { 16 set(value) {
@@ -41,16 +33,6 @@ object EmulationMenuSettings {
41 .apply() 33 .apply()
42 } 34 }
43 35
44 var landscapeScreenLayout: Int
45 get() = preferences.getInt(
46 Settings.PREF_MENU_SETTINGS_LANDSCAPE,
47 LayoutOption_MobileLandscape
48 )
49 set(value) {
50 preferences.edit()
51 .putInt(Settings.PREF_MENU_SETTINGS_LANDSCAPE, value)
52 .apply()
53 }
54 var showFps: Boolean 36 var showFps: Boolean
55 get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, false) 37 get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, false)
56 set(value) { 38 set(value) {
diff --git a/src/android/app/src/main/res/drawable/ic_pip_pause.xml b/src/android/app/src/main/res/drawable/ic_pip_pause.xml
new file mode 100644
index 000000000..4a7d4ea03
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_pip_pause.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportHeight="24"
5 android:viewportWidth="24">
6 <path
7 android:fillColor="@android:color/white"
8 android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z" />
9</vector>
diff --git a/src/android/app/src/main/res/drawable/ic_pip_play.xml b/src/android/app/src/main/res/drawable/ic_pip_play.xml
new file mode 100644
index 000000000..2303a4623
--- /dev/null
+++ b/src/android/app/src/main/res/drawable/ic_pip_play.xml
@@ -0,0 +1,9 @@
1<vector xmlns:android="http://schemas.android.com/apk/res/android"
2 android:width="24dp"
3 android:height="24dp"
4 android:viewportHeight="24"
5 android:viewportWidth="24">
6 <path
7 android:fillColor="@android:color/white"
8 android:pathData="M8,5v14l11,-7z" />
9</vector>
diff --git a/src/android/app/src/main/res/layout/fragment_emulation.xml b/src/android/app/src/main/res/layout/fragment_emulation.xml
index 09b789b6b..ccd0f4c50 100644
--- a/src/android/app/src/main/res/layout/fragment_emulation.xml
+++ b/src/android/app/src/main/res/layout/fragment_emulation.xml
@@ -12,14 +12,21 @@
12 android:layout_width="match_parent" 12 android:layout_width="match_parent"
13 android:layout_height="match_parent"> 13 android:layout_height="match_parent">
14 14
15 <!-- This is what everything is rendered to during emulation --> 15 <FrameLayout
16 <org.yuzu.yuzu_emu.views.FixedRatioSurfaceView 16 android:id="@+id/emulation_container"
17 android:id="@+id/surface_emulation"
18 android:layout_width="match_parent" 17 android:layout_width="match_parent"
19 android:layout_height="match_parent" 18 android:layout_height="match_parent">
20 android:layout_gravity="center" 19
21 android:focusable="false" 20 <!-- This is what everything is rendered to during emulation -->
22 android:focusableInTouchMode="false" /> 21 <org.yuzu.yuzu_emu.views.FixedRatioSurfaceView
22 android:id="@+id/surface_emulation"
23 android:layout_width="match_parent"
24 android:layout_height="match_parent"
25 android:layout_gravity="center"
26 android:focusable="false"
27 android:focusableInTouchMode="false" />
28
29 </FrameLayout>
23 30
24 <FrameLayout 31 <FrameLayout
25 android:id="@+id/overlay_container" 32 android:id="@+id/overlay_container"
@@ -27,34 +34,36 @@
27 android:layout_height="match_parent" 34 android:layout_height="match_parent"
28 android:layout_gravity="bottom"> 35 android:layout_gravity="bottom">
29 36
30 <!-- This is the onscreen input overlay --> 37 <!-- This is the onscreen input overlay -->
31 <org.yuzu.yuzu_emu.overlay.InputOverlay 38 <org.yuzu.yuzu_emu.overlay.InputOverlay
32 android:id="@+id/surface_input_overlay" 39 android:id="@+id/surface_input_overlay"
33 android:layout_width="match_parent" 40 android:layout_width="match_parent"
34 android:layout_height="match_parent" 41 android:layout_height="match_parent"
35 android:focusable="true" 42 android:layout_gravity="bottom"
36 android:focusableInTouchMode="true" /> 43 android:focusable="true"
44 android:focusableInTouchMode="true" />
45
46 <TextView
47 android:id="@+id/show_fps_text"
48 android:layout_width="wrap_content"
49 android:layout_height="wrap_content"
50 android:layout_gravity="left"
51 android:clickable="false"
52 android:focusable="false"
53 android:shadowColor="@android:color/black"
54 android:textColor="@android:color/white"
55 android:textSize="12sp"
56 tools:ignore="RtlHardcoded" />
37 57
38 <TextView 58 <Button
39 android:id="@+id/show_fps_text" 59 style="@style/Widget.Material3.Button.ElevatedButton"
40 android:layout_width="wrap_content" 60 android:id="@+id/done_control_config"
41 android:layout_height="wrap_content" 61 android:layout_width="wrap_content"
42 android:layout_gravity="left" 62 android:layout_height="wrap_content"
43 android:clickable="false" 63 android:layout_gravity="center"
44 android:focusable="false" 64 android:text="@string/emulation_done"
45 android:shadowColor="@android:color/black" 65 android:visibility="gone" />
46 android:textColor="@android:color/white"
47 android:textSize="12sp"
48 tools:ignore="RtlHardcoded" />
49 66
50 <Button
51 style="@style/Widget.Material3.Button.ElevatedButton"
52 android:id="@+id/done_control_config"
53 android:layout_width="wrap_content"
54 android:layout_height="wrap_content"
55 android:layout_gravity="center"
56 android:text="@string/emulation_done"
57 android:visibility="gone" />
58 </FrameLayout> 67 </FrameLayout>
59 68
60 </androidx.coordinatorlayout.widget.CoordinatorLayout> 69 </androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -63,7 +72,7 @@
63 android:id="@+id/in_game_menu" 72 android:id="@+id/in_game_menu"
64 android:layout_width="wrap_content" 73 android:layout_width="wrap_content"
65 android:layout_height="match_parent" 74 android:layout_height="match_parent"
66 android:layout_gravity="start|bottom" 75 android:layout_gravity="start"
67 app:headerLayout="@layout/header_in_game" 76 app:headerLayout="@layout/header_in_game"
68 app:menu="@menu/menu_in_game" /> 77 app:menu="@menu/menu_in_game" />
69 78
diff --git a/src/android/app/src/main/res/values/arrays.xml b/src/android/app/src/main/res/values/arrays.xml
index ea20cb17c..7f7b1938c 100644
--- a/src/android/app/src/main/res/values/arrays.xml
+++ b/src/android/app/src/main/res/values/arrays.xml
@@ -119,6 +119,18 @@
119 <item>3</item> 119 <item>3</item>
120 </integer-array> 120 </integer-array>
121 121
122 <string-array name="rendererScreenLayoutNames">
123 <item>@string/screen_layout_landscape</item>
124 <item>@string/screen_layout_portrait</item>
125 <item>@string/screen_layout_auto</item>
126 </string-array>
127
128 <integer-array name="rendererScreenLayoutValues">
129 <item>5</item>
130 <item>4</item>
131 <item>0</item>
132 </integer-array>
133
122 <string-array name="rendererAspectRatioNames"> 134 <string-array name="rendererAspectRatioNames">
123 <item>@string/ratio_default</item> 135 <item>@string/ratio_default</item>
124 <item>@string/ratio_force_four_three</item> 136 <item>@string/ratio_force_four_three</item>
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index c236811fa..b5bc249d4 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -162,6 +162,7 @@
162 <string name="renderer_accuracy">Accuracy level</string> 162 <string name="renderer_accuracy">Accuracy level</string>
163 <string name="renderer_resolution">Resolution (Handheld/Docked)</string> 163 <string name="renderer_resolution">Resolution (Handheld/Docked)</string>
164 <string name="renderer_vsync">VSync mode</string> 164 <string name="renderer_vsync">VSync mode</string>
165 <string name="renderer_screen_layout">Orientation</string>
165 <string name="renderer_aspect_ratio">Aspect ratio</string> 166 <string name="renderer_aspect_ratio">Aspect ratio</string>
166 <string name="renderer_scaling_filter">Window adapting filter</string> 167 <string name="renderer_scaling_filter">Window adapting filter</string>
167 <string name="renderer_anti_aliasing">Anti-aliasing method</string> 168 <string name="renderer_anti_aliasing">Anti-aliasing method</string>
@@ -326,6 +327,11 @@
326 <string name="anti_aliasing_fxaa">FXAA</string> 327 <string name="anti_aliasing_fxaa">FXAA</string>
327 <string name="anti_aliasing_smaa">SMAA</string> 328 <string name="anti_aliasing_smaa">SMAA</string>
328 329
330 <!-- Screen Layouts -->
331 <string name="screen_layout_landscape">Landscape</string>
332 <string name="screen_layout_portrait">Portrait</string>
333 <string name="screen_layout_auto">Auto</string>
334
329 <!-- Aspect Ratios --> 335 <!-- Aspect Ratios -->
330 <string name="ratio_default">Default (16:9)</string> 336 <string name="ratio_default">Default (16:9)</string>
331 <string name="ratio_force_four_three">Force 4:3</string> 337 <string name="ratio_force_four_three">Force 4:3</string>
@@ -364,6 +370,12 @@
364 <string name="use_black_backgrounds">Black backgrounds</string> 370 <string name="use_black_backgrounds">Black backgrounds</string>
365 <string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string> 371 <string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string>
366 372
373 <!-- Picture-In-Picture -->
374 <string name="picture_in_picture">Picture in Picture</string>
375 <string name="picture_in_picture_description">Minimize window when placed in the background</string>
376 <string name="pause">Pause</string>
377 <string name="play">Play</string>
378
367 <!-- Licenses screen strings --> 379 <!-- Licenses screen strings -->
368 <string name="licenses">Licenses</string> 380 <string name="licenses">Licenses</string>
369 <string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string> 381 <string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string>