android: Add Picture in Picture mode
This commit is contained in:
parent
dc2a0b2e50
commit
fa58cbe0d2
4 changed files with 106 additions and 0 deletions
|
@ -60,6 +60,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
android:theme="@style/Theme.Yuzu.Main"
|
android:theme="@style/Theme.Yuzu.Main"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:screenOrientation="userLandscape"
|
android:screenOrientation="userLandscape"
|
||||||
|
android:supportsPictureInPicture="true"
|
||||||
|
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
|
|
@ -4,15 +4,22 @@
|
||||||
package org.yuzu.yuzu_emu.activities
|
package org.yuzu.yuzu_emu.activities
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.app.PictureInPictureParams
|
||||||
|
import android.app.RemoteAction
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
|
import android.graphics.drawable.Icon
|
||||||
import android.hardware.Sensor
|
import android.hardware.Sensor
|
||||||
import android.hardware.SensorEvent
|
import android.hardware.SensorEvent
|
||||||
import android.hardware.SensorEventListener
|
import android.hardware.SensorEventListener
|
||||||
import android.hardware.SensorManager
|
import android.hardware.SensorManager
|
||||||
import android.hardware.display.DisplayManager
|
import android.hardware.display.DisplayManager
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Display
|
import android.view.Display
|
||||||
import android.view.InputDevice
|
import android.view.InputDevice
|
||||||
|
@ -60,6 +67,10 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||||
private var motionTimestamp: Long = 0
|
private var motionTimestamp: Long = 0
|
||||||
private var flipMotionOrientation: Boolean = false
|
private var flipMotionOrientation: Boolean = false
|
||||||
|
|
||||||
|
private val actionPause = "ACTION_EMULATOR_PAUSE"
|
||||||
|
private val actionPlay = "ACTION_EMULATOR_PLAY"
|
||||||
|
private lateinit var pictureInPictureParamsBuilder : PictureInPictureParams.Builder
|
||||||
|
|
||||||
private lateinit var game: Game
|
private lateinit var game: Game
|
||||||
|
|
||||||
private val settingsViewModel: SettingsViewModel by viewModels()
|
private val settingsViewModel: SettingsViewModel by viewModels()
|
||||||
|
@ -88,6 +99,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||||
// Set these options now so that the SurfaceView the game renders into is the right size.
|
// Set these options now so that the SurfaceView the game renders into is the right size.
|
||||||
enableFullscreenImmersive()
|
enableFullscreenImmersive()
|
||||||
|
|
||||||
|
pictureInPictureParamsBuilder = getPictureInPictureBuilder()
|
||||||
|
setPictureInPictureParams(pictureInPictureParamsBuilder.build())
|
||||||
|
|
||||||
setContentView(R.layout.activity_emulation)
|
setContentView(R.layout.activity_emulation)
|
||||||
window.decorView.setBackgroundColor(getColor(android.R.color.black))
|
window.decorView.setBackgroundColor(getColor(android.R.color.black))
|
||||||
|
|
||||||
|
@ -161,6 +175,12 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||||
stopMotionSensorListener()
|
stopMotionSensorListener()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onUserLeaveHint() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && !isInPictureInPictureMode) {
|
||||||
|
enterPictureInPictureMode(pictureInPictureParamsBuilder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent) {
|
override fun onNewIntent(intent: Intent) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
setIntent(intent)
|
setIntent(intent)
|
||||||
|
@ -289,6 +309,61 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getPictureInPictureBuilder() : PictureInPictureParams.Builder {
|
||||||
|
val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
|
||||||
|
|
||||||
|
val pictureInPictureActions : MutableList<RemoteAction> = mutableListOf()
|
||||||
|
val pendingFlags = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
|
||||||
|
val pauseIcon = Icon.createWithResource(this, R.drawable.ic_pause)
|
||||||
|
val pausePendingIntent = PendingIntent.getBroadcast(this, R.drawable.ic_pause, Intent(actionPause), pendingFlags)
|
||||||
|
val pauseRemoteAction = RemoteAction(pauseIcon, getString(R.string.pause), getString(R.string.pause), pausePendingIntent)
|
||||||
|
pictureInPictureActions.add(pauseRemoteAction)
|
||||||
|
|
||||||
|
val playIcon = Icon.createWithResource(this, R.drawable.ic_play)
|
||||||
|
val playPendingIntent = PendingIntent.getBroadcast(this, R.drawable.ic_play, Intent(actionPlay), pendingFlags)
|
||||||
|
val playRemoteAction = RemoteAction(playIcon, getString(R.string.play), getString(R.string.play), playPendingIntent)
|
||||||
|
pictureInPictureActions.add(playRemoteAction)
|
||||||
|
|
||||||
|
pictureInPictureParamsBuilder.setActions(pictureInPictureActions)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
pictureInPictureParamsBuilder.setAutoEnterEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
setPictureInPictureParams(pictureInPictureParamsBuilder.build())
|
||||||
|
|
||||||
|
return pictureInPictureParamsBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
private var pictureInPictureReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context : Context?, intent : Intent) {
|
||||||
|
if (intent.action == actionPause) {
|
||||||
|
emulationFragment?.onPictureInPicturePause()
|
||||||
|
}
|
||||||
|
if (intent.action == actionPlay) {
|
||||||
|
emulationFragment?.onPictureInPicturePlay()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) {
|
||||||
|
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
|
||||||
|
if (isInPictureInPictureMode) {
|
||||||
|
IntentFilter().apply {
|
||||||
|
addAction(actionPause)
|
||||||
|
addAction(actionPlay)
|
||||||
|
}.also {
|
||||||
|
registerReceiver(pictureInPictureReceiver, it)
|
||||||
|
}
|
||||||
|
emulationFragment?.onPictureInPictureEnter()
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
unregisterReceiver(pictureInPictureReceiver)
|
||||||
|
} catch (ignored : Exception) { }
|
||||||
|
emulationFragment?.onPictureInPictureLeave()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun startMotionSensorListener() {
|
private fun startMotionSensorListener() {
|
||||||
val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||||
val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
|
val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
|
||||||
|
|
|
@ -23,6 +23,7 @@ import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.graphics.Insets
|
import androidx.core.graphics.Insets
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
@ -178,6 +179,30 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
|
||||||
super.onDetach()
|
super.onDetach()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onPictureInPictureEnter() {
|
||||||
|
if (EmulationMenuSettings.showOverlay) {
|
||||||
|
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPictureInPicturePause() {
|
||||||
|
if (!emulationState.isPaused) {
|
||||||
|
emulationState.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPictureInPicturePlay() {
|
||||||
|
if (emulationState.isPaused) {
|
||||||
|
emulationState.run(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onPictureInPictureLeave() {
|
||||||
|
if (EmulationMenuSettings.showOverlay) {
|
||||||
|
binding.surfaceInputOverlay.post { binding.surfaceInputOverlay.isVisible = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun refreshInputOverlay() {
|
private fun refreshInputOverlay() {
|
||||||
binding.surfaceInputOverlay.refreshControls()
|
binding.surfaceInputOverlay.refreshControls()
|
||||||
}
|
}
|
||||||
|
|
|
@ -356,6 +356,10 @@
|
||||||
<string name="use_black_backgrounds">Black backgrounds</string>
|
<string name="use_black_backgrounds">Black backgrounds</string>
|
||||||
<string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string>
|
<string name="use_black_backgrounds_description">When using the dark theme, apply black backgrounds.</string>
|
||||||
|
|
||||||
|
<!-- Picture-In-Picture -->
|
||||||
|
<string name="pause">Pause</string>
|
||||||
|
<string name="play">Play</string>
|
||||||
|
|
||||||
<!-- Licenses screen strings -->
|
<!-- Licenses screen strings -->
|
||||||
<string name="licenses">Licenses</string>
|
<string name="licenses">Licenses</string>
|
||||||
<string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string>
|
<string name="license_fidelityfx_fsr" translatable="false">FidelityFX-FSR</string>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue