Add support for launching the Play window in PiP mode

This commit is contained in:
Fredia Huya-Kouadio 2024-03-07 19:16:25 -08:00
parent db76de5de8
commit 961394a988
23 changed files with 568 additions and 57 deletions

View file

@ -961,7 +961,17 @@
If [code]true[/code], on Linux/BSD, the editor will check for Wayland first instead of X11 (if available).
</member>
<member name="run/window_placement/android_window" type="int" setter="" getter="">
The Android window to display the project on when starting the project from the editor.
Specifies how the Play window is launched relative to the Android editor.
- [b]Auto (based on screen size)[/b] (default) will automatically choose how to launch the Play window based on the device and screen metrics. Defaults to [b]Same as Editor[/b] on phones and [b]Side-by-side with Editor[/b] on tablets.
- [b]Same as Editor[/b] will launch the Play window in the same window as the Editor.
- [b]Side-by-side with Editor[/b] will launch the Play window side-by-side with the Editor window.
[b]Note:[/b] Only available in the Android editor.
</member>
<member name="run/window_placement/play_window_pip_mode" type="int" setter="" getter="">
Specifies the picture-in-picture (PiP) mode for the Play window.
- [b]Disabled:[/b] PiP is disabled for the Play window.
- [b]Enabled:[/b] If the device supports it, PiP is always enabled for the Play window. The Play window will contain a button to enter PiP mode.
- [b]Enabled when Play window is same as Editor[/b] (default for Android editor): If the device supports it, PiP is enabled when the Play window is the same as the Editor. The Play window will contain a button to enter PiP mode.
[b]Note:[/b] Only available in the Android editor.
</member>
<member name="run/window_placement/rect" type="int" setter="" getter="">

View file

@ -825,6 +825,12 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
String android_window_hints = "Auto (based on screen size):0,Same as Editor:1,Side-by-side with Editor:2";
EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/android_window", 0, android_window_hints)
int default_play_window_pip_mode = 0;
#ifdef ANDROID_ENABLED
default_play_window_pip_mode = 2;
#endif
EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/play_window_pip_mode", default_play_window_pip_mode, "Disabled:0,Enabled:1,Enabled when Play window is same as Editor:2")
// Auto save
_initial_set("run/auto_save/save_before_running", true);

View file

@ -42,6 +42,7 @@
android:name=".GodotEditor"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
android:exported="true"
android:icon="@mipmap/icon"
android:launchMode="singleTask"
android:screenOrientation="userLandscape">
<layout
@ -59,9 +60,11 @@
android:name=".GodotGame"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
android:exported="false"
android:label="@string/godot_project_name_string"
android:icon="@mipmap/ic_play_window"
android:label="@string/godot_game_activity_name"
android:launchMode="singleTask"
android:process=":GodotGame"
android:supportsPictureInPicture="true"
android:screenOrientation="userLandscape">
<layout
android:defaultWidth="@dimen/editor_default_window_width"

View file

@ -0,0 +1,192 @@
/**************************************************************************/
/* EditorMessageDispatcher.kt */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
package org.godotengine.editor
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Handler
import android.os.Message
import android.os.Messenger
import android.os.RemoteException
import android.util.Log
import java.util.concurrent.ConcurrentHashMap
/**
* Used by the [GodotEditor] classes to dispatch messages across processes.
*/
internal class EditorMessageDispatcher(private val editor: GodotEditor) {
companion object {
private val TAG = EditorMessageDispatcher::class.java.simpleName
/**
* Extra used to pass the message dispatcher payload through an [Intent]
*/
const val EXTRA_MSG_DISPATCHER_PAYLOAD = "message_dispatcher_payload"
/**
* Key used to pass the editor id through a [Bundle]
*/
private const val KEY_EDITOR_ID = "editor_id"
/**
* Key used to pass the editor messenger through a [Bundle]
*/
private const val KEY_EDITOR_MESSENGER = "editor_messenger"
/**
* Requests the recipient to quit right away.
*/
private const val MSG_FORCE_QUIT = 0
/**
* Requests the recipient to store the passed [android.os.Messenger] instance.
*/
private const val MSG_REGISTER_MESSENGER = 1
}
private val recipientsMessengers = ConcurrentHashMap<Int, Messenger>()
@SuppressLint("HandlerLeak")
private val dispatcherHandler = object : Handler() {
override fun handleMessage(msg: Message) {
when (msg.what) {
MSG_FORCE_QUIT -> editor.finish()
MSG_REGISTER_MESSENGER -> {
val editorId = msg.arg1
val messenger = msg.replyTo
registerMessenger(editorId, messenger)
}
else -> super.handleMessage(msg)
}
}
}
/**
* Request the window with the given [editorId] to force quit.
*/
fun requestForceQuit(editorId: Int): Boolean {
val messenger = recipientsMessengers[editorId] ?: return false
return try {
Log.v(TAG, "Requesting 'forceQuit' for $editorId")
val msg = Message.obtain(null, MSG_FORCE_QUIT)
messenger.send(msg)
true
} catch (e: RemoteException) {
Log.e(TAG, "Error requesting 'forceQuit' to $editorId", e)
recipientsMessengers.remove(editorId)
false
}
}
/**
* Utility method to register a receiver messenger.
*/
private fun registerMessenger(editorId: Int, messenger: Messenger?, messengerDeathCallback: Runnable? = null) {
try {
if (messenger == null) {
Log.w(TAG, "Invalid 'replyTo' payload")
} else if (messenger.binder.isBinderAlive) {
messenger.binder.linkToDeath({
Log.v(TAG, "Removing messenger for $editorId")
recipientsMessengers.remove(editorId)
messengerDeathCallback?.run()
}, 0)
recipientsMessengers[editorId] = messenger
}
} catch (e: RemoteException) {
Log.e(TAG, "Unable to register messenger from $editorId", e)
recipientsMessengers.remove(editorId)
}
}
/**
* Utility method to register a [Messenger] attached to this handler with a host.
*
* This is done so that the host can send request to the editor instance attached to this handle.
*
* Note that this is only done when the editor instance is internal (not exported) to prevent
* arbitrary apps from having the ability to send requests.
*/
private fun registerSelfTo(pm: PackageManager, host: Messenger?, selfId: Int) {
try {
if (host == null || !host.binder.isBinderAlive) {
Log.v(TAG, "Host is unavailable")
return
}
val activityInfo = pm.getActivityInfo(editor.componentName, 0)
if (activityInfo.exported) {
Log.v(TAG, "Not registering self to host as we're exported")
return
}
Log.v(TAG, "Registering self $selfId to host")
val msg = Message.obtain(null, MSG_REGISTER_MESSENGER)
msg.arg1 = selfId
msg.replyTo = Messenger(dispatcherHandler)
host.send(msg)
} catch (e: RemoteException) {
Log.e(TAG, "Unable to register self with host", e)
}
}
/**
* Parses the starting intent and retrieve an editor messenger if available
*/
fun parseStartIntent(pm: PackageManager, intent: Intent) {
val messengerBundle = intent.getBundleExtra(EXTRA_MSG_DISPATCHER_PAYLOAD) ?: return
// Retrieve the sender messenger payload and store it. This can be used to communicate back
// to the sender.
val senderId = messengerBundle.getInt(KEY_EDITOR_ID)
val senderMessenger: Messenger? = messengerBundle.getParcelable(KEY_EDITOR_MESSENGER)
registerMessenger(senderId, senderMessenger)
// Register ourselves to the sender so that it can communicate with us.
registerSelfTo(pm, senderMessenger, editor.getEditorId())
}
/**
* Returns the payload used by the [EditorMessageDispatcher] class to establish an IPC bridge
* across editor instances.
*/
fun getMessageDispatcherPayload(): Bundle {
return Bundle().apply {
putInt(KEY_EDITOR_ID, editor.getEditorId())
putParcelable(KEY_EDITOR_MESSENGER, Messenger(dispatcherHandler))
}
}
}

View file

@ -31,23 +31,24 @@
package org.godotengine.editor
/**
* Specifies the policy for adjacent launches.
* Specifies the policy for launches.
*/
enum class LaunchAdjacentPolicy {
enum class LaunchPolicy {
/**
* Adjacent launches are disabled.
*/
DISABLED,
/**
* Adjacent launches are enabled / disabled based on the device and screen metrics.
* Launch policy is determined by the editor settings or based on the device and screen metrics.
*/
AUTO,
/**
* Launches happen in the same window.
*/
SAME,
/**
* Adjacent launches are enabled.
*/
ENABLED
ADJACENT
}
/**
@ -57,12 +58,14 @@ data class EditorWindowInfo(
val windowClassName: String,
val windowId: Int,
val processNameSuffix: String,
val launchAdjacentPolicy: LaunchAdjacentPolicy = LaunchAdjacentPolicy.DISABLED
val launchPolicy: LaunchPolicy = LaunchPolicy.SAME,
val supportsPiPMode: Boolean = false
) {
constructor(
windowClass: Class<*>,
windowId: Int,
processNameSuffix: String,
launchAdjacentPolicy: LaunchAdjacentPolicy = LaunchAdjacentPolicy.DISABLED
) : this(windowClass.name, windowId, processNameSuffix, launchAdjacentPolicy)
launchPolicy: LaunchPolicy = LaunchPolicy.SAME,
supportsPiPMode: Boolean = false
) : this(windowClass.name, windowId, processNameSuffix, launchPolicy, supportsPiPMode)
}

View file

@ -32,6 +32,7 @@ package org.godotengine.editor
import android.Manifest
import android.app.ActivityManager
import android.app.ActivityOptions
import android.content.ComponentName
import android.content.Context
import android.content.Intent
@ -69,17 +70,24 @@ open class GodotEditor : GodotActivity() {
private const val WAIT_FOR_DEBUGGER = false
private const val EXTRA_COMMAND_LINE_PARAMS = "command_line_params"
@JvmStatic
protected val EXTRA_COMMAND_LINE_PARAMS = "command_line_params"
@JvmStatic
protected val EXTRA_PIP_AVAILABLE = "pip_available"
@JvmStatic
protected val EXTRA_LAUNCH_IN_PIP = "launch_in_pip_requested"
// Command line arguments
private const val EDITOR_ARG = "--editor"
private const val EDITOR_ARG_SHORT = "-e"
private const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager"
private const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p"
private const val BREAKPOINTS_ARG = "--breakpoints"
private const val BREAKPOINTS_ARG_SHORT = "-b"
// Info for the various classes used by the editor
internal val EDITOR_MAIN_INFO = EditorWindowInfo(GodotEditor::class.java, 777, "")
internal val RUN_GAME_INFO = EditorWindowInfo(GodotGame::class.java, 667, ":GodotGame", LaunchAdjacentPolicy.AUTO)
internal val RUN_GAME_INFO = EditorWindowInfo(GodotGame::class.java, 667, ":GodotGame", LaunchPolicy.AUTO, true)
/**
* Sets of constants to specify the window to use to run the project.
@ -90,13 +98,26 @@ open class GodotEditor : GodotActivity() {
private const val ANDROID_WINDOW_AUTO = 0
private const val ANDROID_WINDOW_SAME_AS_EDITOR = 1
private const val ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR = 2
/**
* Sets of constants to specify the Play window PiP mode.
*
* Should match the values in `editor/editor_settings.cpp'` for the
* 'run/window_placement/play_window_pip_mode' setting.
*/
private const val PLAY_WINDOW_PIP_DISABLED = 0
private const val PLAY_WINDOW_PIP_ENABLED = 1
private const val PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR = 2
}
private val editorMessageDispatcher = EditorMessageDispatcher(this)
private val commandLineParams = ArrayList<String>()
private val editorLoadingIndicator: View? by lazy { findViewById(R.id.editor_loading_indicator) }
override fun getGodotAppLayout() = R.layout.godot_editor_layout
internal open fun getEditorId() = EDITOR_MAIN_INFO.windowId
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
@ -108,6 +129,8 @@ open class GodotEditor : GodotActivity() {
Log.d(TAG, "Starting intent $intent with parameters ${params.contentToString()}")
updateCommandLineParams(params?.asList() ?: emptyList())
editorMessageDispatcher.parseStartIntent(packageManager, intent)
if (BuildConfig.BUILD_TYPE == "dev" && WAIT_FOR_DEBUGGER) {
Debug.waitForDebugger()
}
@ -189,35 +212,67 @@ open class GodotEditor : GodotActivity() {
}
}
override fun onNewGodotInstanceRequested(args: Array<String>): Int {
val editorWindowInfo = getEditorWindowInfo(args)
// Launch a new activity
protected fun getNewGodotInstanceIntent(editorWindowInfo: EditorWindowInfo, args: Array<String>): Intent {
val newInstance = Intent()
.setComponent(ComponentName(this, editorWindowInfo.windowClassName))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(EXTRA_COMMAND_LINE_PARAMS, args)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (editorWindowInfo.launchAdjacentPolicy == LaunchAdjacentPolicy.ENABLED ||
(editorWindowInfo.launchAdjacentPolicy == LaunchAdjacentPolicy.AUTO && shouldGameLaunchAdjacent())) {
val launchPolicy = resolveLaunchPolicyIfNeeded(editorWindowInfo.launchPolicy)
val isPiPAvailable = if (editorWindowInfo.supportsPiPMode && hasPiPSystemFeature()) {
val pipMode = getPlayWindowPiPMode()
pipMode == PLAY_WINDOW_PIP_ENABLED ||
(pipMode == PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR && launchPolicy == LaunchPolicy.SAME)
} else {
false
}
newInstance.putExtra(EXTRA_PIP_AVAILABLE, isPiPAvailable)
if (launchPolicy == LaunchPolicy.ADJACENT) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Log.v(TAG, "Adding flag for adjacent launch")
newInstance.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT)
}
} else if (launchPolicy == LaunchPolicy.SAME) {
if (isPiPAvailable &&
(args.contains(BREAKPOINTS_ARG) || args.contains(BREAKPOINTS_ARG_SHORT))) {
Log.v(TAG, "Launching in PiP mode because of breakpoints")
newInstance.putExtra(EXTRA_LAUNCH_IN_PIP, true)
}
}
return newInstance
}
override fun onNewGodotInstanceRequested(args: Array<String>): Int {
val editorWindowInfo = getEditorWindowInfo(args)
// Launch a new activity
val sourceView = godotFragment?.view
val activityOptions = if (sourceView == null) {
null
} else {
val startX = sourceView.width / 2
val startY = sourceView.height / 2
ActivityOptions.makeScaleUpAnimation(sourceView, startX, startY, 0, 0)
}
val newInstance = getNewGodotInstanceIntent(editorWindowInfo, args)
if (editorWindowInfo.windowClassName == javaClass.name) {
Log.d(TAG, "Restarting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}")
val godot = godot
if (godot != null) {
godot.destroyAndKillProcess {
ProcessPhoenix.triggerRebirth(this, newInstance)
ProcessPhoenix.triggerRebirth(this, activityOptions?.toBundle(), newInstance)
}
} else {
ProcessPhoenix.triggerRebirth(this, newInstance)
ProcessPhoenix.triggerRebirth(this, activityOptions?.toBundle(), newInstance)
}
} else {
Log.d(TAG, "Starting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}")
newInstance.putExtra(EXTRA_NEW_LAUNCH, true)
startActivity(newInstance)
.putExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD, editorMessageDispatcher.getMessageDispatcherPayload())
startActivity(newInstance, activityOptions?.toBundle())
}
return editorWindowInfo.windowId
}
@ -231,6 +286,12 @@ open class GodotEditor : GodotActivity() {
return true
}
// Send an inter-process message to request the target editor window to force quit.
if (editorMessageDispatcher.requestForceQuit(editorWindowInfo.windowId)) {
return true
}
// Fallback to killing the target process.
val processName = packageName + editorWindowInfo.processNameSuffix
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val runningProcesses = activityManager.runningAppProcesses
@ -285,29 +346,65 @@ open class GodotEditor : GodotActivity() {
java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/enable_pan_and_scale_gestures"))
/**
* Whether we should launch the new godot instance in an adjacent window
* @see https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_LAUNCH_ADJACENT
* Retrieves the play window pip mode editor setting.
*/
private fun shouldGameLaunchAdjacent(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
try {
when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) {
ANDROID_WINDOW_SAME_AS_EDITOR -> false
ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> true
else -> {
// ANDROID_WINDOW_AUTO
isInMultiWindowMode || isLargeScreen
}
}
} catch (e: NumberFormatException) {
// Fall-back to the 'Auto' behavior
isInMultiWindowMode || isLargeScreen
}
private fun getPlayWindowPiPMode(): Int {
return try {
Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/play_window_pip_mode"))
} catch (e: NumberFormatException) {
PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR
}
}
/**
* If the launch policy is [LaunchPolicy.AUTO], resolve it into a specific policy based on the
* editor setting or device and screen metrics.
*
* If the launch policy is [LaunchPolicy.PIP] but PIP is not supported, fallback to the default
* launch policy.
*/
private fun resolveLaunchPolicyIfNeeded(policy: LaunchPolicy): LaunchPolicy {
val inMultiWindowMode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
isInMultiWindowMode
} else {
false
}
val defaultLaunchPolicy = if (inMultiWindowMode || isLargeScreen) {
LaunchPolicy.ADJACENT
} else {
LaunchPolicy.SAME
}
return when (policy) {
LaunchPolicy.AUTO -> {
try {
when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) {
ANDROID_WINDOW_SAME_AS_EDITOR -> LaunchPolicy.SAME
ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> LaunchPolicy.ADJACENT
else -> {
// ANDROID_WINDOW_AUTO
defaultLaunchPolicy
}
}
} catch (e: NumberFormatException) {
Log.w(TAG, "Error parsing the Android window placement editor setting", e)
// Fall-back to the default launch policy
defaultLaunchPolicy
}
}
else -> {
policy
}
}
}
/**
* Returns true the if the device supports picture-in-picture (PiP)
*/
protected open fun hasPiPSystemFeature() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// Check if we got the MANAGE_EXTERNAL_STORAGE permission

View file

@ -30,6 +30,14 @@
package org.godotengine.editor
import android.annotation.SuppressLint
import android.app.PictureInPictureParams
import android.content.Intent
import android.graphics.Rect
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.View
import org.godotengine.godot.GodotLib
/**
@ -37,7 +45,90 @@ import org.godotengine.godot.GodotLib
*/
class GodotGame : GodotEditor() {
override fun getGodotAppLayout() = org.godotengine.godot.R.layout.godot_app_layout
companion object {
private val TAG = GodotGame::class.java.simpleName
}
private val gameViewSourceRectHint = Rect()
private val pipButton: View? by lazy {
findViewById(R.id.godot_pip_button)
}
private var pipAvailable = false
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val gameView = findViewById<View>(R.id.godot_fragment_container)
gameView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
gameView.getGlobalVisibleRect(gameViewSourceRectHint)
}
}
pipButton?.setOnClickListener { enterPiPMode() }
handleStartIntent(intent)
}
override fun onNewIntent(newIntent: Intent) {
super.onNewIntent(newIntent)
handleStartIntent(newIntent)
}
private fun handleStartIntent(intent: Intent) {
pipAvailable = intent.getBooleanExtra(EXTRA_PIP_AVAILABLE, pipAvailable)
updatePiPButtonVisibility()
val pipLaunchRequested = intent.getBooleanExtra(EXTRA_LAUNCH_IN_PIP, false)
if (pipLaunchRequested) {
enterPiPMode()
}
}
private fun updatePiPButtonVisibility() {
pipButton?.visibility = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && pipAvailable && !isInPictureInPictureMode) {
View.VISIBLE
} else {
View.GONE
}
}
private fun enterPiPMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && pipAvailable) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val builder = PictureInPictureParams.Builder().setSourceRectHint(gameViewSourceRectHint)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
builder.setSeamlessResizeEnabled(false)
}
setPictureInPictureParams(builder.build())
}
Log.v(TAG, "Entering PiP mode")
enterPictureInPictureMode()
}
}
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
Log.v(TAG, "onPictureInPictureModeChanged: $isInPictureInPictureMode")
updatePiPButtonVisibility()
}
override fun onStop() {
super.onStop()
val isInPiPMode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode
if (isInPiPMode && !isFinishing) {
// We get in this state when PiP is closed, so we terminate the activity.
finish()
}
}
override fun getGodotAppLayout() = R.layout.godot_game_layout
override fun getEditorId() = RUN_GAME_INFO.windowId
override fun overrideOrientationRequest() = false

View file

@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:scaleX="0.522"
android:scaleY="0.522"
android:translateX="5.736"
android:translateY="5.736">
<path
android:fillColor="@android:color/white"
android:pathData="M21.58,16.09l-1.09,-7.66C20.21,6.46 18.52,5 16.53,5H7.47C5.48,5 3.79,6.46 3.51,8.43l-1.09,7.66C2.2,17.63 3.39,19 4.94,19h0c0.68,0 1.32,-0.27 1.8,-0.75L9,16h6l2.25,2.25c0.48,0.48 1.13,0.75 1.8,0.75h0C20.61,19 21.8,17.63 21.58,16.09zM19.48,16.81C19.4,16.9 19.27,17 19.06,17c-0.15,0 -0.29,-0.06 -0.39,-0.16L15.83,14H8.17l-2.84,2.84C5.23,16.94 5.09,17 4.94,17c-0.21,0 -0.34,-0.1 -0.42,-0.19c-0.08,-0.09 -0.16,-0.23 -0.13,-0.44l1.09,-7.66C5.63,7.74 6.48,7 7.47,7h9.06c0.99,0 1.84,0.74 1.98,1.72l1.09,7.66C19.63,16.58 19.55,16.72 19.48,16.81z" />
<path
android:fillColor="@android:color/white"
android:pathData="M9,8l-1,0l0,2l-2,0l0,1l2,0l0,2l1,0l0,-2l2,0l0,-1l-2,0z" />
<path
android:fillColor="@android:color/white"
android:pathData="M17,12m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
<path
android:fillColor="@android:color/white"
android:pathData="M15,9m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0" />
</group>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z" />
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size
android:width="60dp"
android:height="60dp" />
<solid android:color="#44000000" />
</shape>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/pip_button_activated_bg_drawable" android:state_pressed="true" />
<item android:drawable="@drawable/pip_button_activated_bg_drawable" android:state_hovered="true" />
<item android:drawable="@drawable/pip_button_default_bg_drawable" />
</selector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size
android:width="60dp"
android:height="60dp" />
<solid android:color="#13000000" />
</shape>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/godot_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ImageView
android:id="@+id/godot_pip_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="36dp"
android:contentDescription="@string/pip_button_description"
android:background="@drawable/pip_button_bg_drawable"
android:scaleType="center"
android:src="@drawable/outline_fullscreen_exit_48"
android:visibility="gone"
android:layout_gravity="end|top"
tools:visibility="visible" />
</FrameLayout>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/icon_background"/>
<foreground android:drawable="@drawable/ic_play_window_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="editor_default_window_height">600dp</dimen>
<dimen name="editor_default_window_height">640dp</dimen>
<dimen name="editor_default_window_width">1024dp</dimen>
</resources>

View file

@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="godot_game_activity_name">Godot Play window</string>
<string name="denied_storage_permission_error_msg">Missing storage access permission!</string>
<string name="pip_button_description">Button used to toggle picture-in-picture mode for the Play window</string>
</resources>

View file

@ -52,8 +52,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
companion object {
private val TAG = GodotActivity::class.java.simpleName
@JvmStatic
protected val EXTRA_FORCE_QUIT = "force_quit_requested"
@JvmStatic
protected val EXTRA_NEW_LAUNCH = "new_launch_requested"
}
@ -128,12 +126,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
}
private fun handleStartIntent(intent: Intent, newLaunch: Boolean) {
val forceQuitRequested = intent.getBooleanExtra(EXTRA_FORCE_QUIT, false)
if (forceQuitRequested) {
Log.d(TAG, "Force quit requested, terminating..")
ProcessPhoenix.forceQuit(this)
return
}
if (!newLaunch) {
val newLaunchRequested = intent.getBooleanExtra(EXTRA_NEW_LAUNCH, false)
if (newLaunchRequested) {

View file

@ -24,6 +24,7 @@ package org.godotengine.godot.utils;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
@ -44,6 +45,9 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
*/
public final class ProcessPhoenix extends Activity {
private static final String KEY_RESTART_INTENTS = "phoenix_restart_intents";
// -- GODOT start --
private static final String KEY_RESTART_ACTIVITY_OPTIONS = "phoenix_restart_activity_options";
// -- GODOT end --
private static final String KEY_MAIN_PROCESS_PID = "phoenix_main_process_pid";
/**
@ -56,12 +60,23 @@ public final class ProcessPhoenix extends Activity {
triggerRebirth(context, getRestartIntent(context));
}
// -- GODOT start --
/**
* Call to restart the application process using the specified intents.
* <p>
* Behavior of the current process after invoking this method is undefined.
*/
public static void triggerRebirth(Context context, Intent... nextIntents) {
triggerRebirth(context, null, nextIntents);
}
/**
* Call to restart the application process using the specified intents launched with the given
* {@link ActivityOptions}.
* <p>
* Behavior of the current process after invoking this method is undefined.
*/
public static void triggerRebirth(Context context, Bundle activityOptions, Intent... nextIntents) {
if (nextIntents.length < 1) {
throw new IllegalArgumentException("intents cannot be empty");
}
@ -72,10 +87,12 @@ public final class ProcessPhoenix extends Activity {
intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context.
intent.putParcelableArrayListExtra(KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents)));
intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid());
if (activityOptions != null) {
intent.putExtra(KEY_RESTART_ACTIVITY_OPTIONS, activityOptions);
}
context.startActivity(intent);
}
// -- GODOT start --
/**
* Finish the activity and kill its process
*/
@ -112,9 +129,11 @@ public final class ProcessPhoenix extends Activity {
super.onCreate(savedInstanceState);
// -- GODOT start --
ArrayList<Intent> intents = getIntent().getParcelableArrayListExtra(KEY_RESTART_INTENTS);
startActivities(intents.toArray(new Intent[intents.size()]));
forceQuit(this, getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1));
Intent launchIntent = getIntent();
ArrayList<Intent> intents = launchIntent.getParcelableArrayListExtra(KEY_RESTART_INTENTS);
Bundle activityOptions = launchIntent.getBundleExtra(KEY_RESTART_ACTIVITY_OPTIONS);
startActivities(intents.toArray(new Intent[intents.size()]), activityOptions);
forceQuit(this, launchIntent.getIntExtra(KEY_MAIN_PROCESS_PID, -1));
// -- GODOT end --
}