Add support for launching the Play window in PiP mode
This commit is contained in:
parent
db76de5de8
commit
961394a988
23 changed files with 568 additions and 57 deletions
|
@ -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="">
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 |
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 --
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue