Browse Source

Merge pull request #103972 from m4gr3d/xr_editor_hybrid_support

Add support for running hybrid apps from the XR editor
Thaddeus Crews 2 months ago
parent
commit
f7c6f0057c

+ 1 - 3
editor/debugger/editor_debugger_node.cpp

@@ -311,9 +311,7 @@ void EditorDebuggerNode::stop(bool p_force) {
 
 	// Also close all debugging sessions.
 	_for_all(tabs, [&](ScriptEditorDebugger *dbg) {
-		if (dbg->is_session_active()) {
-			dbg->_stop_and_notify();
-		}
+		dbg->_stop_and_notify();
 	});
 	_break_state_changed();
 	breakpoints.clear();

+ 1 - 1
editor/editor_node.cpp

@@ -2176,7 +2176,7 @@ void EditorNode::try_autosave() {
 		Node *scene = editor_data.get_edited_scene_root();
 
 		if (scene && !scene->get_scene_file_path().is_empty()) { // Only autosave if there is a scene and if it has a path.
-			_save_scene(scene->get_scene_file_path());
+			_save_scene(scene->get_scene_file_path(), -1, false);
 		}
 	}
 	_menu_option(SCENE_SAVE_ALL_SCENES);

+ 2 - 1
platform/android/SCsub

@@ -32,7 +32,8 @@ android_files = [
     "rendering_context_driver_vulkan_android.cpp",
     "variant/callable_jni.cpp",
     "dialog_utils_jni.cpp",
-    "game_menu_utils_jni.cpp",
+    "editor/game_menu_utils_jni.cpp",
+    "editor/editor_utils_jni.cpp",
 ]
 
 env_android = env.Clone()

+ 82 - 0
platform/android/editor/editor_utils_jni.cpp

@@ -0,0 +1,82 @@
+/**************************************************************************/
+/*  editor_utils_jni.cpp                                                  */
+/**************************************************************************/
+/*                         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.                 */
+/**************************************************************************/
+
+#include "editor_utils_jni.h"
+
+#include "jni_utils.h"
+
+#ifdef TOOLS_ENABLED
+#include "editor/gui/editor_run_bar.h"
+#include "main/main.h"
+#endif
+
+extern "C" {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_EditorUtils_runScene(JNIEnv *p_env, jclass, jstring p_scene, jobjectArray p_scene_args) {
+#ifdef TOOLS_ENABLED
+	Vector<String> scene_args;
+	jint length = p_env->GetArrayLength(p_scene_args);
+	for (jint i = 0; i < length; ++i) {
+		jstring j_arg = (jstring)p_env->GetObjectArrayElement(p_scene_args, i);
+		String arg = jstring_to_string(j_arg, p_env);
+		scene_args.push_back(arg);
+		p_env->DeleteLocalRef(j_arg);
+	}
+
+	String scene = jstring_to_string(p_scene, p_env);
+
+	EditorRunBar *editor_run_bar = EditorRunBar::get_singleton();
+	if (editor_run_bar != nullptr) {
+		if (scene.is_empty()) {
+			editor_run_bar->play_main_scene(false);
+		} else {
+			editor_run_bar->play_custom_scene(scene, scene_args);
+		}
+	} else {
+		List<String> args;
+
+		for (const String &a : Main::get_forwardable_cli_arguments(Main::CLI_SCOPE_PROJECT)) {
+			args.push_back(a);
+		}
+
+		for (const String &arg : scene_args) {
+			args.push_back(arg);
+		}
+
+		if (!scene.is_empty()) {
+			args.push_back("--scene");
+			args.push_back(scene);
+		}
+
+		Error err = OS::get_singleton()->create_instance(args);
+		ERR_FAIL_COND(err);
+	}
+#endif
+}
+}

+ 37 - 0
platform/android/editor/editor_utils_jni.h

@@ -0,0 +1,37 @@
+/**************************************************************************/
+/*  editor_utils_jni.h                                                    */
+/**************************************************************************/
+/*                         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.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include <jni.h>
+
+extern "C" {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_EditorUtils_runScene(JNIEnv *p_env, jclass, jstring p_scene, jobjectArray p_scene_args);
+}

+ 11 - 11
platform/android/game_menu_utils_jni.cpp → platform/android/editor/game_menu_utils_jni.cpp

@@ -45,7 +45,7 @@ static GameViewPlugin *_get_game_view_plugin() {
 
 extern "C" {
 
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSuspend(JNIEnv *env, jclass clazz, jboolean enabled) {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSuspend(JNIEnv *env, jclass clazz, jboolean enabled) {
 #ifdef TOOLS_ENABLED
 	GameViewPlugin *game_view_plugin = _get_game_view_plugin();
 	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -54,7 +54,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSuspend
 #endif
 }
 
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_nextFrame(JNIEnv *env, jclass clazz) {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_nextFrame(JNIEnv *env, jclass clazz) {
 #ifdef TOOLS_ENABLED
 	GameViewPlugin *game_view_plugin = _get_game_view_plugin();
 	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -63,7 +63,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_nextFrame(
 #endif
 }
 
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setNodeType(JNIEnv *env, jclass clazz, jint type) {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setNodeType(JNIEnv *env, jclass clazz, jint type) {
 #ifdef TOOLS_ENABLED
 	GameViewPlugin *game_view_plugin = _get_game_view_plugin();
 	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -72,7 +72,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setNodeTyp
 #endif
 }
 
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectMode(JNIEnv *env, jclass clazz, jint mode) {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSelectMode(JNIEnv *env, jclass clazz, jint mode) {
 #ifdef TOOLS_ENABLED
 	GameViewPlugin *game_view_plugin = _get_game_view_plugin();
 	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -81,7 +81,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectM
 #endif
 }
 
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectionVisible(JNIEnv *env, jclass clazz, jboolean visible) {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSelectionVisible(JNIEnv *env, jclass clazz, jboolean visible) {
 #ifdef TOOLS_ENABLED
 	GameViewPlugin *game_view_plugin = _get_game_view_plugin();
 	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -90,7 +90,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelecti
 #endif
 }
 
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled) {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled) {
 #ifdef TOOLS_ENABLED
 	GameViewPlugin *game_view_plugin = _get_game_view_plugin();
 	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -99,7 +99,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraO
 #endif
 }
 
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraManipulateMode(JNIEnv *env, jclass clazz, jint mode) {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setCameraManipulateMode(JNIEnv *env, jclass clazz, jint mode) {
 #ifdef TOOLS_ENABLED
 	GameViewPlugin *game_view_plugin = _get_game_view_plugin();
 	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -108,7 +108,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraM
 #endif
 }
 
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera2DPosition(JNIEnv *env, jclass clazz) {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_resetCamera2DPosition(JNIEnv *env, jclass clazz) {
 #ifdef TOOLS_ENABLED
 	GameViewPlugin *game_view_plugin = _get_game_view_plugin();
 	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -117,7 +117,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamer
 #endif
 }
 
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera3DPosition(JNIEnv *env, jclass clazz) {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_resetCamera3DPosition(JNIEnv *env, jclass clazz) {
 #ifdef TOOLS_ENABLED
 	GameViewPlugin *game_view_plugin = _get_game_view_plugin();
 	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
@@ -126,7 +126,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamer
 #endif
 }
 
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_playMainScene(JNIEnv *env, jclass clazz) {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_playMainScene(JNIEnv *env, jclass clazz) {
 #ifdef TOOLS_ENABLED
 	if (EditorInterface::get_singleton()) {
 		EditorInterface::get_singleton()->play_main_scene();
@@ -134,7 +134,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_playMainSc
 #endif
 }
 
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setDebugMuteAudio(JNIEnv *env, jclass clazz, jboolean enabled) {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setDebugMuteAudio(JNIEnv *env, jclass clazz, jboolean enabled) {
 #ifdef TOOLS_ENABLED
 	GameViewPlugin *game_view_plugin = _get_game_view_plugin();
 	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {

+ 11 - 11
platform/android/game_menu_utils_jni.h → platform/android/editor/game_menu_utils_jni.h

@@ -33,15 +33,15 @@
 #include <jni.h>
 
 extern "C" {
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSuspend(JNIEnv *env, jclass clazz, jboolean enabled);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_nextFrame(JNIEnv *env, jclass clazz);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setNodeType(JNIEnv *env, jclass clazz, jint type);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectMode(JNIEnv *env, jclass clazz, jint mode);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectionVisible(JNIEnv *env, jclass clazz, jboolean visible);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraManipulateMode(JNIEnv *env, jclass clazz, jint mode);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera2DPosition(JNIEnv *env, jclass clazz);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera3DPosition(JNIEnv *env, jclass clazz);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_playMainScene(JNIEnv *env, jclass clazz);
-JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setDebugMuteAudio(JNIEnv *env, jclass clazz, jboolean enabled);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSuspend(JNIEnv *env, jclass clazz, jboolean enabled);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_nextFrame(JNIEnv *env, jclass clazz);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setNodeType(JNIEnv *env, jclass clazz, jint type);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSelectMode(JNIEnv *env, jclass clazz, jint mode);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setSelectionVisible(JNIEnv *env, jclass clazz, jboolean visible);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setCameraManipulateMode(JNIEnv *env, jclass clazz, jint mode);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_resetCamera2DPosition(JNIEnv *env, jclass clazz);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_resetCamera3DPosition(JNIEnv *env, jclass clazz);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_playMainScene(JNIEnv *env, jclass clazz);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_editor_utils_GameMenuUtils_setDebugMuteAudio(JNIEnv *env, jclass clazz, jboolean enabled);
 }

+ 2 - 0
platform/android/java/editor/build.gradle

@@ -180,6 +180,8 @@ android {
 }
 
 dependencies {
+    implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
+
     implementation "androidx.fragment:fragment:$versions.fragmentVersion"
     implementation project(":lib")
 

+ 17 - 2
platform/android/java/editor/src/main/AndroidManifest.xml

@@ -60,6 +60,22 @@
                 <category android:name="android.intent.category.DEFAULT" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
+
+            <!-- Intent filter used to intercept hybrid PANEL launch for the current editor project, and route it
+            properly through the editor 'run' logic (e.g: debugger setup) -->
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="org.godotengine.xr.hybrid.PANEL" />
+            </intent-filter>
+
+            <!-- Intent filter used to intercept hybrid IMMERSIVE launch for the current editor project, and route it
+            properly through the editor 'run' logic (e.g: debugger setup) -->
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="org.godotengine.xr.hybrid.IMMERSIVE" />
+            </intent-filter>
         </activity>
         <activity
             android:name=".GodotGame"
@@ -101,8 +117,7 @@
             android:autoRemoveFromRecents="true"
             android:screenOrientation="landscape"
             android:resizeableActivity="false"
-            android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
-        </activity>
+            android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen" />
 
         <!--
         We remove this meta-data originating from the vendors plugin as we only need the loader for

+ 95 - 23
platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt

@@ -60,14 +60,19 @@ import org.godotengine.editor.utils.verifyApk
 import org.godotengine.godot.BuildConfig
 import org.godotengine.godot.GodotActivity
 import org.godotengine.godot.GodotLib
+import org.godotengine.godot.editor.utils.EditorUtils
+import org.godotengine.godot.editor.utils.GameMenuUtils
+import org.godotengine.godot.editor.utils.GameMenuUtils.GameEmbedMode
+import org.godotengine.godot.editor.utils.GameMenuUtils.fetchGameEmbedMode
 import org.godotengine.godot.error.Error
 import org.godotengine.godot.utils.DialogUtils
-import org.godotengine.godot.utils.GameMenuUtils
-import org.godotengine.godot.utils.GameMenuUtils.GameEmbedMode
-import org.godotengine.godot.utils.GameMenuUtils.fetchGameEmbedMode
 import org.godotengine.godot.utils.PermissionsUtil
 import org.godotengine.godot.utils.ProcessPhoenix
 import org.godotengine.godot.utils.isNativeXRDevice
+import org.godotengine.godot.xr.HybridMode
+import org.godotengine.godot.xr.getHybridAppLaunchMode
+import org.godotengine.godot.xr.HYBRID_APP_PANEL_CATEGORY
+import org.godotengine.godot.xr.HYBRID_APP_IMMERSIVE_CATEGORY
 import kotlin.math.min
 
 /**
@@ -98,6 +103,8 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
 		private const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager"
 		private const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p"
 		internal const val XR_MODE_ARG = "--xr-mode"
+		private const val SCENE_ARG = "--scene"
+		private const val PATH_ARG = "--path"
 
 		// Info for the various classes used by the editor.
 		internal val EDITOR_MAIN_INFO = EditorWindowInfo(GodotEditor::class.java, 777, "")
@@ -236,6 +243,50 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
 		setupGameMenuBar()
 	}
 
+	override fun onNewIntent(newIntent: Intent) {
+		if (newIntent.hasCategory(HYBRID_APP_PANEL_CATEGORY) || newIntent.hasCategory(HYBRID_APP_IMMERSIVE_CATEGORY)) {
+			val params = newIntent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
+			Log.d(TAG, "Received hybrid transition intent $newIntent with parameters ${params.contentToString()}")
+			// Override EXTRA_NEW_LAUNCH so the editor is not restarted
+			newIntent.putExtra(EXTRA_NEW_LAUNCH, false)
+
+			godot?.runOnRenderThread {
+				// Look for the scene and xr-mode arguments
+				var scene = ""
+				var xrMode = XR_MODE_DEFAULT
+				var path = ""
+				if (params != null) {
+					val sceneIndex = params.indexOf(SCENE_ARG)
+					if (sceneIndex != -1 && sceneIndex + 1 < params.size) {
+						scene = params[sceneIndex +1]
+					}
+
+					val xrModeIndex = params.indexOf(XR_MODE_ARG)
+					if (xrModeIndex != -1 && xrModeIndex + 1 < params.size) {
+						xrMode = params[xrModeIndex + 1]
+					}
+
+					val pathIndex = params.indexOf(PATH_ARG)
+					if (pathIndex != -1 && pathIndex + 1 < params.size) {
+						path = params[pathIndex + 1]
+					}
+				}
+
+				val sceneArgs = mutableSetOf(XR_MODE_ARG, xrMode).apply {
+					if (path.isNotEmpty() && scene.isEmpty()) {
+						add(PATH_ARG)
+						add(path)
+					}
+				}
+
+				Log.d(TAG, "Running scene $scene with arguments: $sceneArgs")
+				EditorUtils.runScene(scene, sceneArgs.toTypedArray())
+			}
+		}
+
+		super.onNewIntent(newIntent)
+	}
+
 	protected open fun shouldShowGameMenuBar() = gameMenuContainer != null
 
 	private fun setupGameMenuBar() {
@@ -327,26 +378,41 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
 			}
 		}
 
-		return if (hasEditor) {
-			EDITOR_MAIN_INFO
-		} else {
-			// Launching a game.
-			val openxrEnabled = xrMode == XR_MODE_ON ||
-				(xrMode == XR_MODE_DEFAULT && GodotLib.getGlobal("xr/openxr/enabled").toBoolean())
-			if (openxrEnabled && isNativeXRDevice(applicationContext)) {
-				XR_RUN_GAME_INFO
-			} else {
-				if (godot?.isProjectManagerHint() == true || isNativeXRDevice(applicationContext)) {
+		if (hasEditor) {
+			return EDITOR_MAIN_INFO
+		}
+
+		// Launching a game.
+		if (isNativeXRDevice(applicationContext)) {
+			if (xrMode == XR_MODE_ON) {
+				return XR_RUN_GAME_INFO
+			}
+
+			if ((xrMode == XR_MODE_DEFAULT && GodotLib.getGlobal("xr/openxr/enabled").toBoolean())) {
+				val hybridLaunchMode = getHybridAppLaunchMode()
+
+				return if (hybridLaunchMode == HybridMode.PANEL) {
 					RUN_GAME_INFO
 				} else {
-					val resolvedEmbedMode = resolveGameEmbedModeIfNeeded(gameEmbedMode)
-					if (resolvedEmbedMode == GameEmbedMode.DISABLED) {
-						RUN_GAME_INFO
-					} else {
-						EMBEDDED_RUN_GAME_INFO
-					}
+					XR_RUN_GAME_INFO
 				}
 			}
+
+			// Native XR devices don't support embed mode yet.
+			return RUN_GAME_INFO
+		}
+
+		// Project manager doesn't support embed mode.
+		if (godot?.isProjectManagerHint() == true) {
+			return RUN_GAME_INFO
+		}
+
+		// Check for embed mode launch.
+		val resolvedEmbedMode = resolveGameEmbedModeIfNeeded(gameEmbedMode)
+		return if (resolvedEmbedMode == GameEmbedMode.DISABLED) {
+			RUN_GAME_INFO
+		} else {
+			EMBEDDED_RUN_GAME_INFO
 		}
 	}
 
@@ -626,6 +692,7 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
 		return verifyApk(godot.fileAccessHandler, apkPath)
 	}
 
+	@CallSuper
 	override fun supportsFeature(featureTag: String): Boolean {
 		if (featureTag == "xr_editor") {
 			return isNativeXRDevice(applicationContext)
@@ -639,11 +706,12 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
 			return BuildConfig.FLAVOR == "picoos"
 		}
 
-        return false
+        return super.supportsFeature(featureTag)
     }
 
-	internal fun onEditorConnected(connectedEditorId: Int) {
-		when (connectedEditorId) {
+	internal fun onEditorConnected(editorId: Int) {
+		Log.d(TAG, "Editor $editorId connected!")
+		when (editorId) {
 			EMBEDDED_RUN_GAME_INFO.windowId, RUN_GAME_INFO.windowId -> {
 				runOnUiThread {
 					embeddedGameViewContainerWindow?.isVisible = false
@@ -652,12 +720,16 @@ abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListe
 
 			XR_RUN_GAME_INFO.windowId -> {
 				runOnUiThread {
-					updateEmbeddedGameView(true, false)
+					updateEmbeddedGameView(gameRunning = true, gameEmbedded = false)
 				}
 			}
 		}
 	}
 
+	internal fun onEditorDisconnected(editorId: Int) {
+		Log.d(TAG, "Editor $editorId disconnected!")
+	}
+
 	private fun updateEmbeddedGameView(gameRunning: Boolean, gameEmbedded: Boolean) {
 		if (gameRunning) {
 			embeddedGameStateLabel?.apply {

+ 13 - 1
platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotGame.kt

@@ -35,9 +35,11 @@ import android.util.Log
 import androidx.annotation.CallSuper
 import org.godotengine.godot.Godot
 import org.godotengine.godot.GodotLib
-import org.godotengine.godot.utils.GameMenuUtils
+import org.godotengine.godot.editor.utils.GameMenuUtils
 import org.godotengine.godot.utils.PermissionsUtil
 import org.godotengine.godot.utils.ProcessPhoenix
+import org.godotengine.godot.xr.HYBRID_APP_FEATURE
+import org.godotengine.godot.xr.isHybridAppEnabled
 
 /**
  * Base class for the Godot play windows.
@@ -101,4 +103,14 @@ abstract class BaseGodotGame: GodotEditor() {
 	}
 
 	protected open fun getEditorGameEmbedMode() = GameMenuUtils.GameEmbedMode.AUTO
+
+	@CallSuper
+	override fun supportsFeature(featureTag: String): Boolean {
+		if (HYBRID_APP_FEATURE == featureTag) {
+			// Check if hybrid is enabled
+			return isHybridAppEnabled()
+		}
+
+		return super.supportsFeature(featureTag)
+	}
 }

+ 6 - 3
platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt

@@ -104,7 +104,9 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
 				MSG_REGISTER_MESSENGER -> {
 					val editorId = msg.arg1
 					val messenger = msg.replyTo
-					registerMessenger(editorId, messenger)
+					registerMessenger(editorId, messenger) {
+						editor.onEditorDisconnected(editorId)
+					}
 				}
 
 				MSG_DISPATCH_GAME_MENU_ACTION -> {
@@ -211,8 +213,8 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
 			} else if (messenger.binder.isBinderAlive) {
 				messenger.binder.linkToDeath({
 					Log.v(TAG, "Removing messenger for $editorId")
-					cleanEditorConnection(editorId)
 					messengerDeathCallback?.run()
+					cleanEditorConnection(editorId)
 				}, 0)
 				editorConnectionsInfos[editorId] = EditorConnectionInfo(messenger)
 				editor.onEditorConnected(editorId)
@@ -234,7 +236,8 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
 	/**
 	 * 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.
+	 * This is done so that the host can send request (e.g: force-quit when the host exits) 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.

+ 33 - 1
platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt

@@ -37,12 +37,17 @@ import android.os.Build
 import android.os.Bundle
 import android.util.Log
 import android.view.View
+import androidx.annotation.CallSuper
 import androidx.core.view.isVisible
 import org.godotengine.editor.embed.GameMenuFragment
-import org.godotengine.godot.utils.GameMenuUtils
+import org.godotengine.godot.GodotLib
+import org.godotengine.godot.editor.utils.GameMenuUtils
 import org.godotengine.godot.utils.ProcessPhoenix
 import org.godotengine.godot.utils.isHorizonOSDevice
 import org.godotengine.godot.utils.isNativeXRDevice
+import org.godotengine.godot.xr.HYBRID_APP_PANEL_FEATURE
+import org.godotengine.godot.xr.XRMode
+import org.godotengine.godot.xr.isHybridAppEnabled
 
 /**
  * Drives the 'run project' window of the Godot Editor.
@@ -82,6 +87,18 @@ open class GodotGame : BaseGodotGame() {
 		}
 	}
 
+	override fun getCommandLine(): MutableList<String> {
+		val updatedArgs = super.getCommandLine()
+		if (!updatedArgs.contains(XRMode.REGULAR.cmdLineArg)) {
+			updatedArgs.add(XRMode.REGULAR.cmdLineArg)
+		}
+		if (!updatedArgs.contains(XR_MODE_ARG)) {
+			updatedArgs.add(XR_MODE_ARG)
+			updatedArgs.add("off")
+		}
+		return updatedArgs
+	}
+
 	override fun enterPiPMode() {
 		if (hasPiPSystemFeature()) {
 			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -245,4 +262,19 @@ open class GodotGame : BaseGodotGame() {
 		expandGameMenuButton?.isVisible = shouldShowGameMenuBar() && isMenuBarCollapsable() && collapsed
 	}
 
+	@CallSuper
+	override fun supportsFeature(featureTag: String): Boolean {
+		if (HYBRID_APP_PANEL_FEATURE == featureTag) {
+			// Check if openxr is enabled
+			if (!GodotLib.getGlobal("xr/openxr/enabled").toBoolean()) {
+				return false
+			}
+
+			// Check if hybrid is enabled
+			return isHybridAppEnabled()
+		}
+
+		return super.supportsFeature(featureTag)
+	}
+
 }

+ 1 - 1
platform/android/java/editor/src/main/java/org/godotengine/editor/embed/EmbeddedGodotGame.kt

@@ -40,7 +40,7 @@ import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
 import android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
 import org.godotengine.editor.GodotGame
 import org.godotengine.editor.R
-import org.godotengine.godot.utils.GameMenuUtils
+import org.godotengine.godot.editor.utils.GameMenuUtils
 
 /**
  * Host the Godot game from the editor when the embedded mode is enabled.

+ 1 - 1
platform/android/java/lib/build.gradle

@@ -105,7 +105,7 @@ android {
         }
 
         boolean devBuild = buildType == "dev"
-        boolean debugSymbols = devBuild
+        boolean debugSymbols = devBuild || (buildType == "debug" && isAndroidStudio())
         boolean runTests = devBuild
         boolean storeRelease = buildType == "release"
         boolean productionBuild = storeRelease

+ 1 - 1
platform/android/java/lib/src/org/godotengine/godot/Godot.kt

@@ -1026,7 +1026,7 @@ class Godot private constructor(val context: Context) {
 	 */
 	@Keep
 	private fun hasFeature(feature: String): Boolean {
-		if (primaryHost?.supportsFeature(feature) ?: false) {
+		if (primaryHost?.supportsFeature(feature) == true) {
 			return true;
 		}
 

+ 1 - 1
platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt

@@ -55,7 +55,7 @@ abstract class GodotActivity : FragmentActivity(), GodotHost {
 		private val TAG = GodotActivity::class.java.simpleName
 
 		@JvmStatic
-		protected val EXTRA_COMMAND_LINE_PARAMS = "command_line_params"
+		val EXTRA_COMMAND_LINE_PARAMS = "command_line_params"
 
 		@JvmStatic
 		protected val EXTRA_NEW_LAUNCH = "new_launch_requested"

+ 41 - 0
platform/android/java/lib/src/org/godotengine/godot/editor/utils/EditorUtils.kt

@@ -0,0 +1,41 @@
+/**************************************************************************/
+/*  EditorUtils.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.godot.editor.utils
+
+/**
+ * Utility class for accessing and using editor specific capabilities.
+ *
+ * This class is only functional on editor builds.
+ */
+object EditorUtils {
+	@JvmStatic
+	external fun runScene(scene: String, sceneArgs: Array<String>)
+}

+ 3 - 1
platform/android/java/lib/src/org/godotengine/godot/utils/GameMenuUtils.kt → platform/android/java/lib/src/org/godotengine/godot/editor/utils/GameMenuUtils.kt

@@ -28,13 +28,15 @@
 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
 /**************************************************************************/
 
-package org.godotengine.godot.utils
+package org.godotengine.godot.editor.utils
 
 import android.util.Log
 import org.godotengine.godot.GodotLib
 
 /**
  * Utility class for accessing and using game menu APIs.
+ *
+ * This class is only functional on editor builds.
  */
 object GameMenuUtils {
 	private val TAG = GameMenuUtils::class.java.simpleName

+ 79 - 0
platform/android/java/lib/src/org/godotengine/godot/xr/HybridAppUtils.kt

@@ -0,0 +1,79 @@
+/**************************************************************************/
+/*  HybridAppUtils.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.                 */
+/**************************************************************************/
+
+/**
+ * Contains utility methods and constants for hybrid apps.
+ */
+@file:JvmName("HybridAppUtils")
+
+package org.godotengine.godot.xr
+
+import android.util.Log
+import org.godotengine.godot.GodotLib
+
+private const val TAG = "HybridAppUtils"
+
+enum class HybridMode(private val nativeValue: Int) {
+	NONE( -1),
+	IMMERSIVE(0),
+	PANEL(1);
+
+	companion object {
+		fun fromNative(nativeValue: Int): HybridMode {
+			for (mode in HybridMode.entries) {
+				if (mode.nativeValue == nativeValue) {
+					return mode
+				}
+			}
+			return NONE
+		}
+	}
+}
+
+const val HYBRID_APP_FEATURE = "godot_openxr_hybrid_app"
+const val HYBRID_APP_PANEL_FEATURE = "godot_openxr_panel_app"
+const val HYBRID_APP_PANEL_CATEGORY = "org.godotengine.xr.hybrid.PANEL"
+const val HYBRID_APP_IMMERSIVE_CATEGORY = "org.godotengine.xr.hybrid.IMMERSIVE"
+
+fun isHybridAppEnabled() = GodotLib.getGlobal("xr/hybrid_app/enabled").toBoolean()
+
+fun getHybridAppLaunchMode(): HybridMode {
+	if (!isHybridAppEnabled()) {
+		return HybridMode.NONE
+	}
+
+	try {
+		val launchModeValue = GodotLib.getGlobal("xr/hybrid_app/launch_mode").toInt()
+		return HybridMode.fromNative(launchModeValue)
+	} catch (e: Exception) {
+		Log.w(TAG, "Unable to retrieve 'xr/hybrid_app/launch_mode' project setting", e)
+		return HybridMode.NONE
+	}
+}