Skip to content

Commit

Permalink
Add support for launching the Play window in PiP mode
Browse files Browse the repository at this point in the history
(cherry picked from commit 961394a)
  • Loading branch information
m4gr3d authored and Spartan322 committed Oct 31, 2024
1 parent 6861c0f commit 12fbc08
Show file tree
Hide file tree
Showing 23 changed files with 565 additions and 54 deletions.
12 changes: 11 additions & 1 deletion doc/classes/EditorSettings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,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="">
Expand Down
6 changes: 6 additions & 0 deletions editor/editor_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,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);

Expand Down
5 changes: 4 additions & 1 deletion platform/android/java/editor/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,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.
* Launch policy is determined by the editor settings or based on the device and screen metrics.
*/
DISABLED,
AUTO,


/**
* Adjacent launches are enabled / disabled based on the device and screen metrics.
* Launches happen in the same window.
*/
AUTO,
SAME,

/**
* Adjacent launches are enabled.
*/
ENABLED
ADJACENT
}

/**
Expand All @@ -59,12 +60,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)
}
Loading

0 comments on commit 12fbc08

Please sign in to comment.