android: Add Picture in Picture mode

This commit is contained in:
Abandoned Cart 2023-06-02 05:28:57 -04:00
commit fa58cbe0d2
4 changed files with 106 additions and 0 deletions

View file

@ -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>

View file

@ -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)

View file

@ -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()
} }

View file

@ -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>