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
  • Loading branch information
m4gr3d committed Aug 28, 2024
1 parent db76de5 commit 961394a
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 @@ -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="">
Expand Down
6 changes: 6 additions & 0 deletions editor/editor_settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

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 @@ -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.
* 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 @@ -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)
}
Loading

0 comments on commit 961394a

Please sign in to comment.