Skip to content

Commit 60765be

Browse files
committedAug 28, 2024
Merge pull request #95700 from m4gr3d/add_pip_support_to_game_window
[Android Editor] Add support for launching the Play window in PiP mode
2 parents 2730d70 + 961394a commit 60765be

23 files changed

+565
-54
lines changed
 

‎doc/classes/EditorSettings.xml

+11-1
Original file line numberDiff line numberDiff line change
@@ -963,7 +963,17 @@
963963
If [code]true[/code], on Linux/BSD, the editor will check for Wayland first instead of X11 (if available).
964964
</member>
965965
<member name="run/window_placement/android_window" type="int" setter="" getter="">
966-
The Android window to display the project on when starting the project from the editor.
966+
Specifies how the Play window is launched relative to the Android editor.
967+
- [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.
968+
- [b]Same as Editor[/b] will launch the Play window in the same window as the Editor.
969+
- [b]Side-by-side with Editor[/b] will launch the Play window side-by-side with the Editor window.
970+
[b]Note:[/b] Only available in the Android editor.
971+
</member>
972+
<member name="run/window_placement/play_window_pip_mode" type="int" setter="" getter="">
973+
Specifies the picture-in-picture (PiP) mode for the Play window.
974+
- [b]Disabled:[/b] PiP is disabled for the Play window.
975+
- [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.
976+
- [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.
967977
[b]Note:[/b] Only available in the Android editor.
968978
</member>
969979
<member name="run/window_placement/rect" type="int" setter="" getter="">

‎editor/editor_settings.cpp

+6
Original file line numberDiff line numberDiff line change
@@ -827,6 +827,12 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
827827
String android_window_hints = "Auto (based on screen size):0,Same as Editor:1,Side-by-side with Editor:2";
828828
EDITOR_SETTING(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/android_window", 0, android_window_hints)
829829

830+
int default_play_window_pip_mode = 0;
831+
#ifdef ANDROID_ENABLED
832+
default_play_window_pip_mode = 2;
833+
#endif
834+
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")
835+
830836
// Auto save
831837
_initial_set("run/auto_save/save_before_running", true);
832838

‎platform/android/java/editor/src/main/AndroidManifest.xml

+4-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
android:name=".GodotEditor"
4343
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
4444
android:exported="true"
45+
android:icon="@mipmap/icon"
4546
android:launchMode="singleTask"
4647
android:screenOrientation="userLandscape">
4748
<layout
@@ -59,9 +60,11 @@
5960
android:name=".GodotGame"
6061
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
6162
android:exported="false"
62-
android:label="@string/godot_project_name_string"
63+
android:icon="@mipmap/ic_play_window"
64+
android:label="@string/godot_game_activity_name"
6365
android:launchMode="singleTask"
6466
android:process=":GodotGame"
67+
android:supportsPictureInPicture="true"
6568
android:screenOrientation="userLandscape">
6669
<layout
6770
android:defaultWidth="@dimen/editor_default_window_width"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/**************************************************************************/
2+
/* EditorMessageDispatcher.kt */
3+
/**************************************************************************/
4+
/* This file is part of: */
5+
/* GODOT ENGINE */
6+
/* https://godotengine.org */
7+
/**************************************************************************/
8+
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9+
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10+
/* */
11+
/* Permission is hereby granted, free of charge, to any person obtaining */
12+
/* a copy of this software and associated documentation files (the */
13+
/* "Software"), to deal in the Software without restriction, including */
14+
/* without limitation the rights to use, copy, modify, merge, publish, */
15+
/* distribute, sublicense, and/or sell copies of the Software, and to */
16+
/* permit persons to whom the Software is furnished to do so, subject to */
17+
/* the following conditions: */
18+
/* */
19+
/* The above copyright notice and this permission notice shall be */
20+
/* included in all copies or substantial portions of the Software. */
21+
/* */
22+
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23+
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24+
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25+
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26+
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27+
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28+
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29+
/**************************************************************************/
30+
31+
package org.godotengine.editor
32+
33+
import android.annotation.SuppressLint
34+
import android.content.Intent
35+
import android.content.pm.PackageManager
36+
import android.os.Bundle
37+
import android.os.Handler
38+
import android.os.Message
39+
import android.os.Messenger
40+
import android.os.RemoteException
41+
import android.util.Log
42+
import java.util.concurrent.ConcurrentHashMap
43+
44+
/**
45+
* Used by the [GodotEditor] classes to dispatch messages across processes.
46+
*/
47+
internal class EditorMessageDispatcher(private val editor: GodotEditor) {
48+
49+
companion object {
50+
private val TAG = EditorMessageDispatcher::class.java.simpleName
51+
52+
/**
53+
* Extra used to pass the message dispatcher payload through an [Intent]
54+
*/
55+
const val EXTRA_MSG_DISPATCHER_PAYLOAD = "message_dispatcher_payload"
56+
57+
/**
58+
* Key used to pass the editor id through a [Bundle]
59+
*/
60+
private const val KEY_EDITOR_ID = "editor_id"
61+
62+
/**
63+
* Key used to pass the editor messenger through a [Bundle]
64+
*/
65+
private const val KEY_EDITOR_MESSENGER = "editor_messenger"
66+
67+
/**
68+
* Requests the recipient to quit right away.
69+
*/
70+
private const val MSG_FORCE_QUIT = 0
71+
72+
/**
73+
* Requests the recipient to store the passed [android.os.Messenger] instance.
74+
*/
75+
private const val MSG_REGISTER_MESSENGER = 1
76+
}
77+
78+
private val recipientsMessengers = ConcurrentHashMap<Int, Messenger>()
79+
80+
@SuppressLint("HandlerLeak")
81+
private val dispatcherHandler = object : Handler() {
82+
override fun handleMessage(msg: Message) {
83+
when (msg.what) {
84+
MSG_FORCE_QUIT -> editor.finish()
85+
86+
MSG_REGISTER_MESSENGER -> {
87+
val editorId = msg.arg1
88+
val messenger = msg.replyTo
89+
registerMessenger(editorId, messenger)
90+
}
91+
92+
else -> super.handleMessage(msg)
93+
}
94+
}
95+
}
96+
97+
/**
98+
* Request the window with the given [editorId] to force quit.
99+
*/
100+
fun requestForceQuit(editorId: Int): Boolean {
101+
val messenger = recipientsMessengers[editorId] ?: return false
102+
return try {
103+
Log.v(TAG, "Requesting 'forceQuit' for $editorId")
104+
val msg = Message.obtain(null, MSG_FORCE_QUIT)
105+
messenger.send(msg)
106+
true
107+
} catch (e: RemoteException) {
108+
Log.e(TAG, "Error requesting 'forceQuit' to $editorId", e)
109+
recipientsMessengers.remove(editorId)
110+
false
111+
}
112+
}
113+
114+
/**
115+
* Utility method to register a receiver messenger.
116+
*/
117+
private fun registerMessenger(editorId: Int, messenger: Messenger?, messengerDeathCallback: Runnable? = null) {
118+
try {
119+
if (messenger == null) {
120+
Log.w(TAG, "Invalid 'replyTo' payload")
121+
} else if (messenger.binder.isBinderAlive) {
122+
messenger.binder.linkToDeath({
123+
Log.v(TAG, "Removing messenger for $editorId")
124+
recipientsMessengers.remove(editorId)
125+
messengerDeathCallback?.run()
126+
}, 0)
127+
recipientsMessengers[editorId] = messenger
128+
}
129+
} catch (e: RemoteException) {
130+
Log.e(TAG, "Unable to register messenger from $editorId", e)
131+
recipientsMessengers.remove(editorId)
132+
}
133+
}
134+
135+
/**
136+
* Utility method to register a [Messenger] attached to this handler with a host.
137+
*
138+
* This is done so that the host can send request to the editor instance attached to this handle.
139+
*
140+
* Note that this is only done when the editor instance is internal (not exported) to prevent
141+
* arbitrary apps from having the ability to send requests.
142+
*/
143+
private fun registerSelfTo(pm: PackageManager, host: Messenger?, selfId: Int) {
144+
try {
145+
if (host == null || !host.binder.isBinderAlive) {
146+
Log.v(TAG, "Host is unavailable")
147+
return
148+
}
149+
150+
val activityInfo = pm.getActivityInfo(editor.componentName, 0)
151+
if (activityInfo.exported) {
152+
Log.v(TAG, "Not registering self to host as we're exported")
153+
return
154+
}
155+
156+
Log.v(TAG, "Registering self $selfId to host")
157+
val msg = Message.obtain(null, MSG_REGISTER_MESSENGER)
158+
msg.arg1 = selfId
159+
msg.replyTo = Messenger(dispatcherHandler)
160+
host.send(msg)
161+
} catch (e: RemoteException) {
162+
Log.e(TAG, "Unable to register self with host", e)
163+
}
164+
}
165+
166+
/**
167+
* Parses the starting intent and retrieve an editor messenger if available
168+
*/
169+
fun parseStartIntent(pm: PackageManager, intent: Intent) {
170+
val messengerBundle = intent.getBundleExtra(EXTRA_MSG_DISPATCHER_PAYLOAD) ?: return
171+
172+
// Retrieve the sender messenger payload and store it. This can be used to communicate back
173+
// to the sender.
174+
val senderId = messengerBundle.getInt(KEY_EDITOR_ID)
175+
val senderMessenger: Messenger? = messengerBundle.getParcelable(KEY_EDITOR_MESSENGER)
176+
registerMessenger(senderId, senderMessenger)
177+
178+
// Register ourselves to the sender so that it can communicate with us.
179+
registerSelfTo(pm, senderMessenger, editor.getEditorId())
180+
}
181+
182+
/**
183+
* Returns the payload used by the [EditorMessageDispatcher] class to establish an IPC bridge
184+
* across editor instances.
185+
*/
186+
fun getMessageDispatcherPayload(): Bundle {
187+
return Bundle().apply {
188+
putInt(KEY_EDITOR_ID, editor.getEditorId())
189+
putParcelable(KEY_EDITOR_MESSENGER, Messenger(dispatcherHandler))
190+
}
191+
}
192+
}

‎platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt

+13-10
Original file line numberDiff line numberDiff line change
@@ -31,23 +31,24 @@
3131
package org.godotengine.editor
3232

3333
/**
34-
* Specifies the policy for adjacent launches.
34+
* Specifies the policy for launches.
3535
*/
36-
enum class LaunchAdjacentPolicy {
36+
enum class LaunchPolicy {
3737
/**
38-
* Adjacent launches are disabled.
38+
* Launch policy is determined by the editor settings or based on the device and screen metrics.
3939
*/
40-
DISABLED,
40+
AUTO,
41+
4142

4243
/**
43-
* Adjacent launches are enabled / disabled based on the device and screen metrics.
44+
* Launches happen in the same window.
4445
*/
45-
AUTO,
46+
SAME,
4647

4748
/**
4849
* Adjacent launches are enabled.
4950
*/
50-
ENABLED
51+
ADJACENT
5152
}
5253

5354
/**
@@ -57,12 +58,14 @@ data class EditorWindowInfo(
5758
val windowClassName: String,
5859
val windowId: Int,
5960
val processNameSuffix: String,
60-
val launchAdjacentPolicy: LaunchAdjacentPolicy = LaunchAdjacentPolicy.DISABLED
61+
val launchPolicy: LaunchPolicy = LaunchPolicy.SAME,
62+
val supportsPiPMode: Boolean = false
6163
) {
6264
constructor(
6365
windowClass: Class<*>,
6466
windowId: Int,
6567
processNameSuffix: String,
66-
launchAdjacentPolicy: LaunchAdjacentPolicy = LaunchAdjacentPolicy.DISABLED
67-
) : this(windowClass.name, windowId, processNameSuffix, launchAdjacentPolicy)
68+
launchPolicy: LaunchPolicy = LaunchPolicy.SAME,
69+
supportsPiPMode: Boolean = false
70+
) : this(windowClass.name, windowId, processNameSuffix, launchPolicy, supportsPiPMode)
6871
}

0 commit comments

Comments
 (0)
Please sign in to comment.