summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar bunnei2023-06-15 16:40:13 -0700
committerGravatar GitHub2023-06-15 16:40:13 -0700
commit3c217a5574b4179c8bb19d94bb5f735ee25f46bb (patch)
treeadeddcf478b3268a4cf18a46f3a730c5cc585c97
parentMerge pull request #10729 from liamwhite/windows-is-a-meme (diff)
parentandroid: Move overlays to their own layout (diff)
downloadyuzu-3c217a5574b4179c8bb19d94bb5f735ee25f46bb.tar.gz
yuzu-3c217a5574b4179c8bb19d94bb5f735ee25f46bb.tar.xz
yuzu-3c217a5574b4179c8bb19d94bb5f735ee25f46bb.zip
Merge pull request #10639 from 8bitDream/pictureinpicture
android: Support for Picture in Picture / Portrait
Diffstat (limited to '')
-rw-r--r--.gitignore2
-rw-r--r--src/android/app/src/main/AndroidManifest.xml3
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt5
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt116
-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.kt5
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/features/settings/ui/SettingsActivity.kt13
-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.kt134
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt284
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/utils/EmulationMenuSettings.kt18
-rw-r--r--src/android/app/src/main/jni/native.cpp13
-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.xml82
-rw-r--r--src/android/app/src/main/res/values/arrays.xml12
-rw-r--r--src/android/app/src/main/res/values/integers.xml64
-rw-r--r--src/android/app/src/main/res/values/strings.xml12
19 files changed, 649 insertions, 159 deletions
diff --git a/.gitignore b/.gitignore
index a5f7248c7..fbadb208b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,8 @@ CMakeSettings.json
26# OSX global filetypes 26# OSX global filetypes
27# Created by Finder or Spotlight in directories for various OS functionality (indexing, etc) 27# Created by Finder or Spotlight in directories for various OS functionality (indexing, etc)
28.DS_Store 28.DS_Store
29.DS_Store?
30._*
29.AppleDouble 31.AppleDouble
30.LSOverride 32.LSOverride
31.Spotlight-V100 33.Spotlight-V100
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index b474ddb0b..a6f87fc2e 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -53,7 +53,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
53 <activity 53 <activity
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:supportsPictureInPicture="true"
57 android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
57 android:exported="true"> 58 android:exported="true">
58 59
59 <intent-filter> 60 <intent-filter>
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
index 4be9ade14..22f0a2646 100644
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/NativeLibrary.kt
@@ -283,6 +283,11 @@ object NativeLibrary {
283 external fun isRunning(): Boolean 283 external fun isRunning(): Boolean
284 284
285 /** 285 /**
286 * Returns true if emulation is paused.
287 */
288 external fun isPaused(): Boolean
289
290 /**
286 * Returns the performance stats for the current game 291 * Returns the performance stats for the current game
287 */ 292 */
288 external fun getPerfStats(): DoubleArray 293 external fun getPerfStats(): DoubleArray
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/activities/EmulationActivity.kt
index caf660348..5ca519f0a 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,96 @@ 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 // Best fit
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 if (NativeLibrary.isPaused()) {
275 val playIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_play)
276 val playPendingIntent = PendingIntent.getBroadcast(
277 this@EmulationActivity,
278 R.drawable.ic_pip_play,
279 Intent(actionPlay),
280 pendingFlags
281 )
282 val playRemoteAction = RemoteAction(
283 playIcon,
284 getString(R.string.play),
285 getString(R.string.play),
286 playPendingIntent
287 )
288 pictureInPictureActions.add(playRemoteAction)
289 } else {
290 val pauseIcon = Icon.createWithResource(this@EmulationActivity, R.drawable.ic_pip_pause)
291 val pausePendingIntent = PendingIntent.getBroadcast(
292 this@EmulationActivity,
293 R.drawable.ic_pip_pause,
294 Intent(actionPause),
295 pendingFlags
296 )
297 val pauseRemoteAction = RemoteAction(
298 pauseIcon,
299 getString(R.string.pause),
300 getString(R.string.pause),
301 pausePendingIntent
302 )
303 pictureInPictureActions.add(pauseRemoteAction)
304 }
305
306 return this.apply { setActions(pictureInPictureActions) }
307 }
308
309 fun buildPictureInPictureParams() {
310 val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
311 .getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
312 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
313 pictureInPictureParamsBuilder.setAutoEnterEnabled(BooleanSetting.PICTURE_IN_PICTURE.boolean)
314 }
315 setPictureInPictureParams(pictureInPictureParamsBuilder.build())
316 }
317
318 private var pictureInPictureReceiver = object : BroadcastReceiver() {
319 override fun onReceive(context: Context?, intent: Intent) {
320 if (intent.action == actionPlay) {
321 if (NativeLibrary.isPaused()) NativeLibrary.unPauseEmulation()
322 } else if (intent.action == actionPause) {
323 if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation()
324 }
325 buildPictureInPictureParams()
326 }
327 }
328
329 override fun onPictureInPictureModeChanged(
330 isInPictureInPictureMode: Boolean,
331 newConfig: Configuration
332 ) {
333 super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
334 if (isInPictureInPictureMode) {
335 IntentFilter().apply {
336 addAction(actionPause)
337 addAction(actionPlay)
338 }.also {
339 registerReceiver(pictureInPictureReceiver, it)
340 }
341 } else {
342 try {
343 unregisterReceiver(pictureInPictureReceiver)
344 } catch (ignored : Exception) {
345 }
346 }
347 }
348
233 private fun startMotionSensorListener() { 349 private fun startMotionSensorListener() {
234 val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager 350 val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
235 val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) 351 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..6bcb7bee0 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,10 @@ 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 const val LayoutOption_Unspecified = 0
147 const val LayoutOption_MobilePortrait = 4
148 const val LayoutOption_MobileLandscape = 5
149
147 init { 150 init {
148 configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] = 151 configFileSectionsMap[SettingsFile.FILE_NAME_CONFIG] =
149 listOf( 152 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..da7062b87 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,17 @@ 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(
245 context: Context,
246 launcher: ActivityResultLauncher<Intent>,
247 menuTag: String?,
248 gameId: String?
249 ) {
250 val settings = Intent(context, SettingsActivity::class.java)
251 settings.putExtra(ARG_MENU_TAG, menuTag)
252 settings.putExtra(ARG_GAME_ID, gameId)
253 launcher.launch(settings)
254 }
242 } 255 }
243} 256}
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..4b2305892 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,24 +7,26 @@ 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.Configuration
13import android.graphics.Color 14import android.graphics.Color
14import android.os.Bundle 15import android.os.Bundle
15import android.os.Handler 16import android.os.Handler
16import android.os.Looper 17import android.os.Looper
17import android.util.Rational 18import android.util.Rational
18import android.util.TypedValue
19import android.view.* 19import android.view.*
20import android.widget.TextView 20import android.widget.TextView
21import androidx.activity.OnBackPressedCallback 21import androidx.activity.OnBackPressedCallback
22import androidx.activity.result.ActivityResultLauncher
23import androidx.activity.result.contract.ActivityResultContracts
22import androidx.appcompat.widget.PopupMenu 24import androidx.appcompat.widget.PopupMenu
23import androidx.core.content.res.ResourcesCompat 25import androidx.core.content.res.ResourcesCompat
24import androidx.core.graphics.Insets 26import androidx.core.graphics.Insets
25import androidx.core.view.ViewCompat 27import androidx.core.view.ViewCompat
26import androidx.core.view.WindowInsetsCompat 28import androidx.core.view.WindowInsetsCompat
27import androidx.core.view.updatePadding 29import androidx.core.view.isVisible
28import androidx.fragment.app.Fragment 30import androidx.fragment.app.Fragment
29import androidx.lifecycle.Lifecycle 31import androidx.lifecycle.Lifecycle
30import androidx.lifecycle.lifecycleScope 32import androidx.lifecycle.lifecycleScope
@@ -48,6 +50,7 @@ import org.yuzu.yuzu_emu.features.settings.model.IntSetting
48import org.yuzu.yuzu_emu.features.settings.model.Settings 50import org.yuzu.yuzu_emu.features.settings.model.Settings
49import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity 51import org.yuzu.yuzu_emu.features.settings.ui.SettingsActivity
50import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile 52import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
53import org.yuzu.yuzu_emu.overlay.InputOverlay
51import org.yuzu.yuzu_emu.utils.* 54import org.yuzu.yuzu_emu.utils.*
52 55
53class EmulationFragment : Fragment(), SurfaceHolder.Callback { 56class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@@ -61,11 +64,40 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
61 64
62 val args by navArgs<EmulationFragmentArgs>() 65 val args by navArgs<EmulationFragmentArgs>()
63 66
67 private var isInFoldableLayout = false
68
69 private lateinit var onReturnFromSettings: ActivityResultLauncher<Intent>
70
64 override fun onAttach(context: Context) { 71 override fun onAttach(context: Context) {
65 super.onAttach(context) 72 super.onAttach(context)
66 if (context is EmulationActivity) { 73 if (context is EmulationActivity) {
67 emulationActivity = context 74 emulationActivity = context
68 NativeLibrary.setEmulationActivity(context) 75 NativeLibrary.setEmulationActivity(context)
76
77 lifecycleScope.launch(Dispatchers.Main) {
78 lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
79 WindowInfoTracker.getOrCreate(context)
80 .windowLayoutInfo(context)
81 .collect { updateFoldableLayout(context, it) }
82 }
83 }
84
85 onReturnFromSettings = context.activityResultRegistry.register(
86 "SettingsResult", ActivityResultContracts.StartActivityForResult()
87 ) {
88 binding.surfaceEmulation.setAspectRatio(
89 when (IntSetting.RENDERER_ASPECT_RATIO.int) {
90 0 -> Rational(16, 9)
91 1 -> Rational(4, 3)
92 2 -> Rational(21, 9)
93 3 -> Rational(16, 10)
94 4 -> null // Stretch
95 else -> Rational(16, 9)
96 }
97 )
98 emulationActivity?.buildPictureInPictureParams()
99 updateScreenLayout()
100 }
69 } else { 101 } else {
70 throw IllegalStateException("EmulationFragment must have EmulationActivity parent") 102 throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
71 } 103 }
@@ -129,7 +161,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
129 } 161 }
130 162
131 R.id.menu_settings -> { 163 R.id.menu_settings -> {
132 SettingsActivity.launch(requireContext(), SettingsFile.FILE_NAME_CONFIG, "") 164 SettingsActivity.launch(
165 requireContext(),
166 onReturnFromSettings,
167 SettingsFile.FILE_NAME_CONFIG,
168 ""
169 )
133 true 170 true
134 } 171 }
135 172
@@ -162,7 +199,33 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
162 lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { 199 lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
163 WindowInfoTracker.getOrCreate(requireContext()) 200 WindowInfoTracker.getOrCreate(requireContext())
164 .windowLayoutInfo(requireActivity()) 201 .windowLayoutInfo(requireActivity())
165 .collect { updateCurrentLayout(requireActivity() as EmulationActivity, it) } 202 .collect { updateFoldableLayout(requireActivity() as EmulationActivity, it) }
203 }
204 }
205 }
206
207 override fun onConfigurationChanged(newConfig: Configuration) {
208 super.onConfigurationChanged(newConfig)
209 if (emulationActivity?.isInPictureInPictureMode == true) {
210 if (binding.drawerLayout.isOpen) {
211 binding.drawerLayout.close()
212 }
213 if (EmulationMenuSettings.showOverlay) {
214 binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = false }
215 }
216 } else {
217 if (EmulationMenuSettings.showOverlay) {
218 binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = true }
219 }
220 if (!isInFoldableLayout) {
221 if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
222 binding.surfaceInputOverlay.orientation = InputOverlay.PORTRAIT
223 } else {
224 binding.surfaceInputOverlay.orientation = InputOverlay.LANDSCAPE
225 }
226 }
227 if (!binding.surfaceInputOverlay.isInEditMode) {
228 refreshInputOverlay()
166 } 229 }
167 } 230 }
168 } 231 }
@@ -184,6 +247,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
184 } 247 }
185 ) 248 )
186 249
250 updateScreenLayout()
251
187 emulationState.run(emulationActivity!!.isActivityRecreated) 252 emulationState.run(emulationActivity!!.isActivityRecreated)
188 } 253 }
189 254
@@ -243,31 +308,50 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
243 } 308 }
244 } 309 }
245 310
246 private val Number.toPx get() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics).toInt() 311 @SuppressLint("SourceLockedOrientationActivity")
312 private fun updateScreenLayout() {
313 emulationActivity?.let {
314 it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) {
315 Settings.LayoutOption_MobileLandscape -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
316 Settings.LayoutOption_MobilePortrait -> ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
317 Settings.LayoutOption_Unspecified -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
318 else -> ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
319 }
320 }
321 onConfigurationChanged(resources.configuration)
322 }
247 323
248 fun updateCurrentLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) { 324 private fun updateFoldableLayout(emulationActivity: EmulationActivity, newLayoutInfo: WindowLayoutInfo) {
249 val isFolding = (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let { 325 val isFolding = (newLayoutInfo.displayFeatures.find { it is FoldingFeature } as? FoldingFeature)?.let {
250 if (it.isSeparating) { 326 if (it.isSeparating) {
251 emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED 327 emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
252 if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) { 328 if (it.orientation == FoldingFeature.Orientation.HORIZONTAL) {
253 binding.surfaceEmulation.layoutParams.height = it.bounds.top 329 // Restrict emulation and overlays to the top of the screen
330 binding.emulationContainer.layoutParams.height = it.bounds.top
331 binding.overlayContainer.layoutParams.height = it.bounds.top
332 // Restrict input and menu drawer to the bottom of the screen
333 binding.inputContainer.layoutParams.height = it.bounds.bottom
254 binding.inGameMenu.layoutParams.height = it.bounds.bottom 334 binding.inGameMenu.layoutParams.height = it.bounds.bottom
255 binding.overlayContainer.layoutParams.height = it.bounds.bottom - 48.toPx 335
256 binding.overlayContainer.updatePadding(0, 0, 0, 24.toPx) 336 isInFoldableLayout = true
337 binding.surfaceInputOverlay.orientation = InputOverlay.FOLDABLE
338 refreshInputOverlay()
257 } 339 }
258 } 340 }
259 it.isSeparating 341 it.isSeparating
260 } ?: false 342 } ?: false
261 if (!isFolding) { 343 if (!isFolding) {
262 binding.surfaceEmulation.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT 344 binding.emulationContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
263 binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT 345 binding.inputContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
264 binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT 346 binding.overlayContainer.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
265 binding.overlayContainer.updatePadding(0, 0, 0, 0) 347 binding.inGameMenu.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
266 emulationActivity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE 348 isInFoldableLayout = false
349 updateScreenLayout()
267 } 350 }
268 binding.surfaceInputOverlay.requestLayout() 351 binding.emulationContainer.requestLayout()
269 binding.inGameMenu.requestLayout() 352 binding.inputContainer.requestLayout()
270 binding.overlayContainer.requestLayout() 353 binding.overlayContainer.requestLayout()
354 binding.inGameMenu.requestLayout()
271 } 355 }
272 356
273 override fun surfaceCreated(holder: SurfaceHolder) { 357 override fun surfaceCreated(holder: SurfaceHolder) {
@@ -397,7 +481,19 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
397 popup.show() 481 popup.show()
398 } 482 }
399 483
484 @SuppressLint("SourceLockedOrientationActivity")
400 private fun startConfiguringControls() { 485 private fun startConfiguringControls() {
486 // Lock the current orientation to prevent editing inconsistencies
487 if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
488 emulationActivity?.let {
489 it.requestedOrientation =
490 if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
491 ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
492 } else {
493 ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
494 }
495 }
496 }
401 binding.doneControlConfig.visibility = View.VISIBLE 497 binding.doneControlConfig.visibility = View.VISIBLE
402 binding.surfaceInputOverlay.setIsInEditMode(true) 498 binding.surfaceInputOverlay.setIsInEditMode(true)
403 } 499 }
@@ -405,6 +501,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
405 private fun stopConfiguringControls() { 501 private fun stopConfiguringControls() {
406 binding.doneControlConfig.visibility = View.GONE 502 binding.doneControlConfig.visibility = View.GONE
407 binding.surfaceInputOverlay.setIsInEditMode(false) 503 binding.surfaceInputOverlay.setIsInEditMode(false)
504 // Unlock the orientation if it was locked for editing
505 if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
506 emulationActivity?.let {
507 it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
508 }
509 }
408 } 510 }
409 511
410 @SuppressLint("SetTextI18n") 512 @SuppressLint("SetTextI18n")
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..d12d08e9f 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
@@ -51,12 +51,14 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
51 51
52 private lateinit var windowInsets: WindowInsets 52 private lateinit var windowInsets: WindowInsets
53 53
54 var orientation = LANDSCAPE
55
54 override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { 56 override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
55 super.onLayout(changed, left, top, right, bottom) 57 super.onLayout(changed, left, top, right, bottom)
56 58
57 windowInsets = rootWindowInsets 59 windowInsets = rootWindowInsets
58 60
59 if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) { 61 if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) {
60 defaultOverlay() 62 defaultOverlay()
61 } 63 }
62 64
@@ -233,10 +235,6 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
233 val fingerPositionX = event.getX(pointerIndex).toInt() 235 val fingerPositionX = event.getX(pointerIndex).toInt()
234 val fingerPositionY = event.getY(pointerIndex).toInt() 236 val fingerPositionY = event.getY(pointerIndex).toInt()
235 237
236 // TODO: Provide support for portrait layout
237 //val orientation =
238 // if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else ""
239
240 for (button in overlayButtons) { 238 for (button in overlayButtons) {
241 // Determine the button state to apply based on the MotionEvent action flag. 239 // Determine the button state to apply based on the MotionEvent action flag.
242 when (event.action and MotionEvent.ACTION_MASK) { 240 when (event.action and MotionEvent.ACTION_MASK) {
@@ -266,7 +264,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
266 buttonBeingConfigured!!.buttonId, 264 buttonBeingConfigured!!.buttonId,
267 buttonBeingConfigured!!.bounds.centerX(), 265 buttonBeingConfigured!!.bounds.centerX(),
268 buttonBeingConfigured!!.bounds.centerY(), 266 buttonBeingConfigured!!.bounds.centerY(),
269 "" 267 orientation
270 ) 268 )
271 buttonBeingConfigured = null 269 buttonBeingConfigured = null
272 } 270 }
@@ -299,7 +297,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
299 dpadBeingConfigured!!.upId, 297 dpadBeingConfigured!!.upId,
300 dpadBeingConfigured!!.bounds.centerX(), 298 dpadBeingConfigured!!.bounds.centerX(),
301 dpadBeingConfigured!!.bounds.centerY(), 299 dpadBeingConfigured!!.bounds.centerY(),
302 "" 300 orientation
303 ) 301 )
304 dpadBeingConfigured = null 302 dpadBeingConfigured = null
305 } 303 }
@@ -330,7 +328,7 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
330 joystickBeingConfigured!!.buttonId, 328 joystickBeingConfigured!!.buttonId,
331 joystickBeingConfigured!!.bounds.centerX(), 329 joystickBeingConfigured!!.bounds.centerX(),
332 joystickBeingConfigured!!.bounds.centerY(), 330 joystickBeingConfigured!!.bounds.centerY(),
333 "" 331 orientation
334 ) 332 )
335 joystickBeingConfigured = null 333 joystickBeingConfigured = null
336 } 334 }
@@ -533,8 +531,6 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
533 overlayButtons.clear() 531 overlayButtons.clear()
534 overlayDpads.clear() 532 overlayDpads.clear()
535 overlayJoysticks.clear() 533 overlayJoysticks.clear()
536 val orientation =
537 if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else ""
538 534
539 // Add all the enabled overlay items back to the HashSet. 535 // Add all the enabled overlay items back to the HashSet.
540 if (EmulationMenuSettings.showOverlay) { 536 if (EmulationMenuSettings.showOverlay) {
@@ -548,8 +544,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
548 val min = windowSize.first 544 val min = windowSize.first
549 val max = windowSize.second 545 val max = windowSize.second
550 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit() 546 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
551 .putFloat("$sharedPrefsId$orientation-X", (x - min.x).toFloat() / max.x) 547 .putFloat("$sharedPrefsId-X$orientation", (x - min.x).toFloat() / max.x)
552 .putFloat("$sharedPrefsId$orientation-Y", (y - min.y).toFloat() / max.y) 548 .putFloat("$sharedPrefsId-Y$orientation", (y - min.y).toFloat() / max.y)
553 .apply() 549 .apply()
554 } 550 }
555 551
@@ -558,145 +554,250 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
558 } 554 }
559 555
560 private fun defaultOverlay() { 556 private fun defaultOverlay() {
561 if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) { 557 if (!preferences.getBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", false)) {
562 defaultOverlayLandscape() 558 defaultOverlayByLayout(orientation)
563 } 559 }
564 560
565 resetButtonPlacement() 561 resetButtonPlacement()
566 preferences.edit() 562 preferences.edit()
567 .putBoolean(Settings.PREF_OVERLAY_INIT, true) 563 .putBoolean("${Settings.PREF_OVERLAY_INIT}$orientation", true)
568 .apply() 564 .apply()
569 } 565 }
570 566
571 fun resetButtonPlacement() { 567 fun resetButtonPlacement() {
572 defaultOverlayLandscape() 568 defaultOverlayByLayout(orientation)
573 refreshControls() 569 refreshControls()
574 } 570 }
575 571
576 private fun defaultOverlayLandscape() { 572 private val landscapeResources = arrayOf(
573 R.integer.SWITCH_BUTTON_A_X,
574 R.integer.SWITCH_BUTTON_A_Y,
575 R.integer.SWITCH_BUTTON_B_X,
576 R.integer.SWITCH_BUTTON_B_Y,
577 R.integer.SWITCH_BUTTON_X_X,
578 R.integer.SWITCH_BUTTON_X_Y,
579 R.integer.SWITCH_BUTTON_Y_X,
580 R.integer.SWITCH_BUTTON_Y_Y,
581 R.integer.SWITCH_TRIGGER_ZL_X,
582 R.integer.SWITCH_TRIGGER_ZL_Y,
583 R.integer.SWITCH_TRIGGER_ZR_X,
584 R.integer.SWITCH_TRIGGER_ZR_Y,
585 R.integer.SWITCH_BUTTON_DPAD_X,
586 R.integer.SWITCH_BUTTON_DPAD_Y,
587 R.integer.SWITCH_TRIGGER_L_X,
588 R.integer.SWITCH_TRIGGER_L_Y,
589 R.integer.SWITCH_TRIGGER_R_X,
590 R.integer.SWITCH_TRIGGER_R_Y,
591 R.integer.SWITCH_BUTTON_PLUS_X,
592 R.integer.SWITCH_BUTTON_PLUS_Y,
593 R.integer.SWITCH_BUTTON_MINUS_X,
594 R.integer.SWITCH_BUTTON_MINUS_Y,
595 R.integer.SWITCH_BUTTON_HOME_X,
596 R.integer.SWITCH_BUTTON_HOME_Y,
597 R.integer.SWITCH_BUTTON_CAPTURE_X,
598 R.integer.SWITCH_BUTTON_CAPTURE_Y,
599 R.integer.SWITCH_STICK_R_X,
600 R.integer.SWITCH_STICK_R_Y,
601 R.integer.SWITCH_STICK_L_X,
602 R.integer.SWITCH_STICK_L_Y
603 )
604
605 private val portraitResources = arrayOf(
606 R.integer.SWITCH_BUTTON_A_X_PORTRAIT,
607 R.integer.SWITCH_BUTTON_A_Y_PORTRAIT,
608 R.integer.SWITCH_BUTTON_B_X_PORTRAIT,
609 R.integer.SWITCH_BUTTON_B_Y_PORTRAIT,
610 R.integer.SWITCH_BUTTON_X_X_PORTRAIT,
611 R.integer.SWITCH_BUTTON_X_Y_PORTRAIT,
612 R.integer.SWITCH_BUTTON_Y_X_PORTRAIT,
613 R.integer.SWITCH_BUTTON_Y_Y_PORTRAIT,
614 R.integer.SWITCH_TRIGGER_ZL_X_PORTRAIT,
615 R.integer.SWITCH_TRIGGER_ZL_Y_PORTRAIT,
616 R.integer.SWITCH_TRIGGER_ZR_X_PORTRAIT,
617 R.integer.SWITCH_TRIGGER_ZR_Y_PORTRAIT,
618 R.integer.SWITCH_BUTTON_DPAD_X_PORTRAIT,
619 R.integer.SWITCH_BUTTON_DPAD_Y_PORTRAIT,
620 R.integer.SWITCH_TRIGGER_L_X_PORTRAIT,
621 R.integer.SWITCH_TRIGGER_L_Y_PORTRAIT,
622 R.integer.SWITCH_TRIGGER_R_X_PORTRAIT,
623 R.integer.SWITCH_TRIGGER_R_Y_PORTRAIT,
624 R.integer.SWITCH_BUTTON_PLUS_X_PORTRAIT,
625 R.integer.SWITCH_BUTTON_PLUS_Y_PORTRAIT,
626 R.integer.SWITCH_BUTTON_MINUS_X_PORTRAIT,
627 R.integer.SWITCH_BUTTON_MINUS_Y_PORTRAIT,
628 R.integer.SWITCH_BUTTON_HOME_X_PORTRAIT,
629 R.integer.SWITCH_BUTTON_HOME_Y_PORTRAIT,
630 R.integer.SWITCH_BUTTON_CAPTURE_X_PORTRAIT,
631 R.integer.SWITCH_BUTTON_CAPTURE_Y_PORTRAIT,
632 R.integer.SWITCH_STICK_R_X_PORTRAIT,
633 R.integer.SWITCH_STICK_R_Y_PORTRAIT,
634 R.integer.SWITCH_STICK_L_X_PORTRAIT,
635 R.integer.SWITCH_STICK_L_Y_PORTRAIT
636 )
637
638 private val foldableResources = arrayOf(
639 R.integer.SWITCH_BUTTON_A_X_FOLDABLE,
640 R.integer.SWITCH_BUTTON_A_Y_FOLDABLE,
641 R.integer.SWITCH_BUTTON_B_X_FOLDABLE,
642 R.integer.SWITCH_BUTTON_B_Y_FOLDABLE,
643 R.integer.SWITCH_BUTTON_X_X_FOLDABLE,
644 R.integer.SWITCH_BUTTON_X_Y_FOLDABLE,
645 R.integer.SWITCH_BUTTON_Y_X_FOLDABLE,
646 R.integer.SWITCH_BUTTON_Y_Y_FOLDABLE,
647 R.integer.SWITCH_TRIGGER_ZL_X_FOLDABLE,
648 R.integer.SWITCH_TRIGGER_ZL_Y_FOLDABLE,
649 R.integer.SWITCH_TRIGGER_ZR_X_FOLDABLE,
650 R.integer.SWITCH_TRIGGER_ZR_Y_FOLDABLE,
651 R.integer.SWITCH_BUTTON_DPAD_X_FOLDABLE,
652 R.integer.SWITCH_BUTTON_DPAD_Y_FOLDABLE,
653 R.integer.SWITCH_TRIGGER_L_X_FOLDABLE,
654 R.integer.SWITCH_TRIGGER_L_Y_FOLDABLE,
655 R.integer.SWITCH_TRIGGER_R_X_FOLDABLE,
656 R.integer.SWITCH_TRIGGER_R_Y_FOLDABLE,
657 R.integer.SWITCH_BUTTON_PLUS_X_FOLDABLE,
658 R.integer.SWITCH_BUTTON_PLUS_Y_FOLDABLE,
659 R.integer.SWITCH_BUTTON_MINUS_X_FOLDABLE,
660 R.integer.SWITCH_BUTTON_MINUS_Y_FOLDABLE,
661 R.integer.SWITCH_BUTTON_HOME_X_FOLDABLE,
662 R.integer.SWITCH_BUTTON_HOME_Y_FOLDABLE,
663 R.integer.SWITCH_BUTTON_CAPTURE_X_FOLDABLE,
664 R.integer.SWITCH_BUTTON_CAPTURE_Y_FOLDABLE,
665 R.integer.SWITCH_STICK_R_X_FOLDABLE,
666 R.integer.SWITCH_STICK_R_Y_FOLDABLE,
667 R.integer.SWITCH_STICK_L_X_FOLDABLE,
668 R.integer.SWITCH_STICK_L_Y_FOLDABLE
669 )
670
671 private fun getResourceValue(orientation: String, position: Int) : Float {
672 return when (orientation) {
673 PORTRAIT -> resources.getInteger(portraitResources[position]).toFloat() / 1000
674 FOLDABLE -> resources.getInteger(foldableResources[position]).toFloat() / 1000
675 else -> resources.getInteger(landscapeResources[position]).toFloat() / 1000
676 }
677 }
678
679 private fun defaultOverlayByLayout(orientation: String) {
577 // Each value represents the position of the button in relation to the screen size without insets. 680 // Each value represents the position of the button in relation to the screen size without insets.
578 preferences.edit() 681 preferences.edit()
579 .putFloat( 682 .putFloat(
580 ButtonType.BUTTON_A.toString() + "-X", 683 ButtonType.BUTTON_A.toString() + "-X$orientation",
581 resources.getInteger(R.integer.SWITCH_BUTTON_A_X).toFloat() / 1000 684 getResourceValue(orientation, 0)
582 ) 685 )
583 .putFloat( 686 .putFloat(
584 ButtonType.BUTTON_A.toString() + "-Y", 687 ButtonType.BUTTON_A.toString() + "-Y$orientation",
585 resources.getInteger(R.integer.SWITCH_BUTTON_A_Y).toFloat() / 1000 688 getResourceValue(orientation, 1)
586 ) 689 )
587 .putFloat( 690 .putFloat(
588 ButtonType.BUTTON_B.toString() + "-X", 691 ButtonType.BUTTON_B.toString() + "-X$orientation",
589 resources.getInteger(R.integer.SWITCH_BUTTON_B_X).toFloat() / 1000 692 getResourceValue(orientation, 2)
590 ) 693 )
591 .putFloat( 694 .putFloat(
592 ButtonType.BUTTON_B.toString() + "-Y", 695 ButtonType.BUTTON_B.toString() + "-Y$orientation",
593 resources.getInteger(R.integer.SWITCH_BUTTON_B_Y).toFloat() / 1000 696 getResourceValue(orientation, 3)
594 ) 697 )
595 .putFloat( 698 .putFloat(
596 ButtonType.BUTTON_X.toString() + "-X", 699 ButtonType.BUTTON_X.toString() + "-X$orientation",
597 resources.getInteger(R.integer.SWITCH_BUTTON_X_X).toFloat() / 1000 700 getResourceValue(orientation, 4)
598 ) 701 )
599 .putFloat( 702 .putFloat(
600 ButtonType.BUTTON_X.toString() + "-Y", 703 ButtonType.BUTTON_X.toString() + "-Y$orientation",
601 resources.getInteger(R.integer.SWITCH_BUTTON_X_Y).toFloat() / 1000 704 getResourceValue(orientation, 5)
602 ) 705 )
603 .putFloat( 706 .putFloat(
604 ButtonType.BUTTON_Y.toString() + "-X", 707 ButtonType.BUTTON_Y.toString() + "-X$orientation",
605 resources.getInteger(R.integer.SWITCH_BUTTON_Y_X).toFloat() / 1000 708 getResourceValue(orientation, 6)
606 ) 709 )
607 .putFloat( 710 .putFloat(
608 ButtonType.BUTTON_Y.toString() + "-Y", 711 ButtonType.BUTTON_Y.toString() + "-Y$orientation",
609 resources.getInteger(R.integer.SWITCH_BUTTON_Y_Y).toFloat() / 1000 712 getResourceValue(orientation, 7)
610 ) 713 )
611 .putFloat( 714 .putFloat(
612 ButtonType.TRIGGER_ZL.toString() + "-X", 715 ButtonType.TRIGGER_ZL.toString() + "-X$orientation",
613 resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_X).toFloat() / 1000 716 getResourceValue(orientation, 8)
614 ) 717 )
615 .putFloat( 718 .putFloat(
616 ButtonType.TRIGGER_ZL.toString() + "-Y", 719 ButtonType.TRIGGER_ZL.toString() + "-Y$orientation",
617 resources.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y).toFloat() / 1000 720 getResourceValue(orientation, 9)
618 ) 721 )
619 .putFloat( 722 .putFloat(
620 ButtonType.TRIGGER_ZR.toString() + "-X", 723 ButtonType.TRIGGER_ZR.toString() + "-X$orientation",
621 resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_X).toFloat() / 1000 724 getResourceValue(orientation, 10)
622 ) 725 )
623 .putFloat( 726 .putFloat(
624 ButtonType.TRIGGER_ZR.toString() + "-Y", 727 ButtonType.TRIGGER_ZR.toString() + "-Y$orientation",
625 resources.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y).toFloat() / 1000 728 getResourceValue(orientation, 11)
626 ) 729 )
627 .putFloat( 730 .putFloat(
628 ButtonType.DPAD_UP.toString() + "-X", 731 ButtonType.DPAD_UP.toString() + "-X$orientation",
629 resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_X).toFloat() / 1000 732 getResourceValue(orientation, 12)
630 ) 733 )
631 .putFloat( 734 .putFloat(
632 ButtonType.DPAD_UP.toString() + "-Y", 735 ButtonType.DPAD_UP.toString() + "-Y$orientation",
633 resources.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y).toFloat() / 1000 736 getResourceValue(orientation, 13)
634 ) 737 )
635 .putFloat( 738 .putFloat(
636 ButtonType.TRIGGER_L.toString() + "-X", 739 ButtonType.TRIGGER_L.toString() + "-X$orientation",
637 resources.getInteger(R.integer.SWITCH_TRIGGER_L_X).toFloat() / 1000 740 getResourceValue(orientation, 14)
638 ) 741 )
639 .putFloat( 742 .putFloat(
640 ButtonType.TRIGGER_L.toString() + "-Y", 743 ButtonType.TRIGGER_L.toString() + "-Y$orientation",
641 resources.getInteger(R.integer.SWITCH_TRIGGER_L_Y).toFloat() / 1000 744 getResourceValue(orientation, 15)
642 ) 745 )
643 .putFloat( 746 .putFloat(
644 ButtonType.TRIGGER_R.toString() + "-X", 747 ButtonType.TRIGGER_R.toString() + "-X$orientation",
645 resources.getInteger(R.integer.SWITCH_TRIGGER_R_X).toFloat() / 1000 748 getResourceValue(orientation, 16)
646 ) 749 )
647 .putFloat( 750 .putFloat(
648 ButtonType.TRIGGER_R.toString() + "-Y", 751 ButtonType.TRIGGER_R.toString() + "-Y$orientation",
649 resources.getInteger(R.integer.SWITCH_TRIGGER_R_Y).toFloat() / 1000 752 getResourceValue(orientation, 17)
650 ) 753 )
651 .putFloat( 754 .putFloat(
652 ButtonType.BUTTON_PLUS.toString() + "-X", 755 ButtonType.BUTTON_PLUS.toString() + "-X$orientation",
653 resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_X).toFloat() / 1000 756 getResourceValue(orientation, 18)
654 ) 757 )
655 .putFloat( 758 .putFloat(
656 ButtonType.BUTTON_PLUS.toString() + "-Y", 759 ButtonType.BUTTON_PLUS.toString() + "-Y$orientation",
657 resources.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y).toFloat() / 1000 760 getResourceValue(orientation, 19)
658 ) 761 )
659 .putFloat( 762 .putFloat(
660 ButtonType.BUTTON_MINUS.toString() + "-X", 763 ButtonType.BUTTON_MINUS.toString() + "-X$orientation",
661 resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_X).toFloat() / 1000 764 getResourceValue(orientation, 20)
662 ) 765 )
663 .putFloat( 766 .putFloat(
664 ButtonType.BUTTON_MINUS.toString() + "-Y", 767 ButtonType.BUTTON_MINUS.toString() + "-Y$orientation",
665 resources.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y).toFloat() / 1000 768 getResourceValue(orientation, 21)
666 ) 769 )
667 .putFloat( 770 .putFloat(
668 ButtonType.BUTTON_HOME.toString() + "-X", 771 ButtonType.BUTTON_HOME.toString() + "-X$orientation",
669 resources.getInteger(R.integer.SWITCH_BUTTON_HOME_X).toFloat() / 1000 772 getResourceValue(orientation, 22)
670 ) 773 )
671 .putFloat( 774 .putFloat(
672 ButtonType.BUTTON_HOME.toString() + "-Y", 775 ButtonType.BUTTON_HOME.toString() + "-Y$orientation",
673 resources.getInteger(R.integer.SWITCH_BUTTON_HOME_Y).toFloat() / 1000 776 getResourceValue(orientation, 23)
674 ) 777 )
675 .putFloat( 778 .putFloat(
676 ButtonType.BUTTON_CAPTURE.toString() + "-X", 779 ButtonType.BUTTON_CAPTURE.toString() + "-X$orientation",
677 resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X) 780 getResourceValue(orientation, 24)
678 .toFloat() / 1000
679 ) 781 )
680 .putFloat( 782 .putFloat(
681 ButtonType.BUTTON_CAPTURE.toString() + "-Y", 783 ButtonType.BUTTON_CAPTURE.toString() + "-Y$orientation",
682 resources.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y) 784 getResourceValue(orientation, 25)
683 .toFloat() / 1000
684 ) 785 )
685 .putFloat( 786 .putFloat(
686 ButtonType.STICK_R.toString() + "-X", 787 ButtonType.STICK_R.toString() + "-X$orientation",
687 resources.getInteger(R.integer.SWITCH_STICK_R_X).toFloat() / 1000 788 getResourceValue(orientation, 26)
688 ) 789 )
689 .putFloat( 790 .putFloat(
690 ButtonType.STICK_R.toString() + "-Y", 791 ButtonType.STICK_R.toString() + "-Y$orientation",
691 resources.getInteger(R.integer.SWITCH_STICK_R_Y).toFloat() / 1000 792 getResourceValue(orientation, 27)
692 ) 793 )
693 .putFloat( 794 .putFloat(
694 ButtonType.STICK_L.toString() + "-X", 795 ButtonType.STICK_L.toString() + "-X$orientation",
695 resources.getInteger(R.integer.SWITCH_STICK_L_X).toFloat() / 1000 796 getResourceValue(orientation, 28)
696 ) 797 )
697 .putFloat( 798 .putFloat(
698 ButtonType.STICK_L.toString() + "-Y", 799 ButtonType.STICK_L.toString() + "-Y$orientation",
699 resources.getInteger(R.integer.SWITCH_STICK_L_Y).toFloat() / 1000 800 getResourceValue(orientation, 29)
700 ) 801 )
701 .apply() 802 .apply()
702 } 803 }
@@ -709,6 +810,10 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
709 private val preferences: SharedPreferences = 810 private val preferences: SharedPreferences =
710 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext) 811 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
711 812
813 const val LANDSCAPE = ""
814 const val PORTRAIT = "_Portrait"
815 const val FOLDABLE = "_Foldable"
816
712 /** 817 /**
713 * Resizes a [Bitmap] by a given scale factor 818 * Resizes a [Bitmap] by a given scale factor
714 * 819 *
@@ -754,9 +859,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
754 */ 859 */
755 private fun getSafeScreenSize(context: Context): Pair<Point, Point> { 860 private fun getSafeScreenSize(context: Context): Pair<Point, Point> {
756 // Get screen size 861 // Get screen size
757 val windowMetrics = 862 val windowMetrics = WindowMetricsCalculator.getOrCreate()
758 WindowMetricsCalculator.getOrCreate() 863 .computeCurrentWindowMetrics(context as Activity)
759 .computeCurrentWindowMetrics(context as Activity)
760 var maxY = windowMetrics.bounds.height().toFloat() 864 var maxY = windowMetrics.bounds.height().toFloat()
761 var maxX = windowMetrics.bounds.width().toFloat() 865 var maxX = windowMetrics.bounds.width().toFloat()
762 var minY = 0 866 var minY = 0
@@ -769,9 +873,9 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
769 val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout 873 val insets = context.windowManager.currentWindowMetrics.windowInsets.displayCutout
770 if (insets != null) { 874 if (insets != null) {
771 if (insets.boundingRectTop.bottom != 0 && insets.boundingRectTop.bottom > maxY / 2) 875 if (insets.boundingRectTop.bottom != 0 && insets.boundingRectTop.bottom > maxY / 2)
772 insets.boundingRectTop.bottom.toFloat() else maxY 876 maxY = insets.boundingRectTop.bottom.toFloat()
773 if (insets.boundingRectRight.left != 0 && insets.boundingRectRight.left > maxX / 2) 877 if (insets.boundingRectRight.left != 0 && insets.boundingRectRight.left > maxX / 2)
774 insets.boundingRectRight.left.toFloat() else maxX 878 maxX = insets.boundingRectRight.left.toFloat()
775 879
776 minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left 880 minX = insets.boundingRectLeft.right - insets.boundingRectLeft.left
777 minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom 881 minY = insets.boundingRectBottom.top - insets.boundingRectBottom.bottom
@@ -878,8 +982,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
878 982
879 // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. 983 // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
880 // These were set in the input overlay configuration menu. 984 // These were set in the input overlay configuration menu.
881 val xKey = "$buttonId$orientation-X" 985 val xKey = "$buttonId-X$orientation"
882 val yKey = "$buttonId$orientation-Y" 986 val yKey = "$buttonId-Y$orientation"
883 val drawableXPercent = sPrefs.getFloat(xKey, 0f) 987 val drawableXPercent = sPrefs.getFloat(xKey, 0f)
884 val drawableYPercent = sPrefs.getFloat(yKey, 0f) 988 val drawableYPercent = sPrefs.getFloat(yKey, 0f)
885 val drawableX = (drawableXPercent * max.x + min.x).toInt() 989 val drawableX = (drawableXPercent * max.x + min.x).toInt()
@@ -959,8 +1063,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
959 1063
960 // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay. 1064 // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
961 // These were set in the input overlay configuration menu. 1065 // These were set in the input overlay configuration menu.
962 val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-X", 0f) 1066 val drawableXPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}-X$orientation", 0f)
963 val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-Y", 0f) 1067 val drawableYPercent = sPrefs.getFloat("${ButtonType.DPAD_UP}-Y$orientation", 0f)
964 val drawableX = (drawableXPercent * max.x + min.x).toInt() 1068 val drawableX = (drawableXPercent * max.x + min.x).toInt()
965 val drawableY = (drawableYPercent * max.y + min.y).toInt() 1069 val drawableY = (drawableYPercent * max.y + min.y).toInt()
966 val width = overlayDrawable.width 1070 val width = overlayDrawable.width
@@ -1026,8 +1130,8 @@ class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context
1026 1130
1027 // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay. 1131 // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
1028 // These were set in the input overlay configuration menu. 1132 // These were set in the input overlay configuration menu.
1029 val drawableXPercent = sPrefs.getFloat("$button$orientation-X", 0f) 1133 val drawableXPercent = sPrefs.getFloat("$button-X$orientation", 0f)
1030 val drawableYPercent = sPrefs.getFloat("$button$orientation-Y", 0f) 1134 val drawableYPercent = sPrefs.getFloat("$button-Y$orientation", 0f)
1031 val drawableX = (drawableXPercent * max.x + min.x).toInt() 1135 val drawableX = (drawableXPercent * max.x + min.x).toInt()
1032 val drawableY = (drawableYPercent * max.y + min.y).toInt() 1136 val drawableY = (drawableYPercent * max.y + min.y).toInt()
1033 val outerScale = 1.66f 1137 val outerScale = 1.66f
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/jni/native.cpp b/src/android/app/src/main/jni/native.cpp
index 4091c23d1..f9617202b 100644
--- a/src/android/app/src/main/jni/native.cpp
+++ b/src/android/app/src/main/jni/native.cpp
@@ -202,6 +202,11 @@ public:
202 return m_is_running; 202 return m_is_running;
203 } 203 }
204 204
205 bool IsPaused() const {
206 std::scoped_lock lock(m_mutex);
207 return m_is_running && m_is_paused;
208 }
209
205 const Core::PerfStatsResults& PerfStats() const { 210 const Core::PerfStatsResults& PerfStats() const {
206 std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex); 211 std::scoped_lock m_perf_stats_lock(m_perf_stats_mutex);
207 return m_perf_stats; 212 return m_perf_stats;
@@ -287,11 +292,13 @@ public:
287 void PauseEmulation() { 292 void PauseEmulation() {
288 std::scoped_lock lock(m_mutex); 293 std::scoped_lock lock(m_mutex);
289 m_system.Pause(); 294 m_system.Pause();
295 m_is_paused = true;
290 } 296 }
291 297
292 void UnPauseEmulation() { 298 void UnPauseEmulation() {
293 std::scoped_lock lock(m_mutex); 299 std::scoped_lock lock(m_mutex);
294 m_system.Run(); 300 m_system.Run();
301 m_is_paused = false;
295 } 302 }
296 303
297 void HaltEmulation() { 304 void HaltEmulation() {
@@ -473,6 +480,7 @@ private:
473 std::shared_ptr<FileSys::VfsFilesystem> m_vfs; 480 std::shared_ptr<FileSys::VfsFilesystem> m_vfs;
474 Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized}; 481 Core::SystemResultStatus m_load_result{Core::SystemResultStatus::ErrorNotInitialized};
475 bool m_is_running{}; 482 bool m_is_running{};
483 bool m_is_paused{};
476 SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{}; 484 SoftwareKeyboard::AndroidKeyboard* m_software_keyboard{};
477 std::unique_ptr<Service::Account::ProfileManager> m_profile_manager; 485 std::unique_ptr<Service::Account::ProfileManager> m_profile_manager;
478 486
@@ -583,6 +591,11 @@ jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isRunning([[maybe_unused]] JNIEnv
583 return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning()); 591 return static_cast<jboolean>(EmulationSession::GetInstance().IsRunning());
584} 592}
585 593
594jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isPaused([[maybe_unused]] JNIEnv* env,
595 [[maybe_unused]] jclass clazz) {
596 return static_cast<jboolean>(EmulationSession::GetInstance().IsPaused());
597}
598
586jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly([[maybe_unused]] JNIEnv* env, 599jboolean Java_org_yuzu_yuzu_1emu_NativeLibrary_isHandheldOnly([[maybe_unused]] JNIEnv* env,
587 [[maybe_unused]] jclass clazz) { 600 [[maybe_unused]] jclass clazz) {
588 return EmulationSession::GetInstance().IsHandheldOnly(); 601 return EmulationSession::GetInstance().IsHandheldOnly();
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..e54a10e8f 100644
--- a/src/android/app/src/main/res/layout/fragment_emulation.xml
+++ b/src/android/app/src/main/res/layout/fragment_emulation.xml
@@ -12,49 +12,65 @@
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/input_container"
26 android:layout_width="match_parent" 33 android:layout_width="match_parent"
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"
40 android:layout_width="match_parent"
41 android:layout_height="match_parent"
42 android:layout_gravity="center"
43 android:focusable="true"
44 android:focusableInTouchMode="true" />
45
46 <Button
47 style="@style/Widget.Material3.Button.ElevatedButton"
48 android:id="@+id/done_control_config"
49 android:layout_width="wrap_content"
50 android:layout_height="wrap_content"
51 android:layout_gravity="center"
52 android:text="@string/emulation_done"
53 android:visibility="gone" />
54
55 </FrameLayout>
56
57 <FrameLayout
58 android:id="@+id/overlay_container"
33 android:layout_width="match_parent" 59 android:layout_width="match_parent"
34 android:layout_height="match_parent" 60 android:layout_height="match_parent">
35 android:focusable="true"
36 android:focusableInTouchMode="true" />
37 61
38 <TextView 62 <TextView
39 android:id="@+id/show_fps_text" 63 android:id="@+id/show_fps_text"
40 android:layout_width="wrap_content" 64 android:layout_width="wrap_content"
41 android:layout_height="wrap_content" 65 android:layout_height="wrap_content"
42 android:layout_gravity="left" 66 android:layout_gravity="left"
43 android:clickable="false" 67 android:clickable="false"
44 android:focusable="false" 68 android:focusable="false"
45 android:shadowColor="@android:color/black" 69 android:shadowColor="@android:color/black"
46 android:textColor="@android:color/white" 70 android:textColor="@android:color/white"
47 android:textSize="12sp" 71 android:textSize="12sp"
48 tools:ignore="RtlHardcoded" /> 72 tools:ignore="RtlHardcoded" />
49 73
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> 74 </FrameLayout>
59 75
60 </androidx.coordinatorlayout.widget.CoordinatorLayout> 76 </androidx.coordinatorlayout.widget.CoordinatorLayout>
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/integers.xml b/src/android/app/src/main/res/values/integers.xml
index bc614b81d..2e93b408c 100644
--- a/src/android/app/src/main/res/values/integers.xml
+++ b/src/android/app/src/main/res/values/integers.xml
@@ -34,4 +34,68 @@
34 <integer name="SWITCH_BUTTON_DPAD_X">260</integer> 34 <integer name="SWITCH_BUTTON_DPAD_X">260</integer>
35 <integer name="SWITCH_BUTTON_DPAD_Y">790</integer> 35 <integer name="SWITCH_BUTTON_DPAD_Y">790</integer>
36 36
37 <!-- Default SWITCH portrait layout -->
38 <integer name="SWITCH_BUTTON_A_X_PORTRAIT">840</integer>
39 <integer name="SWITCH_BUTTON_A_Y_PORTRAIT">840</integer>
40 <integer name="SWITCH_BUTTON_B_X_PORTRAIT">740</integer>
41 <integer name="SWITCH_BUTTON_B_Y_PORTRAIT">880</integer>
42 <integer name="SWITCH_BUTTON_X_X_PORTRAIT">740</integer>
43 <integer name="SWITCH_BUTTON_X_Y_PORTRAIT">800</integer>
44 <integer name="SWITCH_BUTTON_Y_X_PORTRAIT">640</integer>
45 <integer name="SWITCH_BUTTON_Y_Y_PORTRAIT">840</integer>
46 <integer name="SWITCH_STICK_L_X_PORTRAIT">180</integer>
47 <integer name="SWITCH_STICK_L_Y_PORTRAIT">660</integer>
48 <integer name="SWITCH_STICK_R_X_PORTRAIT">820</integer>
49 <integer name="SWITCH_STICK_R_Y_PORTRAIT">660</integer>
50 <integer name="SWITCH_TRIGGER_L_X_PORTRAIT">140</integer>
51 <integer name="SWITCH_TRIGGER_L_Y_PORTRAIT">260</integer>
52 <integer name="SWITCH_TRIGGER_R_X_PORTRAIT">860</integer>
53 <integer name="SWITCH_TRIGGER_R_Y_PORTRAIT">260</integer>
54 <integer name="SWITCH_TRIGGER_ZL_X_PORTRAIT">140</integer>
55 <integer name="SWITCH_TRIGGER_ZL_Y_PORTRAIT">200</integer>
56 <integer name="SWITCH_TRIGGER_ZR_X_PORTRAIT">860</integer>
57 <integer name="SWITCH_TRIGGER_ZR_Y_PORTRAIT">200</integer>
58 <integer name="SWITCH_BUTTON_MINUS_X_PORTRAIT">440</integer>
59 <integer name="SWITCH_BUTTON_MINUS_Y_PORTRAIT">950</integer>
60 <integer name="SWITCH_BUTTON_PLUS_X_PORTRAIT">560</integer>
61 <integer name="SWITCH_BUTTON_PLUS_Y_PORTRAIT">950</integer>
62 <integer name="SWITCH_BUTTON_HOME_X_PORTRAIT">680</integer>
63 <integer name="SWITCH_BUTTON_HOME_Y_PORTRAIT">950</integer>
64 <integer name="SWITCH_BUTTON_CAPTURE_X_PORTRAIT">320</integer>
65 <integer name="SWITCH_BUTTON_CAPTURE_Y_PORTRAIT">950</integer>
66 <integer name="SWITCH_BUTTON_DPAD_X_PORTRAIT">240</integer>
67 <integer name="SWITCH_BUTTON_DPAD_Y_PORTRAIT">840</integer>
68
69 <!-- Default SWITCH foldable layout -->
70 <integer name="SWITCH_BUTTON_A_X_FOLDABLE">840</integer>
71 <integer name="SWITCH_BUTTON_A_Y_FOLDABLE">390</integer>
72 <integer name="SWITCH_BUTTON_B_X_FOLDABLE">740</integer>
73 <integer name="SWITCH_BUTTON_B_Y_FOLDABLE">430</integer>
74 <integer name="SWITCH_BUTTON_X_X_FOLDABLE">740</integer>
75 <integer name="SWITCH_BUTTON_X_Y_FOLDABLE">350</integer>
76 <integer name="SWITCH_BUTTON_Y_X_FOLDABLE">640</integer>
77 <integer name="SWITCH_BUTTON_Y_Y_FOLDABLE">390</integer>
78 <integer name="SWITCH_STICK_L_X_FOLDABLE">180</integer>
79 <integer name="SWITCH_STICK_L_Y_FOLDABLE">250</integer>
80 <integer name="SWITCH_STICK_R_X_FOLDABLE">820</integer>
81 <integer name="SWITCH_STICK_R_Y_FOLDABLE">250</integer>
82 <integer name="SWITCH_TRIGGER_L_X_FOLDABLE">140</integer>
83 <integer name="SWITCH_TRIGGER_L_Y_FOLDABLE">130</integer>
84 <integer name="SWITCH_TRIGGER_R_X_FOLDABLE">860</integer>
85 <integer name="SWITCH_TRIGGER_R_Y_FOLDABLE">130</integer>
86 <integer name="SWITCH_TRIGGER_ZL_X_FOLDABLE">140</integer>
87 <integer name="SWITCH_TRIGGER_ZL_Y_FOLDABLE">70</integer>
88 <integer name="SWITCH_TRIGGER_ZR_X_FOLDABLE">860</integer>
89 <integer name="SWITCH_TRIGGER_ZR_Y_FOLDABLE">70</integer>
90 <integer name="SWITCH_BUTTON_MINUS_X_FOLDABLE">440</integer>
91 <integer name="SWITCH_BUTTON_MINUS_Y_FOLDABLE">470</integer>
92 <integer name="SWITCH_BUTTON_PLUS_X_FOLDABLE">560</integer>
93 <integer name="SWITCH_BUTTON_PLUS_Y_FOLDABLE">470</integer>
94 <integer name="SWITCH_BUTTON_HOME_X_FOLDABLE">680</integer>
95 <integer name="SWITCH_BUTTON_HOME_Y_FOLDABLE">470</integer>
96 <integer name="SWITCH_BUTTON_CAPTURE_X_FOLDABLE">320</integer>
97 <integer name="SWITCH_BUTTON_CAPTURE_Y_FOLDABLE">470</integer>
98 <integer name="SWITCH_BUTTON_DPAD_X_FOLDABLE">240</integer>
99 <integer name="SWITCH_BUTTON_DPAD_Y_FOLDABLE">390</integer>
100
37</resources> 101</resources>
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>