diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml index 43087f2c01..bfd93bbc21 100644 --- a/src/android/app/src/main/AndroidManifest.xml +++ b/src/android/app/src/main/AndroidManifest.xml @@ -60,6 +60,8 @@ SPDX-License-Identifier: GPL-3.0-or-later android:theme="@style/Theme.Yuzu.Main" android:launchMode="singleTop" android:screenOrientation="userLandscape" + android:supportsPictureInPicture="true" + android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode" android:exported="true"> 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 94d5156cfe..7cbb5427ac 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,15 +4,22 @@ package org.yuzu.yuzu_emu.activities 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.Intent +import android.content.IntentFilter import android.content.res.Configuration import android.graphics.Rect +import android.graphics.drawable.Icon import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener import android.hardware.SensorManager import android.hardware.display.DisplayManager +import android.os.Build import android.os.Bundle import android.view.Display import android.view.InputDevice @@ -60,6 +67,10 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { private var motionTimestamp: Long = 0 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 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. enableFullscreenImmersive() + pictureInPictureParamsBuilder = getPictureInPictureBuilder() + setPictureInPictureParams(pictureInPictureParamsBuilder.build()) + setContentView(R.layout.activity_emulation) window.decorView.setBackgroundColor(getColor(android.R.color.black)) @@ -161,6 +175,12 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { stopMotionSensorListener() } + override fun onUserLeaveHint() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && !isInPictureInPictureMode) { + enterPictureInPictureMode(pictureInPictureParamsBuilder.build()) + } + } + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) @@ -289,6 +309,61 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener { } } + private fun getPictureInPictureBuilder() : PictureInPictureParams.Builder { + val pictureInPictureParamsBuilder = PictureInPictureParams.Builder() + + val pictureInPictureActions : MutableList = 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() { val sensorManager = this.getSystemService(Context.SENSOR_SERVICE) as SensorManager val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) 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 41b1a6e239..91e16e531f 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 @@ -23,6 +23,7 @@ import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.Insets import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat +import androidx.core.view.isVisible import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager @@ -178,6 +179,30 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback { 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() { binding.surfaceInputOverlay.refreshControls() } diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml index 0ae69afb47..f40e62c0d7 100644 --- a/src/android/app/src/main/res/values/strings.xml +++ b/src/android/app/src/main/res/values/strings.xml @@ -356,6 +356,10 @@ Black backgrounds When using the dark theme, apply black backgrounds. + + Pause + Play + Licenses FidelityFX-FSR