Browse Source

Add support for embedding game process in the Android Editor

- Implement Android editor specific `EmbeddedGodotGame` to support embedding the game window in the Android editor
Fredia Huya-Kouadio 8 months ago
parent
commit
7495a8a02e
71 changed files with 2494 additions and 298 deletions
  1. 0 8
      doc/classes/EditorSettings.xml
  2. 7 0
      editor/editor_main_screen.cpp
  3. 2 0
      editor/editor_main_screen.h
  4. 10 7
      editor/editor_settings.cpp
  5. 36 18
      editor/plugins/game_view_plugin.cpp
  6. 12 1
      editor/plugins/game_view_plugin.h
  7. 1 0
      platform/android/SCsub
  8. 127 0
      platform/android/game_menu_utils_jni.cpp
  9. 49 0
      platform/android/game_menu_utils_jni.h
  10. 15 0
      platform/android/java/editor/src/main/AndroidManifest.xml
  11. 369 104
      platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt
  12. 104 0
      platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotGame.kt
  13. 108 9
      platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt
  14. 4 11
      platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt
  15. 137 88
      platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt
  16. 3 1
      platform/android/java/editor/src/main/java/org/godotengine/editor/GodotXRGame.kt
  17. 147 0
      platform/android/java/editor/src/main/java/org/godotengine/editor/embed/EmbeddedGodotGame.kt
  18. 480 0
      platform/android/java/editor/src/main/java/org/godotengine/editor/embed/GameMenuFragment.kt
  19. 9 0
      platform/android/java/editor/src/main/res/color/game_menu_icons_color_state.xml
  20. 5 0
      platform/android/java/editor/src/main/res/drawable/baseline_close_24.xml
  21. 12 0
      platform/android/java/editor/src/main/res/drawable/baseline_expand_less_24.xml
  22. 2 2
      platform/android/java/editor/src/main/res/drawable/baseline_expand_more_48.xml
  23. 5 0
      platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_24.xml
  24. 5 0
      platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_exit_24.xml
  25. 6 0
      platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_selector.xml
  26. 5 0
      platform/android/java/editor/src/main/res/drawable/baseline_minimize_24.xml
  27. 5 0
      platform/android/java/editor/src/main/res/drawable/baseline_picture_in_picture_alt_24.xml
  28. 5 0
      platform/android/java/editor/src/main/res/drawable/baseline_push_pin_24.xml
  29. 9 0
      platform/android/java/editor/src/main/res/drawable/camera.xml
  30. 1 6
      platform/android/java/editor/src/main/res/drawable/expand_more_bg.xml
  31. 7 0
      platform/android/java/editor/src/main/res/drawable/game_menu_button_bg.xml
  32. 6 0
      platform/android/java/editor/src/main/res/drawable/game_menu_message_bg.xml
  33. 10 0
      platform/android/java/editor/src/main/res/drawable/game_menu_selected_bg.xml
  34. 6 0
      platform/android/java/editor/src/main/res/drawable/game_menu_selected_button_bg.xml
  35. 9 0
      platform/android/java/editor/src/main/res/drawable/gui_tab_menu.xml
  36. 9 0
      platform/android/java/editor/src/main/res/drawable/gui_visibility_hidden.xml
  37. 5 0
      platform/android/java/editor/src/main/res/drawable/gui_visibility_selector.xml
  38. 9 0
      platform/android/java/editor/src/main/res/drawable/gui_visibility_visible.xml
  39. 24 0
      platform/android/java/editor/src/main/res/drawable/input_event_joypad_motion.xml
  40. 9 0
      platform/android/java/editor/src/main/res/drawable/list_select.xml
  41. 9 0
      platform/android/java/editor/src/main/res/drawable/next_frame.xml
  42. 11 0
      platform/android/java/editor/src/main/res/drawable/node_3d.xml
  43. 16 0
      platform/android/java/editor/src/main/res/drawable/nodes_2d.xml
  44. 9 0
      platform/android/java/editor/src/main/res/drawable/pause.xml
  45. 5 0
      platform/android/java/editor/src/main/res/drawable/pause_play_selector.xml
  46. 0 9
      platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml
  47. 0 10
      platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml
  48. 9 0
      platform/android/java/editor/src/main/res/drawable/play.xml
  49. 9 0
      platform/android/java/editor/src/main/res/drawable/play_48dp.xml
  50. 9 0
      platform/android/java/editor/src/main/res/drawable/tool_select.xml
  51. 190 0
      platform/android/java/editor/src/main/res/layout/game_menu_fragment_layout.xml
  52. 44 3
      platform/android/java/editor/src/main/res/layout/godot_editor_layout.xml
  53. 23 16
      platform/android/java/editor/src/main/res/layout/godot_game_layout.xml
  54. 11 0
      platform/android/java/editor/src/main/res/layout/godot_xr_game_layout.xml
  55. 56 0
      platform/android/java/editor/src/main/res/menu/options_menu.xml
  56. 8 0
      platform/android/java/editor/src/main/res/values/colors.xml
  57. 4 0
      platform/android/java/editor/src/main/res/values/dimens.xml
  58. 17 1
      platform/android/java/editor/src/main/res/values/strings.xml
  59. 3 0
      platform/android/java/editor/src/main/res/values/themes.xml
  60. 16 1
      platform/android/java/lib/src/org/godotengine/godot/Godot.kt
  61. 7 0
      platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java
  62. 5 0
      platform/android/java/lib/src/org/godotengine/godot/GodotHost.java
  63. 28 0
      platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
  64. 3 3
      platform/android/java/lib/src/org/godotengine/godot/utils/DialogUtils.kt
  65. 117 0
      platform/android/java/lib/src/org/godotengine/godot/utils/GameMenuUtils.kt
  66. 58 0
      platform/android/java_godot_lib_jni.cpp
  67. 5 0
      platform/android/java_godot_lib_jni.h
  68. 11 0
      platform/android/java_godot_wrapper.cpp
  69. 3 0
      platform/android/java_godot_wrapper.h
  70. 30 0
      platform/android/os_android.cpp
  71. 4 0
      platform/android/os_android.h

+ 0 - 8
doc/classes/EditorSettings.xml

@@ -1154,19 +1154,11 @@
 			- [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]Launch in PiP mode[/b] will launch the Play window directly in picture-in-picture (PiP) mode if PiP mode is supported and enabled. When maximized, the Play window will occupy the same window as the Editor.
 			[b]Note:[/b] Only available in the Android editor.
 		</member>
 		<member name="run/window_placement/game_embed_mode" type="int" setter="" getter="">
 			Overrides game embedding setting for all newly opened projects. If enabled, game embedding settings are not saved.
 		</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="">
 			The window mode to use to display the project when starting the project from the editor.
 			[b]Note:[/b] Game embedding is not available for "Force Maximized" or "Force Fullscreen."

+ 7 - 0
editor/editor_main_screen.cpp

@@ -228,6 +228,11 @@ EditorPlugin *EditorMainScreen::get_selected_plugin() const {
 	return selected_plugin;
 }
 
+EditorPlugin *EditorMainScreen::get_plugin_by_name(const String &p_plugin_name) const {
+	ERR_FAIL_COND_V(!main_editor_plugins.has(p_plugin_name), nullptr);
+	return main_editor_plugins[p_plugin_name];
+}
+
 VBoxContainer *EditorMainScreen::get_control() const {
 	return main_screen_vbox;
 }
@@ -254,6 +259,7 @@ void EditorMainScreen::add_main_plugin(EditorPlugin *p_editor) {
 	buttons.push_back(tb);
 	button_hb->add_child(tb);
 	editor_table.push_back(p_editor);
+	main_editor_plugins.insert(p_editor->get_plugin_name(), p_editor);
 }
 
 void EditorMainScreen::remove_main_plugin(EditorPlugin *p_editor) {
@@ -280,6 +286,7 @@ void EditorMainScreen::remove_main_plugin(EditorPlugin *p_editor) {
 	}
 
 	editor_table.erase(p_editor);
+	main_editor_plugins.erase(p_editor->get_plugin_name());
 }
 
 EditorMainScreen::EditorMainScreen() {

+ 2 - 0
editor/editor_main_screen.h

@@ -58,6 +58,7 @@ private:
 	HBoxContainer *button_hb = nullptr;
 	Vector<Button *> buttons;
 	Vector<EditorPlugin *> editor_table;
+	HashMap<String, EditorPlugin *> main_editor_plugins;
 
 	int _get_current_main_editor() const;
 
@@ -80,6 +81,7 @@ public:
 	int get_selected_index() const;
 	int get_plugin_index(EditorPlugin *p_editor) const;
 	EditorPlugin *get_selected_plugin() const;
+	EditorPlugin *get_plugin_by_name(const String &p_plugin_name) const;
 
 	VBoxContainer *get_control() const;
 

+ 10 - 7
editor/editor_settings.cpp

@@ -939,17 +939,20 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
 	_initial_set("run/window_placement/rect_custom_position", Vector2());
 	EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/screen", -5, screen_hints)
 #endif
-	// Should match the ANDROID_WINDOW_* constants in 'platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt'
-	String android_window_hints = "Auto (based on screen size):0,Same as Editor:1,Side-by-side with Editor:2,Launch in PiP mode:3";
+	// Should match the ANDROID_WINDOW_* constants in 'platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt'.
+	String android_window_hints = "Auto (based on screen size):0,Same as Editor:1,Side-by-side with Editor:2";
 	EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/android_window", 0, android_window_hints)
 
-	EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/game_embed_mode", 0, "Use Per-Project Configuration:0,Embed Game:1,Make Game Workspace Floating:2,Disabled:3");
-
-	int default_play_window_pip_mode = 0;
+	String game_embed_mode_hints = "Disabled:-1,Use Per-Project Configuration:0,Embed Game:1,Make Game Workspace Floating:2";
 #ifdef ANDROID_ENABLED
-	default_play_window_pip_mode = 2;
+	if (OS::get_singleton()->has_feature("xr_editor")) {
+		game_embed_mode_hints = "Disabled:-1";
+	} else {
+		game_embed_mode_hints = "Disabled:-1,Auto (based on screen size):0,Enabled:1";
+	}
 #endif
-	EDITOR_SETTING_BASIC(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")
+	int default_game_embed_mode = OS::get_singleton()->has_feature("xr_editor") ? -1 : 0;
+	EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/game_embed_mode", default_game_embed_mode, game_embed_mode_hints);
 
 	// Auto save
 	_initial_set("run/auto_save/save_before_running", true, true);

+ 36 - 18
editor/plugins/game_view_plugin.cpp

@@ -614,6 +614,10 @@ void GameView::_notification(int p_what) {
 				// Embedding available.
 				int game_mode = EDITOR_GET("run/window_placement/game_embed_mode");
 				switch (game_mode) {
+					case -1: { // Disabled.
+						embed_on_play = false;
+						make_floating_on_play = false;
+					} break;
 					case 1: { // Embed.
 						embed_on_play = true;
 						make_floating_on_play = false;
@@ -622,10 +626,6 @@ void GameView::_notification(int p_what) {
 						embed_on_play = true;
 						make_floating_on_play = true;
 					} break;
-					case 3: { // Disabled.
-						embed_on_play = false;
-						make_floating_on_play = false;
-					} break;
 					default: {
 						embed_on_play = EditorSettings::get_singleton()->get_project_metadata("game_view", "embed_on_play", true);
 						make_floating_on_play = EditorSettings::get_singleton()->get_project_metadata("game_view", "make_floating_on_play", true);
@@ -1027,6 +1027,18 @@ GameView::GameView(Ref<GameViewDebugger> p_debugger, WindowWrapper *p_wrapper) {
 
 ///////
 
+void GameViewPlugin::selected_notify() {
+	if (_is_window_wrapper_enabled()) {
+#ifdef ANDROID_ENABLED
+		notify_main_screen_changed(get_plugin_name());
+#else
+		window_wrapper->grab_window_focus();
+#endif
+		_focus_another_editor();
+	}
+}
+
+#ifndef ANDROID_ENABLED
 void GameViewPlugin::make_visible(bool p_visible) {
 	if (p_visible) {
 		window_wrapper->show();
@@ -1035,13 +1047,6 @@ void GameViewPlugin::make_visible(bool p_visible) {
 	}
 }
 
-void GameViewPlugin::selected_notify() {
-	if (window_wrapper->get_window_enabled()) {
-		window_wrapper->grab_window_focus();
-		_focus_another_editor();
-	}
-}
-
 void GameViewPlugin::set_window_layout(Ref<ConfigFile> p_layout) {
 	game_view->set_window_layout(p_layout);
 }
@@ -1058,6 +1063,11 @@ Dictionary GameViewPlugin::get_state() const {
 	return game_view->get_state();
 }
 
+void GameViewPlugin::_window_visibility_changed(bool p_visible) {
+	_focus_another_editor();
+}
+#endif
+
 void GameViewPlugin::_notification(int p_what) {
 	switch (p_what) {
 		case NOTIFICATION_ENTER_TREE: {
@@ -1082,13 +1092,11 @@ void GameViewPlugin::_feature_profile_changed() {
 		debugger->set_is_feature_enabled(is_feature_enabled);
 	}
 
+#ifndef ANDROID_ENABLED
 	if (game_view) {
 		game_view->set_is_feature_enabled(is_feature_enabled);
 	}
-}
-
-void GameViewPlugin::_window_visibility_changed(bool p_visible) {
-	_focus_another_editor();
+#endif
 }
 
 void GameViewPlugin::_save_last_editor(const String &p_editor) {
@@ -1098,7 +1106,7 @@ void GameViewPlugin::_save_last_editor(const String &p_editor) {
 }
 
 void GameViewPlugin::_focus_another_editor() {
-	if (window_wrapper->get_window_enabled()) {
+	if (_is_window_wrapper_enabled()) {
 		if (last_editor.is_empty()) {
 			EditorNode::get_singleton()->get_editor_main_screen()->select(EditorMainScreen::EDITOR_2D);
 		} else {
@@ -1107,13 +1115,22 @@ void GameViewPlugin::_focus_another_editor() {
 	}
 }
 
+bool GameViewPlugin::_is_window_wrapper_enabled() const {
+#ifdef ANDROID_ENABLED
+	return true;
+#else
+	return window_wrapper->get_window_enabled();
+#endif
+}
+
 GameViewPlugin::GameViewPlugin() {
+	debugger.instantiate();
+
+#ifndef ANDROID_ENABLED
 	window_wrapper = memnew(WindowWrapper);
 	window_wrapper->set_window_title(vformat(TTR("%s - Godot Engine"), TTR("Game Workspace")));
 	window_wrapper->set_margins_enabled(true);
 
-	debugger.instantiate();
-
 	game_view = memnew(GameView(debugger, window_wrapper));
 	game_view->set_v_size_flags(Control::SIZE_EXPAND_FILL);
 
@@ -1123,6 +1140,7 @@ GameViewPlugin::GameViewPlugin() {
 	window_wrapper->set_v_size_flags(Control::SIZE_EXPAND_FILL);
 	window_wrapper->hide();
 	window_wrapper->connect("window_visibility_changed", callable_mp(this, &GameViewPlugin::_window_visibility_changed));
+#endif
 
 	EditorFeatureProfileManager::get_singleton()->connect("current_feature_profile_changed", callable_mp(this, &GameViewPlugin::_feature_profile_changed));
 }

+ 12 - 1
editor/plugins/game_view_plugin.h

@@ -32,6 +32,7 @@
 #define GAME_VIEW_PLUGIN_H
 
 #include "editor/debugger/editor_debugger_node.h"
+#include "editor/editor_main_screen.h"
 #include "editor/plugins/editor_debugger_plugin.h"
 #include "editor/plugins/editor_plugin.h"
 #include "scene/debugger/scene_debugger.h"
@@ -208,17 +209,22 @@ public:
 class GameViewPlugin : public EditorPlugin {
 	GDCLASS(GameViewPlugin, EditorPlugin);
 
+#ifndef ANDROID_ENABLED
 	GameView *game_view = nullptr;
 	WindowWrapper *window_wrapper = nullptr;
+#endif
 
 	Ref<GameViewDebugger> debugger;
 
 	String last_editor;
 
 	void _feature_profile_changed();
+#ifndef ANDROID_ENABLED
 	void _window_visibility_changed(bool p_visible);
+#endif
 	void _save_last_editor(const String &p_editor);
 	void _focus_another_editor();
+	bool _is_window_wrapper_enabled() const;
 
 protected:
 	void _notification(int p_what);
@@ -228,14 +234,19 @@ public:
 	bool has_main_screen() const override { return true; }
 	virtual void edit(Object *p_object) override {}
 	virtual bool handles(Object *p_object) const override { return false; }
-	virtual void make_visible(bool p_visible) override;
 	virtual void selected_notify() override;
 
+	Ref<GameViewDebugger> get_debugger() const { return debugger; }
+
+#ifndef ANDROID_ENABLED
+	virtual void make_visible(bool p_visible) override;
+
 	virtual void set_window_layout(Ref<ConfigFile> p_layout) override;
 	virtual void get_window_layout(Ref<ConfigFile> p_layout) override;
 
 	virtual void set_state(const Dictionary &p_state) override;
 	virtual Dictionary get_state() const override;
+#endif
 
 	GameViewPlugin();
 	~GameViewPlugin();

+ 1 - 0
platform/android/SCsub

@@ -30,6 +30,7 @@ android_files = [
     "rendering_context_driver_vulkan_android.cpp",
     "variant/callable_jni.cpp",
     "dialog_utils_jni.cpp",
+    "game_menu_utils_jni.cpp",
 ]
 
 env_android = env.Clone()

+ 127 - 0
platform/android/game_menu_utils_jni.cpp

@@ -0,0 +1,127 @@
+/**************************************************************************/
+/*  game_menu_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 "game_menu_utils_jni.h"
+
+#ifdef TOOLS_ENABLED
+#include "editor/editor_interface.h"
+#include "editor/editor_node.h"
+#include "editor/plugins/game_view_plugin.h"
+#endif
+
+extern "C" {
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSuspend(JNIEnv *env, jclass clazz, jboolean enabled) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->set_suspend(enabled);
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_nextFrame(JNIEnv *env, jclass clazz) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->next_frame();
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setNodeType(JNIEnv *env, jclass clazz, jint type) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->set_node_type(type);
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectMode(JNIEnv *env, jclass clazz, jint mode) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->set_select_mode(mode);
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectionVisible(JNIEnv *env, jclass clazz, jboolean visible) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->set_selection_visible(visible);
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->set_camera_override(enabled);
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraManipulateMode(JNIEnv *env, jclass clazz, jint mode) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->set_camera_manipulate_mode(static_cast<EditorDebuggerNode::CameraOverride>(mode));
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera2DPosition(JNIEnv *env, jclass clazz) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->reset_camera_2d_position();
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera3DPosition(JNIEnv *env, jclass clazz) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->reset_camera_3d_position();
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_playMainScene(JNIEnv *env, jclass clazz) {
+#ifdef TOOLS_ENABLED
+	EditorInterface::get_singleton()->play_main_scene();
+#endif
+}
+}

+ 49 - 0
platform/android/game_menu_utils_jni.h

@@ -0,0 +1,49 @@
+/**************************************************************************/
+/*  game_menu_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.                 */
+/**************************************************************************/
+
+#ifndef GAME_MENU_UTILS_JNI_H
+#define GAME_MENU_UTILS_JNI_H
+
+#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);
+}
+
+#endif // GAME_MENU_UTILS_JNI_H

+ 15 - 0
platform/android/java/editor/src/main/AndroidManifest.xml

@@ -65,12 +65,26 @@
             android:label="@string/godot_game_activity_name"
             android:launchMode="singleTask"
             android:process=":GodotGame"
+            android:autoRemoveFromRecents="true"
             android:supportsPictureInPicture="true"
             android:screenOrientation="userLandscape">
             <layout
                 android:defaultWidth="@dimen/editor_default_window_width"
                 android:defaultHeight="@dimen/editor_default_window_height" />
         </activity>
+        <activity
+            android:name=".embed.EmbeddedGodotGame"
+            android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
+            android:exported="false"
+            android:icon="@mipmap/ic_play_window"
+            android:label="@string/godot_game_activity_name"
+            android:theme="@style/GodotEmbeddedGameTheme"
+            android:taskAffinity=":embed"
+            android:excludeFromRecents="true"
+            android:launchMode="singleTask"
+            android:process=":EmbeddedGodotGame"
+            android:supportsPictureInPicture="true"
+            android:screenOrientation="userLandscape" />
         <activity
             android:name=".GodotXRGame"
             android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
@@ -79,6 +93,7 @@
             android:icon="@mipmap/ic_play_window"
             android:label="@string/godot_game_activity_name"
             android:exported="false"
+            android:autoRemoveFromRecents="true"
             android:screenOrientation="landscape"
             android:resizeableActivity="false"
             android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">

+ 369 - 104
platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt

@@ -38,23 +38,32 @@ import android.content.Context
 import android.content.Intent
 import android.content.pm.PackageManager
 import android.os.*
+import android.preference.PreferenceManager
 import android.util.Log
 import android.view.View
 import android.view.WindowManager
+import android.widget.TextView
 import android.widget.Toast
 import androidx.annotation.CallSuper
+import androidx.core.content.edit
 import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.core.view.isVisible
 import androidx.window.layout.WindowMetricsCalculator
+import org.godotengine.editor.embed.EmbeddedGodotGame
+import org.godotengine.editor.embed.GameMenuFragment
 import org.godotengine.editor.utils.signApk
 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.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 java.util.*
 import kotlin.math.min
 
 /**
@@ -64,32 +73,32 @@ import kotlin.math.min
  * Each derived activity runs in its own process, which enable up to have several instances of
  * the Godot engine up and running at the same time.
  */
-abstract class BaseGodotEditor : GodotActivity() {
+abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListener {
 
 	companion object {
 		private val TAG = BaseGodotEditor::class.java.simpleName
 
 		private const val WAIT_FOR_DEBUGGER = false
 
-		@JvmStatic
-		protected val EXTRA_PIP_AVAILABLE = "pip_available"
-		@JvmStatic
-		protected val EXTRA_LAUNCH_IN_PIP = "launch_in_pip_requested"
+		internal const val EXTRA_EDITOR_HINT = "editor_hint"
+		internal const val EXTRA_PROJECT_MANAGER_HINT = "project_manager_hint"
+		internal const val EXTRA_GAME_MENU_STATE = "game_menu_state"
+		internal const val EXTRA_IS_GAME_EMBEDDED = "is_game_embedded"
+		internal const val EXTRA_IS_GAME_RUNNING = "is_game_running"
 
-		// Command line arguments
+		// Command line arguments.
 		private const val FULLSCREEN_ARG = "--fullscreen"
 		private const val FULLSCREEN_ARG_SHORT = "-f"
-		internal const val EDITOR_ARG = "--editor"
-		internal const val EDITOR_ARG_SHORT = "-e"
-		internal const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager"
-		internal const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p"
-		internal const val BREAKPOINTS_ARG = "--breakpoints"
-		internal const val BREAKPOINTS_ARG_SHORT = "-b"
+		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"
 		internal const val XR_MODE_ARG = "--xr-mode"
 
-		// Info for the various classes used by the editor
+		// 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", LaunchPolicy.AUTO, true)
+		internal val RUN_GAME_INFO = EditorWindowInfo(GodotGame::class.java, 667, ":GodotGame", LaunchPolicy.AUTO)
+		internal val EMBEDDED_RUN_GAME_INFO = EditorWindowInfo(EmbeddedGodotGame::class.java, 2667, ":EmbeddedGodotGame")
 		internal val XR_RUN_GAME_INFO = EditorWindowInfo(GodotXRGame::class.java, 1667, ":GodotXRGame")
 
 		/** Default behavior, means we check project settings **/
@@ -114,22 +123,54 @@ abstract class BaseGodotEditor : 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
-		private const val ANDROID_WINDOW_SAME_AS_EDITOR_AND_LAUNCH_IN_PIP_MODE = 3
 
-		/**
-		 * 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
+		// Game menu constants.
+		internal const val KEY_GAME_MENU_ACTION = "key_game_menu_action"
+		internal const val KEY_GAME_MENU_ACTION_PARAM1 = "key_game_menu_action_param1"
+
+		internal const val GAME_MENU_ACTION_SET_SUSPEND = "setSuspend"
+		internal const val GAME_MENU_ACTION_NEXT_FRAME = "nextFrame"
+		internal const val GAME_MENU_ACTION_SET_NODE_TYPE = "setNodeType"
+		internal const val GAME_MENU_ACTION_SET_SELECT_MODE = "setSelectMode"
+		internal const val GAME_MENU_ACTION_SET_SELECTION_VISIBLE = "setSelectionVisible"
+		internal const val GAME_MENU_ACTION_SET_CAMERA_OVERRIDE = "setCameraOverride"
+		internal const val GAME_MENU_ACTION_SET_CAMERA_MANIPULATE_MODE = "setCameraManipulateMode"
+		internal const val GAME_MENU_ACTION_RESET_CAMERA_2D_POSITION = "resetCamera2DPosition"
+		internal const val GAME_MENU_ACTION_RESET_CAMERA_3D_POSITION = "resetCamera3DPosition"
+		internal const val GAME_MENU_ACTION_EMBED_GAME_ON_PLAY = "embedGameOnPlay"
+
+		private const val GAME_WORKSPACE = "Game"
+
+		internal const val SNACKBAR_SHOW_DURATION_MS = 5000L
+
+		private const val PREF_KEY_DONT_SHOW_GAME_RESUME_HINT = "pref_key_dont_show_game_resume_hint"
 	}
 
-	private val editorMessageDispatcher = EditorMessageDispatcher(this)
+	internal val editorMessageDispatcher = EditorMessageDispatcher(this)
 	private val editorLoadingIndicator: View? by lazy { findViewById(R.id.editor_loading_indicator) }
 
+	private val embeddedGameViewContainerWindow: View? by lazy { findViewById<View?>(R.id.embedded_game_view_container_window)?.apply {
+		setOnClickListener {
+			// Hide the game menu screen overlay.
+			it.isVisible = false
+		}
+
+		// Prevent the game menu screen overlay from hiding when clicking inside of the panel bounds.
+		findViewById<View?>(R.id.embedded_game_view_container)?.isClickable = true
+	} }
+	private val embeddedGameStateLabel: TextView? by lazy { findViewById<TextView?>(R.id.embedded_game_state_label)?.apply {
+		setOnClickListener {
+			godot?.runOnRenderThread {
+				GameMenuUtils.playMainScene()
+			}
+		}
+	} }
+	protected val gameMenuContainer: View? by lazy {
+		findViewById(R.id.game_menu_fragment_container)
+	}
+	protected var gameMenuFragment: GameMenuFragment? = null
+	protected val gameMenuState = Bundle()
+
 	override fun getGodotAppLayout() = R.layout.godot_editor_layout
 
 	internal open fun getEditorWindowInfo() = EDITOR_MAIN_INFO
@@ -187,6 +228,30 @@ abstract class BaseGodotEditor : GodotActivity() {
 		}
 
 		super.onCreate(savedInstanceState)
+
+		// Add the game menu bar.
+		setupGameMenuBar()
+	}
+
+	protected open fun shouldShowGameMenuBar() = gameMenuContainer != null
+
+	private fun setupGameMenuBar() {
+		if (shouldShowGameMenuBar()) {
+			var currentFragment = supportFragmentManager.findFragmentById(R.id.game_menu_fragment_container)
+			if (currentFragment !is GameMenuFragment) {
+				Log.v(TAG, "Creating game menu fragment instance")
+				currentFragment = GameMenuFragment().apply {
+					arguments = Bundle().apply {
+						putBundle(EXTRA_GAME_MENU_STATE, gameMenuState)
+					}
+				}
+				supportFragmentManager.beginTransaction()
+					.replace(R.id.game_menu_fragment_container, currentFragment, GameMenuFragment.TAG)
+					.commitNowAllowingStateLoss()
+			}
+
+			gameMenuFragment = currentFragment
+		}
 	}
 
 	override fun onGodotSetupCompleted() {
@@ -211,8 +276,32 @@ abstract class BaseGodotEditor : GodotActivity() {
 		}
 	}
 
+	override fun onResume() {
+		super.onResume()
+		if (getEditorWindowInfo() == EDITOR_MAIN_INFO &&
+			godot?.isEditorHint() == true &&
+			(editorMessageDispatcher.hasEditorConnection(EMBEDDED_RUN_GAME_INFO) ||
+				editorMessageDispatcher.hasEditorConnection(RUN_GAME_INFO))) {
+			// If this is the editor window, and this is not the project manager, and we have a running game, then show
+			// a hint for how to resume the playing game.
+			val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
+			if (!sharedPrefs.getBoolean(PREF_KEY_DONT_SHOW_GAME_RESUME_HINT, false)) {
+				DialogUtils.showSnackbar(
+					this,
+					getString(R.string.show_game_resume_hint),
+					SNACKBAR_SHOW_DURATION_MS,
+					getString(R.string.dont_show_again_message)
+				) {
+					sharedPrefs.edit {
+						putBoolean(PREF_KEY_DONT_SHOW_GAME_RESUME_HINT, true)
+					}
+				}
+			}
+		}
+	}
+
 	@CallSuper
-	protected override fun updateCommandLineParams(args: Array<String>) {
+	override fun updateCommandLineParams(args: Array<String>) {
 		val args = if (BuildConfig.BUILD_TYPE == "dev") {
 			args + "--benchmark"
 		} else {
@@ -221,7 +310,7 @@ abstract class BaseGodotEditor : GodotActivity() {
 		super.updateCommandLineParams(args);
 	}
 
-	protected open fun retrieveEditorWindowInfo(args: Array<String>): EditorWindowInfo {
+	protected fun retrieveEditorWindowInfo(args: Array<String>, gameEmbedMode: GameEmbedMode): EditorWindowInfo {
 		var hasEditor = false
 		var xrMode = XR_MODE_DEFAULT
 
@@ -238,12 +327,22 @@ abstract class BaseGodotEditor : GodotActivity() {
 		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 {
-				RUN_GAME_INFO
+				if (godot?.isProjectManagerHint() == true || isNativeXRDevice(applicationContext)) {
+					RUN_GAME_INFO
+				} else {
+					val resolvedEmbedMode = resolveGameEmbedModeIfNeeded(gameEmbedMode)
+					if (resolvedEmbedMode == GameEmbedMode.DISABLED) {
+						RUN_GAME_INFO
+					} else {
+						EMBEDDED_RUN_GAME_INFO
+					}
+				}
 			}
 		}
 	}
@@ -253,20 +352,21 @@ abstract class BaseGodotEditor : GodotActivity() {
 			RUN_GAME_INFO.windowId -> RUN_GAME_INFO
 			EDITOR_MAIN_INFO.windowId -> EDITOR_MAIN_INFO
 			XR_RUN_GAME_INFO.windowId -> XR_RUN_GAME_INFO
+			EMBEDDED_RUN_GAME_INFO.windowId -> EMBEDDED_RUN_GAME_INFO
 			else -> null
 		}
 	}
 
 	protected fun getNewGodotInstanceIntent(editorWindowInfo: EditorWindowInfo, args: Array<String>): Intent {
+		// If we're launching an editor window (project manager or editor) and we're in
+		// fullscreen mode, we want to remain in fullscreen mode.
+		// This doesn't apply to the play / game window since for that window fullscreen is
+		// controlled by the game logic.
 		val updatedArgs = if (editorWindowInfo == EDITOR_MAIN_INFO &&
 			godot?.isInImmersiveMode() == true &&
 			!args.contains(FULLSCREEN_ARG) &&
 			!args.contains(FULLSCREEN_ARG_SHORT)
 		) {
-			// If we're launching an editor window (project manager or editor) and we're in
-			// fullscreen mode, we want to remain in fullscreen mode.
-			// This doesn't apply to the play / game window since for that window fullscreen is
-			// controlled by the game logic.
 			args + FULLSCREEN_ARG
 		} else {
 			args
@@ -278,40 +378,28 @@ abstract class BaseGodotEditor : GodotActivity() {
 			.putExtra(EXTRA_COMMAND_LINE_PARAMS, updatedArgs)
 
 		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 || launchPolicy == LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE))
-		} else {
-			false
-		}
-		newInstance.putExtra(EXTRA_PIP_AVAILABLE, isPiPAvailable)
-
-		var launchInPiP = false
 		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) {
-			launchInPiP = isPiPAvailable &&
-				(updatedArgs.contains(BREAKPOINTS_ARG) || updatedArgs.contains(BREAKPOINTS_ARG_SHORT))
-		} else if (launchPolicy == LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE) {
-			launchInPiP = isPiPAvailable
-		}
-
-		if (launchInPiP) {
-			Log.v(TAG, "Launching in PiP mode")
-			newInstance.putExtra(EXTRA_LAUNCH_IN_PIP, launchInPiP)
 		}
 		return newInstance
 	}
 
-	override fun onNewGodotInstanceRequested(args: Array<String>): Int {
-		val editorWindowInfo = retrieveEditorWindowInfo(args)
+	final override fun onNewGodotInstanceRequested(args: Array<String>): Int {
+		val editorWindowInfo = retrieveEditorWindowInfo(args, fetchGameEmbedMode())
+
+		// Check if this editor window is being terminated. If it's, delay the creation of a new instance until the
+		// termination is complete.
+		if (editorMessageDispatcher.isPendingForceQuit(editorWindowInfo)) {
+			Log.v(TAG, "Scheduling new launch after termination of ${editorWindowInfo.windowId}")
+			editorMessageDispatcher.runTaskAfterForceQuit(editorWindowInfo) {
+				onNewGodotInstanceRequested(args)
+			}
+			return editorWindowInfo.windowId
+		}
 
-		// Launch a new activity
 		val sourceView = godotFragment?.view
 		val activityOptions = if (sourceView == null) {
 			null
@@ -322,6 +410,12 @@ abstract class BaseGodotEditor : GodotActivity() {
 		}
 
 		val newInstance = getNewGodotInstanceIntent(editorWindowInfo, args)
+		newInstance.apply {
+			putExtra(EXTRA_EDITOR_HINT, godot?.isEditorHint() == true)
+			putExtra(EXTRA_PROJECT_MANAGER_HINT, godot?.isProjectManagerHint() == true)
+			putExtra(EXTRA_GAME_MENU_STATE, gameMenuState)
+		}
+
 		if (editorWindowInfo.windowClassName == javaClass.name) {
 			Log.d(TAG, "Restarting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}")
 			triggerRebirth(activityOptions?.toBundle(), newInstance)
@@ -344,7 +438,7 @@ abstract class BaseGodotEditor : GodotActivity() {
 		}
 
 		// Send an inter-process message to request the target editor window to force quit.
-		if (editorMessageDispatcher.requestForceQuit(editorWindowInfo.windowId)) {
+		if (editorMessageDispatcher.requestForceQuit(editorWindowInfo)) {
 			return true
 		}
 
@@ -402,58 +496,57 @@ abstract class BaseGodotEditor : GodotActivity() {
 	protected open fun enablePanAndScaleGestures() =
 		java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/enable_pan_and_scale_gestures"))
 
-	/**
-	 * Retrieves the play window pip mode editor setting.
-	 */
-	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
+	private fun resolveGameEmbedModeIfNeeded(embedMode: GameEmbedMode): GameEmbedMode {
+		return when (embedMode) {
+			GameEmbedMode.AUTO -> {
+				val inMultiWindowMode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+					isInMultiWindowMode
+				} else {
+					false
+				}
+				if (inMultiWindowMode || isLargeScreen || isNativeXRDevice(applicationContext)) {
+					GameEmbedMode.DISABLED
+				} else {
+					GameEmbedMode.ENABLED
+				}
+			}
+
+			else -> embedMode
 		}
 	}
 
 	/**
 	 * 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.SAME_AND_LAUNCH_IN_PIP_MODE] 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 -> {
-				if (isNativeXRDevice(applicationContext)) {
-					// Native XR devices are more desktop-like and have support for launching adjacent
-					// windows. So we always want to launch in adjacent mode when auto is selected.
+				val inMultiWindowMode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+					isInMultiWindowMode
+				} else {
+					false
+				}
+				val defaultLaunchPolicy = if (inMultiWindowMode || isLargeScreen || isNativeXRDevice(applicationContext)) {
 					LaunchPolicy.ADJACENT
 				} else {
-					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
-							ANDROID_WINDOW_SAME_AS_EDITOR_AND_LAUNCH_IN_PIP_MODE -> LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE
-							else -> {
-								// ANDROID_WINDOW_AUTO
-								defaultLaunchPolicy
-							}
+					LaunchPolicy.SAME
+				}
+
+				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
 					}
+				} catch (e: NumberFormatException) {
+					Log.w(TAG, "Error parsing the Android window placement editor setting", e)
+					// Fall-back to the default launch policy.
+					defaultLaunchPolicy
 				}
 			}
 
@@ -463,14 +556,6 @@ abstract class BaseGodotEditor : GodotActivity() {
 		}
 	}
 
-	/**
-	 * Returns true the if the device supports picture-in-picture (PiP)
-	 */
-	protected open fun hasPiPSystemFeature(): Boolean {
-		return 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
@@ -558,4 +643,184 @@ abstract class BaseGodotEditor : GodotActivity() {
 
         return false
     }
+
+	internal fun onEditorConnected(connectedEditorId: Int) {
+		when (connectedEditorId) {
+			EMBEDDED_RUN_GAME_INFO.windowId, RUN_GAME_INFO.windowId -> {
+				runOnUiThread {
+					embeddedGameViewContainerWindow?.isVisible = false
+				}
+			}
+
+			XR_RUN_GAME_INFO.windowId -> {
+				runOnUiThread {
+					updateEmbeddedGameView(true, false)
+				}
+			}
+		}
+	}
+
+	private fun updateEmbeddedGameView(gameRunning: Boolean, gameEmbedded: Boolean) {
+		if (gameRunning) {
+			embeddedGameStateLabel?.apply {
+				setText(R.string.running_game_not_embedded_message)
+				setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
+				isClickable = false
+			}
+		} else {
+			embeddedGameStateLabel?.apply{
+				setText(R.string.embedded_game_not_running_message)
+				setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, R.drawable.play_48dp)
+				isClickable = true
+			}
+		}
+
+		gameMenuState.putBoolean(EXTRA_IS_GAME_EMBEDDED, gameEmbedded)
+		gameMenuState.putBoolean(EXTRA_IS_GAME_RUNNING, gameRunning)
+		gameMenuFragment?.refreshGameMenu(gameMenuState)
+	}
+
+	override fun onEditorWorkspaceSelected(workspace: String) {
+		if (workspace == GAME_WORKSPACE && shouldShowGameMenuBar()) {
+			if (editorMessageDispatcher.bringEditorWindowToFront(EMBEDDED_RUN_GAME_INFO) || editorMessageDispatcher.bringEditorWindowToFront(RUN_GAME_INFO)) {
+				return
+			}
+
+			val xrGameRunning = editorMessageDispatcher.hasEditorConnection(XR_RUN_GAME_INFO)
+			val gameEmbedMode = resolveGameEmbedModeIfNeeded(fetchGameEmbedMode())
+			runOnUiThread {
+				updateEmbeddedGameView(xrGameRunning, gameEmbedMode != GameEmbedMode.DISABLED)
+				embeddedGameViewContainerWindow?.isVisible = true
+			}
+		}
+	}
+
+	internal open fun bringSelfToFront() {
+		runOnUiThread {
+			Log.v(TAG, "Bringing self to front")
+			val relaunchIntent = Intent(intent)
+			// Don't restart.
+			relaunchIntent.putExtra(EXTRA_NEW_LAUNCH, false)
+			startActivity(relaunchIntent)
+		}
+	}
+
+	internal fun parseGameMenuAction(actionData: Bundle) {
+		val action = actionData.getString(KEY_GAME_MENU_ACTION) ?: return
+		when (action) {
+			GAME_MENU_ACTION_SET_SUSPEND -> {
+				val suspended = actionData.getBoolean(KEY_GAME_MENU_ACTION_PARAM1)
+				suspendGame(suspended)
+			}
+			GAME_MENU_ACTION_NEXT_FRAME -> {
+				dispatchNextFrame()
+			}
+			GAME_MENU_ACTION_SET_NODE_TYPE -> {
+				val nodeType = actionData.getSerializable(KEY_GAME_MENU_ACTION_PARAM1) as GameMenuFragment.GameMenuListener.NodeType?
+				if (nodeType != null) {
+					selectRuntimeNode(nodeType)
+				}
+			}
+			GAME_MENU_ACTION_SET_SELECTION_VISIBLE -> {
+				val enabled = actionData.getBoolean(KEY_GAME_MENU_ACTION_PARAM1)
+				toggleSelectionVisibility(enabled)
+			}
+			GAME_MENU_ACTION_SET_CAMERA_OVERRIDE -> {
+				val enabled = actionData.getBoolean(KEY_GAME_MENU_ACTION_PARAM1)
+				overrideCamera(enabled)
+			}
+			GAME_MENU_ACTION_SET_SELECT_MODE -> {
+				val selectMode = actionData.getSerializable(KEY_GAME_MENU_ACTION_PARAM1) as GameMenuFragment.GameMenuListener.SelectMode?
+				if (selectMode != null) {
+					selectRuntimeNodeSelectMode(selectMode)
+				}
+			}
+			GAME_MENU_ACTION_RESET_CAMERA_2D_POSITION -> {
+				reset2DCamera()
+			}
+			GAME_MENU_ACTION_RESET_CAMERA_3D_POSITION -> {
+				reset3DCamera()
+			}
+			GAME_MENU_ACTION_SET_CAMERA_MANIPULATE_MODE -> {
+				val mode = actionData.getSerializable(KEY_GAME_MENU_ACTION_PARAM1) as? GameMenuFragment.GameMenuListener.CameraMode?
+				if (mode != null) {
+					manipulateCamera(mode)
+				}
+			}
+			GAME_MENU_ACTION_EMBED_GAME_ON_PLAY -> {
+				val embedded = actionData.getBoolean(KEY_GAME_MENU_ACTION_PARAM1)
+				embedGameOnPlay(embedded)
+			}
+		}
+	}
+
+	override fun suspendGame(suspended: Boolean) {
+		gameMenuState.putBoolean(GAME_MENU_ACTION_SET_SUSPEND, suspended)
+		godot?.runOnRenderThread {
+			GameMenuUtils.setSuspend(suspended)
+		}
+	}
+
+	override fun dispatchNextFrame() {
+		godot?.runOnRenderThread {
+			GameMenuUtils.nextFrame()
+		}
+	}
+
+	override fun toggleSelectionVisibility(enabled: Boolean) {
+		gameMenuState.putBoolean(GAME_MENU_ACTION_SET_SELECTION_VISIBLE, enabled)
+		godot?.runOnRenderThread {
+			GameMenuUtils.setSelectionVisible(enabled)
+		}
+	}
+
+	override fun overrideCamera(enabled: Boolean) {
+		gameMenuState.putBoolean(GAME_MENU_ACTION_SET_CAMERA_OVERRIDE, enabled)
+		godot?.runOnRenderThread {
+			GameMenuUtils.setCameraOverride(enabled)
+		}
+	}
+
+	override fun selectRuntimeNode(nodeType: GameMenuFragment.GameMenuListener.NodeType) {
+		gameMenuState.putSerializable(GAME_MENU_ACTION_SET_NODE_TYPE, nodeType)
+		godot?.runOnRenderThread {
+			GameMenuUtils.setNodeType(nodeType.ordinal)
+		}
+	}
+
+	override fun selectRuntimeNodeSelectMode(selectMode: GameMenuFragment.GameMenuListener.SelectMode) {
+		gameMenuState.putSerializable(GAME_MENU_ACTION_SET_SELECT_MODE, selectMode)
+		godot?.runOnRenderThread {
+			GameMenuUtils.setSelectMode(selectMode.ordinal)
+		}
+	}
+
+	override fun reset2DCamera() {
+		godot?.runOnRenderThread {
+			GameMenuUtils.resetCamera2DPosition()
+		}
+	}
+
+	override fun reset3DCamera() {
+		godot?.runOnRenderThread {
+			GameMenuUtils.resetCamera3DPosition()
+		}
+	}
+
+	override fun manipulateCamera(mode: GameMenuFragment.GameMenuListener.CameraMode) {
+		gameMenuState.putSerializable(GAME_MENU_ACTION_SET_CAMERA_MANIPULATE_MODE, mode)
+		godot?.runOnRenderThread {
+			GameMenuUtils.setCameraManipulateMode(mode.ordinal)
+		}
+	}
+
+	override fun embedGameOnPlay(embedded: Boolean) {
+		gameMenuState.putBoolean(GAME_MENU_ACTION_EMBED_GAME_ON_PLAY, embedded)
+		godot?.runOnRenderThread {
+			val gameEmbedMode = if (embedded) GameEmbedMode.ENABLED else GameEmbedMode.DISABLED
+			GameMenuUtils.saveGameEmbedMode(gameEmbedMode)
+		}
+	}
+
+	override fun isGameEmbeddingSupported() = !isNativeXRDevice(applicationContext)
 }

+ 104 - 0
platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotGame.kt

@@ -0,0 +1,104 @@
+/**************************************************************************/
+/*  BaseGodotGame.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.Manifest
+import android.util.Log
+import androidx.annotation.CallSuper
+import org.godotengine.godot.GodotLib
+import org.godotengine.godot.utils.GameMenuUtils
+import org.godotengine.godot.utils.PermissionsUtil
+import org.godotengine.godot.utils.ProcessPhoenix
+
+/**
+ * Base class for the Godot play windows.
+ */
+abstract class BaseGodotGame: GodotEditor() {
+	companion object {
+		private val TAG = BaseGodotGame::class.java.simpleName
+	}
+
+	override fun enableLongPressGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click"))
+
+	override fun enablePanAndScaleGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures"))
+
+	override fun onGodotSetupCompleted() {
+		super.onGodotSetupCompleted()
+		Log.v(TAG, "OnGodotSetupCompleted")
+
+		// Check if we should be running in XR instead (if available) as it's possible we were
+		// launched from the project manager which doesn't have that information.
+		val launchingArgs = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
+		if (launchingArgs != null) {
+			val editorWindowInfo = retrieveEditorWindowInfo(launchingArgs, getEditorGameEmbedMode())
+			if (editorWindowInfo != getEditorWindowInfo()) {
+				val relaunchIntent = getNewGodotInstanceIntent(editorWindowInfo, launchingArgs)
+				relaunchIntent.putExtra(EXTRA_NEW_LAUNCH, true)
+					.putExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD, intent.getBundleExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD))
+
+				Log.d(TAG, "Relaunching XR project using ${editorWindowInfo.windowClassName} with parameters ${launchingArgs.contentToString()}")
+				val godot = godot
+				if (godot != null) {
+					godot.destroyAndKillProcess {
+						ProcessPhoenix.triggerRebirth(this, relaunchIntent)
+					}
+				} else {
+					ProcessPhoenix.triggerRebirth(this, relaunchIntent)
+				}
+				return
+			}
+		}
+
+		// Request project runtime permissions if necessary.
+		val permissionsToEnable = getProjectPermissionsToEnable()
+		if (permissionsToEnable.isNotEmpty()) {
+			PermissionsUtil.requestPermissions(this, permissionsToEnable)
+		}
+	}
+
+	/**
+	 * Check for project permissions to enable.
+	 */
+	@CallSuper
+	protected open fun getProjectPermissionsToEnable(): MutableList<String> {
+		val permissionsToEnable = mutableListOf<String>()
+
+		// Check for RECORD_AUDIO permission.
+		val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input"))
+		if (audioInputEnabled) {
+			permissionsToEnable.add(Manifest.permission.RECORD_AUDIO)
+		}
+
+		return permissionsToEnable
+	}
+
+	protected open fun getEditorGameEmbedMode() = GameMenuUtils.GameEmbedMode.AUTO
+}

+ 108 - 9
platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt

@@ -73,15 +73,33 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
 		 * Requests the recipient to store the passed [android.os.Messenger] instance.
 		 */
 		private const val MSG_REGISTER_MESSENGER = 1
+
+		/**
+		 * Requests the recipient to dispatch the given game menu action.
+		 */
+		private const val MSG_DISPATCH_GAME_MENU_ACTION = 2
+
+		/**
+		 * Requests the recipient resumes itself / brings itself to front.
+		 */
+		private const val MSG_BRING_SELF_TO_FRONT = 3
 	}
 
-	private val recipientsMessengers = ConcurrentHashMap<Int, Messenger>()
+	private data class EditorConnectionInfo(
+		val messenger: Messenger,
+		var pendingForceQuit: Boolean = false,
+		val scheduledTasksPendingForceQuit: HashSet<Runnable> = HashSet()
+	)
+	private val editorConnectionsInfos = ConcurrentHashMap<Int, EditorConnectionInfo>()
 
 	@SuppressLint("HandlerLeak")
 	private val dispatcherHandler = object : Handler() {
 		override fun handleMessage(msg: Message) {
 			when (msg.what) {
-				MSG_FORCE_QUIT -> editor.finish()
+				MSG_FORCE_QUIT -> {
+					Log.v(TAG, "Force quitting ${editor.getEditorWindowInfo().windowId}")
+					editor.finishAndRemoveTask()
+				}
 
 				MSG_REGISTER_MESSENGER -> {
 					val editorId = msg.arg1
@@ -89,28 +107,100 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
 					registerMessenger(editorId, messenger)
 				}
 
+				MSG_DISPATCH_GAME_MENU_ACTION -> {
+					val actionData = msg.data
+					if (actionData != null) {
+						editor.parseGameMenuAction(actionData)
+					}
+				}
+
+				MSG_BRING_SELF_TO_FRONT -> editor.bringSelfToFront()
+
 				else -> super.handleMessage(msg)
 			}
 		}
 	}
 
+	fun hasEditorConnection(editorWindow: EditorWindowInfo) = editorConnectionsInfos.containsKey(editorWindow.windowId)
+
 	/**
-	 * Request the window with the given [editorId] to force quit.
+	 * Request the window with the given [editorWindow] to force quit.
 	 */
-	fun requestForceQuit(editorId: Int): Boolean {
-		val messenger = recipientsMessengers[editorId] ?: return false
+	fun requestForceQuit(editorWindow: EditorWindowInfo): Boolean {
+		val editorId = editorWindow.windowId
+		val info = editorConnectionsInfos[editorId] ?: return false
+		if (info.pendingForceQuit) {
+			return true
+		}
+
+		val messenger = info.messenger
 		return try {
 			Log.v(TAG, "Requesting 'forceQuit' for $editorId")
 			val msg = Message.obtain(null, MSG_FORCE_QUIT)
 			messenger.send(msg)
+			info.pendingForceQuit = true
+
 			true
 		} catch (e: RemoteException) {
 			Log.e(TAG, "Error requesting 'forceQuit' to $editorId", e)
-			recipientsMessengers.remove(editorId)
+			cleanEditorConnection(editorId)
+			false
+		}
+	}
+
+	internal fun isPendingForceQuit(editorWindow: EditorWindowInfo): Boolean {
+		return editorConnectionsInfos[editorWindow.windowId]?.pendingForceQuit == true
+	}
+
+	internal fun runTaskAfterForceQuit(editorWindow: EditorWindowInfo, task: Runnable) {
+		val connectionInfo = editorConnectionsInfos[editorWindow.windowId]
+		if (connectionInfo == null || !connectionInfo.pendingForceQuit) {
+			task.run()
+		} else {
+			connectionInfo.scheduledTasksPendingForceQuit.add(task)
+		}
+	}
+
+	/**
+	 * Request the given [editorWindow] to bring itself to front / resume itself.
+	 *
+	 * Returns true if the request was successfully dispatched, false otherwise.
+	 */
+	fun bringEditorWindowToFront(editorWindow: EditorWindowInfo): Boolean {
+		val editorId = editorWindow.windowId
+		val info = editorConnectionsInfos[editorId] ?: return false
+		val messenger = info.messenger
+		return try {
+			Log.v(TAG, "Requesting 'bringSelfToFront' for $editorId")
+			val msg = Message.obtain(null, MSG_BRING_SELF_TO_FRONT)
+			messenger.send(msg)
+			true
+		} catch (e: RemoteException) {
+			Log.e(TAG, "Error requesting 'bringSelfToFront' to $editorId", e)
+			cleanEditorConnection(editorId)
 			false
 		}
 	}
 
+	/**
+	 * Dispatch a game menu action to another editor instance.
+	 */
+	fun dispatchGameMenuAction(editorWindow: EditorWindowInfo, actionData: Bundle) {
+		val editorId = editorWindow.windowId
+		val info = editorConnectionsInfos[editorId] ?: return
+		val messenger = info.messenger
+		try {
+			Log.d(TAG, "Dispatch game menu action to $editorId")
+			val msg = Message.obtain(null, MSG_DISPATCH_GAME_MENU_ACTION).apply {
+				data = actionData
+			}
+			messenger.send(msg)
+		} catch (e: RemoteException) {
+			Log.e(TAG, "Error dispatching game menu action to $editorId", e)
+			cleanEditorConnection(editorId)
+		}
+	}
+
 	/**
 	 * Utility method to register a receiver messenger.
 	 */
@@ -121,14 +211,23 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
 			} else if (messenger.binder.isBinderAlive) {
 				messenger.binder.linkToDeath({
 					Log.v(TAG, "Removing messenger for $editorId")
-					recipientsMessengers.remove(editorId)
+					cleanEditorConnection(editorId)
 					messengerDeathCallback?.run()
 				}, 0)
-				recipientsMessengers[editorId] = messenger
+				editorConnectionsInfos[editorId] = EditorConnectionInfo(messenger)
+				editor.onEditorConnected(editorId)
 			}
 		} catch (e: RemoteException) {
 			Log.e(TAG, "Unable to register messenger from $editorId", e)
-			recipientsMessengers.remove(editorId)
+			cleanEditorConnection(editorId)
+		}
+	}
+
+	private fun cleanEditorConnection(editorId: Int) {
+		val connectionInfo = editorConnectionsInfos.remove(editorId) ?: return
+		Log.v(TAG, "Cleaning info for recipient $editorId")
+		for (task in connectionInfo.scheduledTasksPendingForceQuit) {
+			task.run()
 		}
 	}
 

+ 4 - 11
platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt

@@ -48,12 +48,7 @@ enum class LaunchPolicy {
 	/**
 	 * Adjacent launches are enabled.
 	 */
-	ADJACENT,
-
-	/**
-	 * Launches happen in the same window but start in PiP mode.
-	 */
-	SAME_AND_LAUNCH_IN_PIP_MODE
+	ADJACENT
 }
 
 /**
@@ -63,14 +58,12 @@ data class EditorWindowInfo(
 	val windowClassName: String,
 	val windowId: Int,
 	val processNameSuffix: String,
-	val launchPolicy: LaunchPolicy = LaunchPolicy.SAME,
-	val supportsPiPMode: Boolean = false
+	val launchPolicy: LaunchPolicy = LaunchPolicy.SAME
 ) {
 	constructor(
 		windowClass: Class<*>,
 		windowId: Int,
 		processNameSuffix: String,
-		launchPolicy: LaunchPolicy = LaunchPolicy.SAME,
-		supportsPiPMode: Boolean = false
-	) : this(windowClass.name, windowId, processNameSuffix, launchPolicy, supportsPiPMode)
+		launchPolicy: LaunchPolicy = LaunchPolicy.SAME
+	) : this(windowClass.name, windowId, processNameSuffix, launchPolicy)
 }

+ 137 - 88
platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt

@@ -30,77 +30,59 @@
 
 package org.godotengine.editor
 
-import android.Manifest
-import android.annotation.SuppressLint
 import android.app.PictureInPictureParams
-import android.content.Intent
+import android.content.pm.PackageManager
 import android.graphics.Rect
 import android.os.Build
 import android.os.Bundle
 import android.util.Log
 import android.view.View
-import androidx.annotation.CallSuper
-import org.godotengine.godot.GodotLib
-import org.godotengine.godot.utils.PermissionsUtil
+import androidx.core.view.isVisible
+import org.godotengine.editor.embed.GameMenuFragment
+import org.godotengine.godot.utils.GameMenuUtils
 import org.godotengine.godot.utils.ProcessPhoenix
+import org.godotengine.godot.utils.isNativeXRDevice
 
 /**
  * Drives the 'run project' window of the Godot Editor.
  */
-open class GodotGame : GodotEditor() {
+open class GodotGame : BaseGodotGame() {
 
 	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
+	private val expandGameMenuButton: View? by lazy { findViewById(R.id.game_menu_expand_button) }
 
-	@SuppressLint("ClickableViewAccessibility")
 	override fun onCreate(savedInstanceState: Bundle?) {
+		gameMenuState.clear()
+		intent.getBundleExtra(EXTRA_GAME_MENU_STATE)?.let {
+			gameMenuState.putAll(it)
+		}
+		gameMenuState.putBoolean(EXTRA_IS_GAME_EMBEDDED, isGameEmbedded())
+		gameMenuState.putBoolean(EXTRA_IS_GAME_RUNNING, true)
+
 		super.onCreate(savedInstanceState)
 
+		gameMenuContainer?.isVisible = shouldShowGameMenuBar()
+		expandGameMenuButton?.apply{
+			isVisible = shouldShowGameMenuBar() && isMenuBarCollapsable()
+			setOnClickListener {
+				gameMenuFragment?.expandGameMenu()
+			}
+		}
+
 		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) {
+	override fun enterPiPMode() {
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && hasPiPSystemFeature()) {
 			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
 				val builder = PictureInPictureParams.Builder().setSourceRectHint(gameViewSourceRectHint)
 				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -114,10 +96,27 @@ open class GodotGame : GodotEditor() {
 		}
 	}
 
+	/**
+	 * Returns true the if the device supports picture-in-picture (PiP).
+	 */
+	protected fun hasPiPSystemFeature(): Boolean {
+		return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
+			packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
+	}
+
+	override fun shouldShowGameMenuBar(): Boolean {
+		return intent.getBooleanExtra(
+			EXTRA_EDITOR_HINT,
+			false
+		) && gameMenuContainer != null
+	}
+
 	override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
 		super.onPictureInPictureModeChanged(isInPictureInPictureMode)
 		Log.v(TAG, "onPictureInPictureModeChanged: $isInPictureInPictureMode")
-		updatePiPButtonVisibility()
+
+		// Hide the game menu fragment when in PiP.
+		gameMenuContainer?.isVisible = !isInPictureInPictureMode
 	}
 
 	override fun onStop() {
@@ -134,59 +133,109 @@ open class GodotGame : GodotEditor() {
 
 	override fun getEditorWindowInfo() = RUN_GAME_INFO
 
+	override fun getEditorGameEmbedMode() = GameMenuUtils.GameEmbedMode.DISABLED
+
 	override fun overrideOrientationRequest() = false
 
-	override fun enableLongPressGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click"))
-
-	override fun enablePanAndScaleGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures"))
-
-	override fun onGodotSetupCompleted() {
-		super.onGodotSetupCompleted()
-		Log.v(TAG, "OnGodotSetupCompleted")
-
-		// Check if we should be running in XR instead (if available) as it's possible we were
-		// launched from the project manager which doesn't have that information.
-		val launchingArgs = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
-		if (launchingArgs != null) {
-			val editorWindowInfo = retrieveEditorWindowInfo(launchingArgs)
-			if (editorWindowInfo != getEditorWindowInfo()) {
-				val relaunchIntent = getNewGodotInstanceIntent(editorWindowInfo, launchingArgs)
-				relaunchIntent.putExtra(EXTRA_NEW_LAUNCH, true)
-					.putExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD, intent.getBundleExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD))
-
-				Log.d(TAG, "Relaunching XR project using ${editorWindowInfo.windowClassName} with parameters ${launchingArgs.contentToString()}")
-				val godot = godot
-				if (godot != null) {
-					godot.destroyAndKillProcess {
-						ProcessPhoenix.triggerRebirth(this, relaunchIntent)
-					}
-				} else {
-					ProcessPhoenix.triggerRebirth(this, relaunchIntent)
-				}
-				return
-			}
+	override fun suspendGame(suspended: Boolean) {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_SUSPEND)
+			putBoolean(KEY_GAME_MENU_ACTION_PARAM1, suspended)
 		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+	}
 
-		// Request project runtime permissions if necessary
-		val permissionsToEnable = getProjectPermissionsToEnable()
-		if (permissionsToEnable.isNotEmpty()) {
-			PermissionsUtil.requestPermissions(this, permissionsToEnable)
+	override fun dispatchNextFrame() {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_NEXT_FRAME)
 		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
 	}
 
-	/**
-	 * Check for project permissions to enable
-	 */
-	@CallSuper
-	protected open fun getProjectPermissionsToEnable(): MutableList<String> {
-		val permissionsToEnable = mutableListOf<String>()
-
-		// Check for RECORD_AUDIO permission
-		val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input"))
-		if (audioInputEnabled) {
-			permissionsToEnable.add(Manifest.permission.RECORD_AUDIO)
+	override fun toggleSelectionVisibility(enabled: Boolean) {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_SELECTION_VISIBLE)
+			putBoolean(KEY_GAME_MENU_ACTION_PARAM1, enabled)
+		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+	}
+
+	override fun overrideCamera(enabled: Boolean) {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_CAMERA_OVERRIDE)
+			putBoolean(KEY_GAME_MENU_ACTION_PARAM1, enabled)
+		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+	}
+
+	override fun selectRuntimeNode(nodeType: GameMenuFragment.GameMenuListener.NodeType) {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_NODE_TYPE)
+			putSerializable(KEY_GAME_MENU_ACTION_PARAM1, nodeType)
+		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+	}
+
+	override fun selectRuntimeNodeSelectMode(selectMode: GameMenuFragment.GameMenuListener.SelectMode) {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_SELECT_MODE)
+			putSerializable(KEY_GAME_MENU_ACTION_PARAM1, selectMode)
+		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+	}
+
+	override fun reset2DCamera() {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_RESET_CAMERA_2D_POSITION)
+		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+	}
+
+	override fun reset3DCamera() {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_RESET_CAMERA_3D_POSITION)
+		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+	}
+
+	override fun manipulateCamera(mode: GameMenuFragment.GameMenuListener.CameraMode) {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_CAMERA_MANIPULATE_MODE)
+			putSerializable(KEY_GAME_MENU_ACTION_PARAM1, mode)
+		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+	}
+
+	override fun embedGameOnPlay(embedded: Boolean) {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_EMBED_GAME_ON_PLAY)
+			putBoolean(KEY_GAME_MENU_ACTION_PARAM1, embedded)
 		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+	}
+
+	protected open fun isGameEmbedded() = false
+
+	override fun isGameEmbeddingSupported() = !isNativeXRDevice(applicationContext)
+
+	override fun isMinimizedButtonEnabled() = isTaskRoot && !isNativeXRDevice(applicationContext)
 
-		return permissionsToEnable
+	override fun isCloseButtonEnabled() = !isNativeXRDevice(applicationContext)
+
+	override fun isPiPButtonEnabled() = hasPiPSystemFeature()
+
+	override fun isMenuBarCollapsable() = true
+
+	override fun minimizeGameWindow() {
+		moveTaskToBack(false)
+	}
+
+	override fun closeGameWindow() {
+		ProcessPhoenix.forceQuit(this)
+	}
+
+	override fun onGameMenuCollapsed(collapsed: Boolean) {
+		expandGameMenuButton?.isVisible = shouldShowGameMenuBar() && isMenuBarCollapsable() && collapsed
 	}
+
 }

+ 3 - 1
platform/android/java/editor/src/main/java/org/godotengine/editor/GodotXRGame.kt

@@ -36,7 +36,7 @@ import org.godotengine.godot.xr.XRMode
 /**
  * Provide support for running XR apps / games from the editor window.
  */
-open class GodotXRGame: GodotGame() {
+open class GodotXRGame: BaseGodotGame() {
 
 	override fun overrideOrientationRequest() = true
 
@@ -56,6 +56,8 @@ open class GodotXRGame: GodotGame() {
 
 	override fun getEditorWindowInfo() = XR_RUN_GAME_INFO
 
+	override fun getGodotAppLayout() = R.layout.godot_xr_game_layout
+
 	override fun getProjectPermissionsToEnable(): MutableList<String> {
 		val permissionsToEnable = super.getProjectPermissionsToEnable()
 

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

@@ -0,0 +1,147 @@
+/**************************************************************************/
+/*  EmbeddedGodotGame.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.embed
+
+import android.os.Bundle
+import android.view.Gravity
+import android.view.MotionEvent
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND
+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
+
+/**
+ * Host the Godot game from the editor when the embedded mode is enabled.
+ */
+class EmbeddedGodotGame : GodotGame() {
+
+	companion object {
+		private val TAG = EmbeddedGodotGame::class.java.simpleName
+
+		private const val FULL_SCREEN_WIDTH = WindowManager.LayoutParams.MATCH_PARENT
+		private const val FULL_SCREEN_HEIGHT = WindowManager.LayoutParams.MATCH_PARENT
+	}
+
+	private val defaultWidthInPx : Int by lazy {
+		resources.getDimensionPixelSize(R.dimen.embed_game_window_default_width)
+	}
+	private val defaultHeightInPx : Int by lazy {
+		resources.getDimensionPixelSize(R.dimen.embed_game_window_default_height)
+	}
+
+	private var layoutWidthInPx = 0
+	private var layoutHeightInPx = 0
+
+	override fun onCreate(savedInstanceState: Bundle?) {
+		super.onCreate(savedInstanceState)
+
+		setFinishOnTouchOutside(false)
+
+		val layoutParams = window.attributes
+		layoutParams.flags = layoutParams.flags or FLAG_NOT_TOUCH_MODAL or FLAG_WATCH_OUTSIDE_TOUCH
+		layoutParams.flags = layoutParams.flags and FLAG_DIM_BEHIND.inv()
+		layoutParams.gravity = Gravity.END or Gravity.BOTTOM
+
+		layoutWidthInPx = defaultWidthInPx
+		layoutHeightInPx = defaultHeightInPx
+
+		layoutParams.width = layoutWidthInPx
+		layoutParams.height = layoutHeightInPx
+		window.attributes = layoutParams
+	}
+
+	override fun dispatchTouchEvent(event: MotionEvent): Boolean {
+		when (event.actionMasked) {
+			MotionEvent.ACTION_OUTSIDE -> {
+				if (gameMenuFragment?.isAlwaysOnTop() == true) {
+					enterPiPMode()
+				} else {
+					minimizeGameWindow()
+				}
+			}
+
+			MotionEvent.ACTION_MOVE -> {
+//				val layoutParams = window.attributes
+				// TODO: Add logic to move the embedded window.
+//				window.attributes = layoutParams
+			}
+		}
+		return super.dispatchTouchEvent(event)
+	}
+
+	override fun getEditorWindowInfo() = EMBEDDED_RUN_GAME_INFO
+
+	override fun getEditorGameEmbedMode() = GameMenuUtils.GameEmbedMode.ENABLED
+
+	override fun isGameEmbedded() = true
+
+	private fun updateWindowDimensions(widthInPx: Int, heightInPx: Int) {
+		val layoutParams = window.attributes
+		layoutParams.width = widthInPx
+		layoutParams.height = heightInPx
+		window.attributes = layoutParams
+	}
+
+	override fun isMinimizedButtonEnabled() = true
+
+	override fun isCloseButtonEnabled() = true
+
+	override fun isFullScreenButtonEnabled() = true
+
+	override fun isPiPButtonEnabled() = false
+
+	override fun isMenuBarCollapsable() = false
+
+	override fun isAlwaysOnTopSupported() = hasPiPSystemFeature()
+
+	override fun onFullScreenUpdated(enabled: Boolean) {
+		godot?.enableImmersiveMode(enabled)
+		if (enabled) {
+			layoutWidthInPx = FULL_SCREEN_WIDTH
+			layoutHeightInPx = FULL_SCREEN_HEIGHT
+		} else {
+			layoutWidthInPx = defaultWidthInPx
+			layoutHeightInPx = defaultHeightInPx
+		}
+		updateWindowDimensions(layoutWidthInPx, layoutHeightInPx)
+	}
+
+	override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
+		super.onPictureInPictureModeChanged(isInPictureInPictureMode)
+		// Maximize the dimensions when entering PiP so the window fills the full PiP bounds.
+		onFullScreenUpdated(isInPictureInPictureMode)
+	}
+
+	override fun shouldShowGameMenuBar() = gameMenuContainer != null
+}

+ 480 - 0
platform/android/java/editor/src/main/java/org/godotengine/editor/embed/GameMenuFragment.kt

@@ -0,0 +1,480 @@
+/**************************************************************************/
+/*  GameMenuFragment.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.embed
+
+import android.content.Context
+import android.os.Build
+import android.os.Bundle
+import android.preference.PreferenceManager
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.PopupMenu
+import android.widget.RadioButton
+import androidx.core.content.edit
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import org.godotengine.editor.BaseGodotEditor
+import org.godotengine.editor.BaseGodotEditor.Companion.SNACKBAR_SHOW_DURATION_MS
+import org.godotengine.editor.R
+import org.godotengine.godot.utils.DialogUtils
+
+/**
+ * Implements the game menu interface for the Android editor.
+ */
+class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
+
+	companion object {
+		val TAG = GameMenuFragment::class.java.simpleName
+
+		private const val PREF_KEY_ALWAYS_ON_TOP = "pref_key_always_on_top"
+		private const val PREF_KEY_DONT_SHOW_RESTART_GAME_HINT = "pref_key_dont_show_restart_game_hint"
+		private const val PREF_KEY_GAME_MENU_BAR_COLLAPSED = "pref_key_game_menu_bar_collapsed"
+	}
+
+	/**
+	 * Used to be notified of events fired when interacting with the game menu.
+	 */
+	interface GameMenuListener {
+
+		/**
+		 * Kotlin representation of the RuntimeNodeSelect::SelectMode enum in 'scene/debugger/scene_debugger.h'.
+		 */
+		enum class SelectMode {
+			SINGLE,
+			LIST
+		}
+
+		/**
+		 * Kotlin representation of the RuntimeNodeSelect::NodeType enum in 'scene/debugger/scene_debugger.h'.
+		 */
+		enum class NodeType {
+			NONE,
+			TYPE_2D,
+			TYPE_3D
+		}
+
+		/**
+		 * Kotlin representation of the EditorDebuggerNode::CameraOverride in 'editor/debugger/editor_debugger_node.h'.
+		 */
+		enum class CameraMode {
+			NONE,
+			IN_GAME,
+			EDITORS
+		}
+
+		fun suspendGame(suspended: Boolean)
+		fun dispatchNextFrame()
+		fun toggleSelectionVisibility(enabled: Boolean)
+		fun overrideCamera(enabled: Boolean)
+		fun selectRuntimeNode(nodeType: NodeType)
+		fun selectRuntimeNodeSelectMode(selectMode: SelectMode)
+		fun reset2DCamera()
+		fun reset3DCamera()
+		fun manipulateCamera(mode: CameraMode)
+
+		fun isGameEmbeddingSupported(): Boolean
+		fun embedGameOnPlay(embedded: Boolean)
+
+		fun enterPiPMode() {}
+		fun minimizeGameWindow() {}
+		fun closeGameWindow() {}
+
+		fun isMinimizedButtonEnabled() = false
+		fun isFullScreenButtonEnabled() = false
+		fun isCloseButtonEnabled() = false
+		fun isPiPButtonEnabled() = false
+		fun isMenuBarCollapsable() = false
+
+		fun isAlwaysOnTopSupported() = false
+
+		fun onFullScreenUpdated(enabled: Boolean) {}
+		fun onGameMenuCollapsed(collapsed: Boolean) {}
+	}
+
+	private val collapseMenuButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_collapse_button)
+	}
+	private val pauseButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_pause_button)
+	}
+	private val nextFrameButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_next_frame_button)
+	}
+	private val unselectNodesButton: RadioButton? by lazy {
+		view?.findViewById(R.id.game_menu_unselect_nodes_button)
+	}
+	private val select2DNodesButton: RadioButton? by lazy {
+		view?.findViewById(R.id.game_menu_select_2d_nodes_button)
+	}
+	private val select3DNodesButton: RadioButton? by lazy {
+		view?.findViewById(R.id.game_menu_select_3d_nodes_button)
+	}
+	private val guiVisibilityButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_gui_visibility_button)
+	}
+	private val toolSelectButton: RadioButton? by lazy {
+		view?.findViewById(R.id.game_menu_tool_select_button)
+	}
+	private val listSelectButton: RadioButton? by lazy {
+		view?.findViewById(R.id.game_menu_list_select_button)
+	}
+	private val optionsButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_options_button)
+	}
+	private val minimizeButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_minimize_button)
+	}
+	private val pipButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_pip_button)
+	}
+	private val fullscreenButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_fullscreen_button)
+	}
+	private val closeButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_close_button)
+	}
+
+	private val popupMenu: PopupMenu by lazy {
+		PopupMenu(context, optionsButton).apply {
+			setOnMenuItemClickListener(this@GameMenuFragment)
+			inflate(R.menu.options_menu)
+
+			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+				menu.setGroupDividerEnabled(true)
+			}
+		}
+	}
+
+	private val menuItemActionView: View by lazy {
+		View(context)
+	}
+	private val menuItemActionExpandListener = object: MenuItem.OnActionExpandListener {
+		override fun onMenuItemActionExpand(item: MenuItem): Boolean {
+			return false
+		}
+
+		override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
+			return false
+		}
+	}
+
+	private var menuListener: GameMenuListener? = null
+	private var alwaysOnTopChecked = false
+	private var isGameEmbedded = false
+	private var isGameRunning = false
+
+	override fun onAttach(context: Context) {
+		super.onAttach(context)
+		val parentActivity = activity
+		if (parentActivity is GameMenuListener) {
+			menuListener = parentActivity
+		} else {
+			val parentFragment = parentFragment
+			if (parentFragment is GameMenuListener) {
+				menuListener = parentFragment
+			}
+		}
+	}
+
+	override fun onDetach() {
+		super.onDetach()
+		menuListener = null
+	}
+
+	override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, bundle: Bundle?): View? {
+		return inflater.inflate(R.layout.game_menu_fragment_layout, container, false)
+	}
+
+	override fun onViewCreated(view: View, bundle: Bundle?) {
+		super.onViewCreated(view, bundle)
+
+		val isMinimizeButtonEnabled = menuListener?.isMinimizedButtonEnabled() == true
+		val isFullScreenButtonEnabled = menuListener?.isFullScreenButtonEnabled() == true
+		val isCloseButtonEnabled = menuListener?.isCloseButtonEnabled() == true
+		val isPiPButtonEnabled = menuListener?.isPiPButtonEnabled() == true
+		val isMenuBarCollapsable = menuListener?.isMenuBarCollapsable() == true
+
+		// Show the divider if any of the window controls is visible
+		view.findViewById<View>(R.id.game_menu_window_controls_divider)?.isVisible =
+			isMinimizeButtonEnabled ||
+				isFullScreenButtonEnabled ||
+				isCloseButtonEnabled ||
+				isPiPButtonEnabled ||
+				isMenuBarCollapsable
+
+		collapseMenuButton?.apply {
+			isVisible = isMenuBarCollapsable
+			setOnClickListener {
+				collapseGameMenu()
+			}
+		}
+		fullscreenButton?.apply{
+			isVisible = isFullScreenButtonEnabled
+			setOnClickListener {
+				it.isActivated = !it.isActivated
+				menuListener?.onFullScreenUpdated(it.isActivated)
+			}
+		}
+		pipButton?.apply {
+			isVisible = isPiPButtonEnabled
+			setOnClickListener {
+				menuListener?.enterPiPMode()
+			}
+		}
+		minimizeButton?.apply {
+			isVisible = isMinimizeButtonEnabled
+			setOnClickListener {
+				menuListener?.minimizeGameWindow()
+			}
+		}
+		closeButton?.apply{
+			isVisible = isCloseButtonEnabled
+			setOnClickListener {
+				menuListener?.closeGameWindow()
+			}
+		}
+		pauseButton?.apply {
+			setOnClickListener {
+				val isActivated = !it.isActivated
+				menuListener?.suspendGame(isActivated)
+				it.isActivated = isActivated
+			}
+		}
+		nextFrameButton?.apply {
+			setOnClickListener {
+				menuListener?.dispatchNextFrame()
+			}
+		}
+
+		unselectNodesButton?.apply{
+			setOnCheckedChangeListener { buttonView, isChecked ->
+				if (isChecked) {
+					menuListener?.selectRuntimeNode(GameMenuListener.NodeType.NONE)
+				}
+			}
+		}
+		select2DNodesButton?.apply{
+			setOnCheckedChangeListener { buttonView, isChecked ->
+				if (isChecked) {
+					menuListener?.selectRuntimeNode(GameMenuListener.NodeType.TYPE_2D)
+				}
+			}
+		}
+		select3DNodesButton?.apply{
+			setOnCheckedChangeListener { buttonView, isChecked ->
+				if (isChecked) {
+					menuListener?.selectRuntimeNode(GameMenuListener.NodeType.TYPE_3D)
+				}
+			}
+		}
+		guiVisibilityButton?.apply{
+			setOnClickListener {
+				val isActivated = !it.isActivated
+				menuListener?.toggleSelectionVisibility(!isActivated)
+				it.isActivated = isActivated
+			}
+		}
+
+		toolSelectButton?.apply{
+			setOnCheckedChangeListener { buttonView, isChecked ->
+				if (isChecked) {
+					menuListener?.selectRuntimeNodeSelectMode(GameMenuListener.SelectMode.SINGLE)
+				}
+			}
+		}
+		listSelectButton?.apply{
+			setOnCheckedChangeListener { buttonView, isChecked ->
+				if (isChecked) {
+					menuListener?.selectRuntimeNodeSelectMode(GameMenuListener.SelectMode.LIST)
+				}
+			}
+		}
+		optionsButton?.setOnClickListener {
+			popupMenu.show()
+		}
+
+		refreshGameMenu(arguments?.getBundle(BaseGodotEditor.EXTRA_GAME_MENU_STATE) ?: Bundle())
+	}
+
+	internal fun refreshGameMenu(gameMenuState: Bundle) {
+		val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context)
+		if (menuListener?.isMenuBarCollapsable() == true) {
+			val collapsed = sharedPrefs.getBoolean(PREF_KEY_GAME_MENU_BAR_COLLAPSED, false)
+			view?.isVisible = !collapsed
+			menuListener?.onGameMenuCollapsed(collapsed)
+		}
+		alwaysOnTopChecked = sharedPrefs.getBoolean(PREF_KEY_ALWAYS_ON_TOP, false)
+		isGameEmbedded = gameMenuState.getBoolean(BaseGodotEditor.EXTRA_IS_GAME_EMBEDDED, false)
+		isGameRunning = gameMenuState.getBoolean(BaseGodotEditor.EXTRA_IS_GAME_RUNNING, false)
+
+		pauseButton?.isEnabled = isGameRunning
+		nextFrameButton?.isEnabled = isGameRunning
+
+		val nodeType = gameMenuState.getSerializable(BaseGodotEditor.GAME_MENU_ACTION_SET_NODE_TYPE) as GameMenuListener.NodeType? ?: GameMenuListener.NodeType.NONE
+		unselectNodesButton?.isChecked = nodeType == GameMenuListener.NodeType.NONE
+		select2DNodesButton?.isChecked = nodeType == GameMenuListener.NodeType.TYPE_2D
+		select3DNodesButton?.isChecked = nodeType == GameMenuListener.NodeType.TYPE_3D
+
+		guiVisibilityButton?.isActivated = !gameMenuState.getBoolean(BaseGodotEditor.GAME_MENU_ACTION_SET_SELECTION_VISIBLE, true)
+
+		val selectMode = gameMenuState.getSerializable(BaseGodotEditor.GAME_MENU_ACTION_SET_SELECT_MODE) as GameMenuListener.SelectMode? ?: GameMenuListener.SelectMode.SINGLE
+		toolSelectButton?.isChecked = selectMode == GameMenuListener.SelectMode.SINGLE
+		listSelectButton?.isChecked = selectMode == GameMenuListener.SelectMode.LIST
+
+		popupMenu.menu.apply {
+			if (menuListener?.isGameEmbeddingSupported() == false) {
+				setGroupEnabled(R.id.group_menu_embed_options, false)
+				setGroupVisible(R.id.group_menu_embed_options, false)
+			} else {
+				findItem(R.id.menu_embed_game_on_play)?.isChecked = isGameEmbedded
+
+				val keepOnTopMenuItem = findItem(R.id.menu_embed_game_keep_on_top)
+				if (menuListener?.isAlwaysOnTopSupported() == false) {
+					keepOnTopMenuItem?.isVisible = false
+				} else {
+					keepOnTopMenuItem?.isEnabled = isGameEmbedded
+				}
+			}
+
+			setGroupEnabled(R.id.group_menu_camera_options, isGameRunning)
+			setGroupVisible(R.id.group_menu_camera_options, isGameRunning)
+			findItem(R.id.menu_camera_options)?.isEnabled = false
+
+			findItem(R.id.menu_embed_game_keep_on_top)?.isChecked = alwaysOnTopChecked
+
+			val cameraMode = gameMenuState.getSerializable(BaseGodotEditor.GAME_MENU_ACTION_SET_CAMERA_MANIPULATE_MODE) as GameMenuListener.CameraMode? ?: GameMenuListener.CameraMode.NONE
+			if (cameraMode == GameMenuListener.CameraMode.IN_GAME || cameraMode == GameMenuListener.CameraMode.NONE) {
+				findItem(R.id.menu_manipulate_camera_in_game)?.isChecked = true
+			} else {
+				findItem(R.id.menu_manipulate_camera_from_editors)?.isChecked = true
+			}
+		}
+	}
+
+	internal fun isAlwaysOnTop() = isGameEmbedded && alwaysOnTopChecked
+
+	private fun collapseGameMenu() {
+		view?.isVisible = false
+		PreferenceManager.getDefaultSharedPreferences(context).edit {
+			putBoolean(PREF_KEY_GAME_MENU_BAR_COLLAPSED, true)
+		}
+		menuListener?.onGameMenuCollapsed(true)
+	}
+
+	internal fun expandGameMenu() {
+		view?.isVisible = true
+		PreferenceManager.getDefaultSharedPreferences(context).edit {
+			putBoolean(PREF_KEY_GAME_MENU_BAR_COLLAPSED, false)
+		}
+		menuListener?.onGameMenuCollapsed(false)
+	}
+
+	private fun updateAlwaysOnTop(enabled: Boolean) {
+		alwaysOnTopChecked = enabled
+		PreferenceManager.getDefaultSharedPreferences(context).edit {
+			putBoolean(PREF_KEY_ALWAYS_ON_TOP, enabled)
+		}
+	}
+
+	private fun preventMenuItemCollapse(item: MenuItem) {
+		item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW)
+		item.setActionView(menuItemActionView)
+		item.setOnActionExpandListener(menuItemActionExpandListener)
+	}
+
+	override fun onMenuItemClick(item: MenuItem): Boolean {
+		if (!item.hasSubMenu()) {
+			preventMenuItemCollapse(item)
+		}
+
+		when(item.itemId) {
+			R.id.menu_embed_game_on_play -> {
+				item.isChecked = !item.isChecked
+				menuListener?.embedGameOnPlay(item.isChecked)
+
+				if (item.isChecked != isGameEmbedded && isGameRunning) {
+					activity?.let {
+						val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context)
+						if (!sharedPrefs.getBoolean(PREF_KEY_DONT_SHOW_RESTART_GAME_HINT, false)) {
+							DialogUtils.showSnackbar(
+								it,
+								if (item.isChecked) getString(R.string.restart_embed_game_hint) else getString(R.string.restart_non_embedded_game_hint),
+								SNACKBAR_SHOW_DURATION_MS,
+								getString(R.string.dont_show_again_message)
+							) {
+								sharedPrefs.edit {
+									putBoolean(PREF_KEY_DONT_SHOW_RESTART_GAME_HINT, true)
+								}
+							}
+						}
+					}
+				}
+			}
+
+			R.id.menu_embed_game_keep_on_top -> {
+				item.isChecked = !item.isChecked
+				updateAlwaysOnTop(item.isChecked)
+			}
+
+			R.id.menu_camera_override -> {
+				item.isChecked = !item.isChecked
+				menuListener?.overrideCamera(item.isChecked)
+
+				popupMenu.menu.findItem(R.id.menu_camera_options)?.isEnabled = item.isChecked
+			}
+
+			R.id.menu_reset_2d_camera -> {
+				menuListener?.reset2DCamera()
+			}
+
+			R.id.menu_reset_3d_camera -> {
+				menuListener?.reset3DCamera()
+			}
+
+			R.id.menu_manipulate_camera_in_game -> {
+				if (!item.isChecked) {
+					item.isChecked = true
+					menuListener?.manipulateCamera(GameMenuListener.CameraMode.IN_GAME)
+				}
+			}
+
+			R.id.menu_manipulate_camera_from_editors -> {
+				if (!item.isChecked) {
+					item.isChecked = true
+					menuListener?.manipulateCamera(GameMenuListener.CameraMode.EDITORS)
+				}
+			}
+		}
+		return false
+	}
+}

+ 9 - 0
platform/android/java/editor/src/main/res/color/game_menu_icons_color_state.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+	<item android:color="@color/game_menu_icon_activated_color" android:state_activated="true" />
+	<item android:color="@color/game_menu_icon_activated_color" android:state_selected="true" />
+	<item android:color="@color/game_menu_icon_activated_color" android:state_pressed="true" />
+	<item android:color="@color/game_menu_icon_activated_color" android:state_checked="true" />
+	<item android:color="@color/game_menu_icon_disabled_color" android:state_enabled="false" />
+	<item android:color="@color/game_menu_icon_default_color" />
+</selector>

+ 5 - 0
platform/android/java/editor/src/main/res/drawable/baseline_close_24.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+    <path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+
+</vector>

+ 12 - 0
platform/android/java/editor/src/main/res/drawable/baseline_expand_less_24.xml

@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:tint="@color/game_menu_icons_color_state"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z" />
+
+</vector>

+ 2 - 2
platform/android/java/editor/src/main/res/drawable/outline_fullscreen_exit_48.xml → platform/android/java/editor/src/main/res/drawable/baseline_expand_more_48.xml

@@ -1,12 +1,12 @@
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
 	android:width="48dp"
 	android:height="48dp"
-	android:tint="#FFFFFF"
+	android:tint="@color/game_menu_icons_color_state"
 	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" />
+		android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z" />
 
 </vector>

+ 5 - 0
platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_24.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+    <path android:fillColor="@android:color/white" android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z"/>
+
+</vector>

+ 5 - 0
platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_exit_24.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+    <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>

+ 6 - 0
platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_selector.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+	<item android:drawable="@drawable/baseline_fullscreen_exit_24" android:state_activated="true" />
+	<item android:drawable="@drawable/baseline_fullscreen_24" />
+</selector>

+ 5 - 0
platform/android/java/editor/src/main/res/drawable/baseline_minimize_24.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+    <path android:fillColor="@android:color/white" android:pathData="M6,19h12v2H6z"/>
+
+</vector>

+ 5 - 0
platform/android/java/editor/src/main/res/drawable/baseline_picture_in_picture_alt_24.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+    <path android:fillColor="@android:color/white" android:pathData="M19,11h-8v6h8v-6zM23,19L23,4.98C23,3.88 22.1,3 21,3L3,3c-1.1,0 -2,0.88 -2,1.98L1,19c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2zM21,19.02L3,19.02L3,4.97h18v14.05z"/>
+
+</vector>

+ 5 - 0
platform/android/java/editor/src/main/res/drawable/baseline_push_pin_24.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+    <path android:fillColor="@android:color/white" android:fillType="evenOdd" android:pathData="M16,9V4l1,0c0.55,0 1,-0.45 1,-1v0c0,-0.55 -0.45,-1 -1,-1H7C6.45,2 6,2.45 6,3v0c0,0.55 0.45,1 1,1l1,0v5c0,1.66 -1.34,3 -3,3h0v2h5.97v7l1,1l1,-1v-7H19v-2h0C17.34,12 16,10.66 16,9z"/>
+
+</vector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/camera.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M9,2a3,3 0,0 0,-3 2.777,3 3,0 1,0 -3,5.047V12a1,1 0,0 0,1 1h6a1,1 0,0 0,1 -1v-1l3,2V7l-3,2V7.23A3,3 0,0 0,9 2z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 1 - 6
platform/android/java/editor/src/main/res/drawable/pip_button_activated_bg_drawable.xml → platform/android/java/editor/src/main/res/drawable/expand_more_bg.xml

@@ -1,10 +1,5 @@
 <?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" />
+	<solid android:color="#99aaaaaa" />
 </shape>

+ 7 - 0
platform/android/java/editor/src/main/res/drawable/game_menu_button_bg.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+	<item android:drawable="@drawable/game_menu_selected_bg" android:state_pressed="true" />
+	<item android:drawable="@drawable/game_menu_selected_bg" android:state_activated="true" />
+	<item android:drawable="@drawable/game_menu_selected_bg" android:state_checked="true" />
+	<item android:drawable="@color/game_menu_default_bg" />
+</selector>

+ 6 - 0
platform/android/java/editor/src/main/res/drawable/game_menu_message_bg.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+	android:shape="rectangle">
+
+	<solid android:color="#232b2b" />
+</shape>

+ 10 - 0
platform/android/java/editor/src/main/res/drawable/game_menu_selected_bg.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+	android:shape="rectangle">
+
+	<solid android:color="#3333b5e5" />
+	<corners android:radius="5dp" />
+	<stroke
+		android:width="1dp"
+		android:color="@android:color/holo_blue_dark" />
+</shape>

+ 6 - 0
platform/android/java/editor/src/main/res/drawable/game_menu_selected_button_bg.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+	<item android:drawable="@drawable/game_menu_selected_bg" android:state_pressed="true" />
+	<item android:drawable="@drawable/game_menu_selected_bg" android:state_selected="true" />
+	<item android:drawable="@color/game_menu_default_bg" />
+</selector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/gui_tab_menu.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M8,0a2,2 0,0 0,0 4,2 2,0 0,0 0,-4zM8,6a2,2 0,0 0,0 4,2 2,0 0,0 0,-4zM8,12a2,2 0,0 0,0 4,2 2,0 0,0 0,-4z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/gui_visibility_hidden.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="m2.96,7.727 l-1.921,0.548c0.32,1.12 0.824,2.06 1.432,2.84l-0.834,0.834 1.414,1.414 0.843,-0.843c0.986,0.747 2.077,1.206 3.106,1.386V15h2v-1.094c1.029,-0.18 2.12,-0.639 3.105,-1.386l0.844,0.843 1.414,-1.414 -0.834,-0.834a8.285,8.285 0,0 0,1.432 -2.84l-1.922,-0.548C12.163,10.79 9.499,12 7.999,12s-4.163,-1.209 -5.038,-4.273z"
+      android:fillColor="@color/game_menu_icon_activated_color"/>
+</vector>

+ 5 - 0
platform/android/java/editor/src/main/res/drawable/gui_visibility_selector.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+	<item android:drawable="@drawable/gui_visibility_hidden" android:state_activated="true" />
+	<item android:drawable="@drawable/gui_visibility_visible" />
+</selector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/gui_visibility_visible.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M8,2C5.443,2 2.209,3.948 1.045,7.705a1,1 0,0 0,0 0.55C2.163,12.211 5.5,14 8,14s5.836,-1.789 6.961,-5.725a1,1 0,0 0,0 -0.55C13.861,3.935 10.554,2 8,2zM8,4a4,4 0,0 1,0 8,4 4,0 0,1 0,-8zM8,6a2,2 0,0 0,0 4,2 2,0 0,0 0,-4z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 24 - 0
platform/android/java/editor/src/main/res/drawable/input_event_joypad_motion.xml

@@ -0,0 +1,24 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M11.5,16v-1.5a1,1 0,0 0,-1 -1H10l1.6,-4a4,4 0,1 0,-3.5 -1.4l-2.2,5.4h-0.7a1,1 0,0 0,-1 1V16z"
+      android:fillColor="@color/game_menu_icon_default_color"/>
+  <path
+      android:pathData="M5.25,12.2 L2,4a8,8 0,0 1,7 -3,5 5,0 0,0 -2.1,7.2z">
+    <aapt:attr name="android:fillColor">
+      <gradient
+          android:startX="2"
+          android:startY="0"
+          android:endX="9"
+          android:endY="0"
+          android:type="linear">
+        <item android:offset="0" android:color="#0069C4D4"/>
+        <item android:offset="0.6" android:color="#FF69C4D4"/>
+      </gradient>
+    </aapt:attr>
+  </path>
+</vector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/list_select.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="m1,1v14h8.258l-0.822,-2h-5.436v-2h4.611l-0.822,-2h-3.789v-2h3.887a1.5,1.5 0,0 1,1.098 -0.498v-0.002a1.5,1.5 0,0 1,0.586 0.111l0.945,0.389h0.484v0.199l2,0.822v-7.022h-11zM3,3h7v2h-7zM8,8 L11.291,16 12.238,13.18 14.121,15.063 15.064,14.121 13.18,12.238 16,11.291 8,8z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/next_frame.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="m12,3c-0.552,0 -1,0.448 -1,1v8c0,0.552 0.448,1 1,1h1c0.552,0 1,-0.448 1,-1V4C14,3.448 13.552,3 13,3ZM2.975,3.002C2.433,3.016 2.001,3.458 2,4v8c-0,0.839 0.97,1.305 1.625,0.781l5,-4c0.499,-0.4 0.499,-1.16 0,-1.56l-5,-4C3.441,3.074 3.211,2.996 2.975,3.002Z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 11 - 0
platform/android/java/editor/src/main/res/drawable/node_3d.xml

@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M8,8m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
+      android:strokeWidth="2"
+      android:fillColor="#00000000"
+      android:strokeColor="#fc7f7f"/>
+</vector>

+ 16 - 0
platform/android/java/editor/src/main/res/drawable/nodes_2d.xml

@@ -0,0 +1,16 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M8,13C5.239,13 3,10.761 3,8 3,5.239 5.239,3 8,3"
+      android:strokeWidth="2"
+      android:fillColor="#00000000"
+      android:strokeColor="#8da5f3"/>
+  <path
+      android:pathData="m8,13c2.761,0 5,-2.239 5,-5C13,5.239 10.761,3 8,3"
+      android:strokeWidth="2"
+      android:fillColor="#00000000"
+      android:strokeColor="#8eef97"/>
+</vector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/pause.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M4,3a1,1 0,0 0,-1 1v8a1,1 0,0 0,1 1h2a1,1 0,0 0,1 -1L7,4a1,1 0,0 0,-1 -1zM10,3a1,1 0,0 0,-1 1v8a1,1 0,0 0,1 1h2a1,1 0,0 0,1 -1L13,4a1,1 0,0 0,-1 -1z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 5 - 0
platform/android/java/editor/src/main/res/drawable/pause_play_selector.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+	<item android:drawable="@drawable/play" android:state_activated="true" />
+	<item android:drawable="@drawable/pause" />
+</selector>

+ 0 - 9
platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml

@@ -1,9 +0,0 @@
-<?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 - 10
platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml

@@ -1,10 +0,0 @@
-<?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>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/play.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M4,12a1,1 0,0 0,1.555 0.832l6,-4a1,1 0,0 0,0 -1.664l-6,-4A1,1 0,0 0,4 4z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/play_48dp.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M4,12a1,1 0,0 0,1.555 0.832l6,-4a1,1 0,0 0,0 -1.664l-6,-4A1,1 0,0 0,4 4z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/tool_select.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M14,6.932 L2,1.995l4.936,12 1.421,-4.23 2.826,2.825 1.412,-1.412L9.77,8.352z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 190 - 0
platform/android/java/editor/src/main/res/layout/game_menu_fragment_layout.xml

@@ -0,0 +1,190 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	android:layout_width="match_parent"
+	android:layout_height="wrap_content"
+	android:orientation="horizontal"
+	android:background="@android:color/black">
+
+	<HorizontalScrollView
+		android:layout_width="0dp"
+		android:layout_height="wrap_content"
+		android:layout_weight="1">
+
+		<LinearLayout
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:gravity="center_vertical"
+			android:minHeight="48dp"
+			android:orientation="horizontal">
+
+			<ImageButton
+				android:id="@+id/game_menu_pause_button"
+				style="?android:attr/borderlessButtonStyle"
+				android:layout_width="48dp"
+				android:layout_height="48dp"
+				android:background="@drawable/game_menu_button_bg"
+				android:src="@drawable/pause_play_selector" />
+
+			<ImageButton
+				android:id="@+id/game_menu_next_frame_button"
+				style="?android:attr/borderlessButtonStyle"
+				android:layout_width="48dp"
+				android:layout_height="48dp"
+				android:background="@drawable/game_menu_button_bg"
+				android:src="@drawable/next_frame" />
+
+			<View
+				android:layout_width="1dp"
+				android:layout_height="match_parent"
+				android:layout_marginHorizontal="@dimen/game_menu_vseparator_horizontal_margin"
+				android:layout_marginVertical="@dimen/game_menu_vseparator_vertical_margin"
+				android:background="@color/game_menu_divider_color" />
+
+			<RadioGroup
+				android:layout_width="wrap_content"
+				android:layout_height="wrap_content"
+				android:orientation="horizontal">
+
+				<RadioButton
+					android:id="@+id/game_menu_unselect_nodes_button"
+					style="?android:attr/borderlessButtonStyle"
+					android:layout_width="wrap_content"
+					android:layout_height="48dp"
+					android:background="@drawable/game_menu_button_bg"
+					android:button="@null"
+					android:checked="true"
+					android:drawableStart="@drawable/input_event_joypad_motion"
+					android:padding="5dp"
+					android:text="@string/game_menu_input_event_joypad_motion_label" />
+
+				<RadioButton
+					android:id="@+id/game_menu_select_2d_nodes_button"
+					style="?android:attr/borderlessButtonStyle"
+					android:layout_width="48dp"
+					android:layout_height="48dp"
+					android:background="@drawable/game_menu_button_bg"
+					android:button="@null"
+					android:drawableStart="@drawable/nodes_2d"
+					android:padding="5dp"
+					android:text="@string/game_menu_nodes_2d_button_label" />
+
+				<RadioButton
+					android:id="@+id/game_menu_select_3d_nodes_button"
+					style="?android:attr/borderlessButtonStyle"
+					android:layout_width="48dp"
+					android:layout_height="48dp"
+					android:background="@drawable/game_menu_button_bg"
+					android:button="@null"
+					android:drawableStart="@drawable/node_3d"
+					android:padding="5dp"
+					android:text="@string/game_menu_node_3d_button_label" />
+			</RadioGroup>
+
+			<View
+				android:layout_width="1dp"
+				android:layout_height="match_parent"
+				android:layout_marginHorizontal="@dimen/game_menu_vseparator_horizontal_margin"
+				android:layout_marginVertical="@dimen/game_menu_vseparator_vertical_margin"
+				android:background="@color/game_menu_divider_color" />
+
+			<ImageButton
+				android:id="@+id/game_menu_gui_visibility_button"
+				style="?android:attr/borderlessButtonStyle"
+				android:layout_width="48dp"
+				android:layout_height="48dp"
+				android:background="@drawable/game_menu_button_bg"
+				android:src="@drawable/gui_visibility_selector" />
+
+			<View
+				android:layout_width="1dp"
+				android:layout_height="match_parent"
+				android:layout_marginHorizontal="@dimen/game_menu_vseparator_horizontal_margin"
+				android:layout_marginVertical="@dimen/game_menu_vseparator_vertical_margin"
+				android:background="@color/game_menu_divider_color" />
+
+			<RadioGroup
+				android:layout_width="wrap_content"
+				android:layout_height="wrap_content"
+				android:orientation="horizontal">
+
+				<RadioButton
+					android:id="@+id/game_menu_tool_select_button"
+					style="?android:attr/borderlessButtonStyle"
+					android:layout_width="48dp"
+					android:layout_height="48dp"
+					android:background="@drawable/game_menu_button_bg"
+					android:button="@null"
+					android:checked="true"
+					android:drawableStart="@drawable/tool_select"
+					android:padding="15dp" />
+
+				<RadioButton
+					android:id="@+id/game_menu_list_select_button"
+					style="?android:attr/borderlessButtonStyle"
+					android:layout_width="48dp"
+					android:layout_height="48dp"
+					android:background="@drawable/game_menu_button_bg"
+					android:button="@null"
+					android:drawableStart="@drawable/list_select"
+					android:padding="15dp" />
+			</RadioGroup>
+		</LinearLayout>
+	</HorizontalScrollView>
+
+	<ImageButton
+		android:id="@+id/game_menu_options_button"
+		style="?android:attr/borderlessButtonStyle"
+		android:layout_width="48dp"
+		android:layout_height="48dp"
+		android:background="@drawable/game_menu_button_bg"
+		android:src="@drawable/gui_tab_menu" />
+
+	<View
+		android:id="@+id/game_menu_window_controls_divider"
+		android:layout_width="1dp"
+		android:layout_height="match_parent"
+		android:layout_marginHorizontal="@dimen/game_menu_vseparator_horizontal_margin"
+		android:layout_marginVertical="@dimen/game_menu_vseparator_vertical_margin"
+		android:background="@color/game_menu_divider_color" />
+
+	<ImageButton
+		android:id="@+id/game_menu_collapse_button"
+		style="?android:attr/borderlessButtonStyle"
+		android:layout_width="48dp"
+		android:layout_height="48dp"
+		android:background="@drawable/game_menu_selected_button_bg"
+		android:src="@drawable/baseline_expand_less_24"
+		/>
+
+	<ImageButton
+		android:id="@+id/game_menu_minimize_button"
+		style="?android:attr/borderlessButtonStyle"
+		android:layout_width="48dp"
+		android:layout_height="48dp"
+		android:background="@drawable/game_menu_button_bg"
+		android:src="@drawable/baseline_minimize_24"/>
+
+	<ImageButton
+		android:id="@+id/game_menu_pip_button"
+		style="?android:attr/borderlessButtonStyle"
+		android:layout_width="48dp"
+		android:layout_height="48dp"
+		android:background="@drawable/game_menu_selected_button_bg"
+		android:src="@drawable/baseline_picture_in_picture_alt_24"/>
+
+	<ImageButton
+		android:id="@+id/game_menu_fullscreen_button"
+		style="?android:attr/borderlessButtonStyle"
+		android:layout_width="48dp"
+		android:layout_height="48dp"
+		android:background="@drawable/game_menu_selected_button_bg"
+		android:src="@drawable/baseline_fullscreen_selector"/>
+
+	<ImageButton
+		android:id="@+id/game_menu_close_button"
+		style="?android:attr/borderlessButtonStyle"
+		android:layout_width="48dp"
+		android:layout_height="48dp"
+		android:background="@drawable/game_menu_button_bg"
+		android:src="@drawable/baseline_close_24"/>
+</LinearLayout>

+ 44 - 3
platform/android/java/editor/src/main/res/layout/godot_editor_layout.xml

@@ -1,14 +1,55 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout
-	xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
 	android:layout_width="match_parent"
 	android:layout_height="match_parent"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:background="@android:color/black"
+	tools:background="@android:color/background_light"
 	xmlns:app="http://schemas.android.com/apk/res-auto">
 
 	<FrameLayout
 		android:id="@+id/godot_fragment_container"
 		android:layout_width="match_parent"
-		android:layout_height="match_parent" />
+		android:layout_height="match_parent"/>
+
+	<FrameLayout
+		android:id="@+id/embedded_game_view_container_window"
+		android:layout_width="match_parent"
+		android:layout_height="match_parent"
+		android:background="#22bebebe"
+		android:visibility="gone"
+		tools:visibility="visible">
+
+		<androidx.constraintlayout.widget.ConstraintLayout
+			android:id="@+id/embedded_game_view_container"
+			android:layout_width="@dimen/embed_game_window_default_width"
+			android:layout_height="@dimen/embed_game_window_default_height"
+			android:background="@drawable/game_menu_message_bg"
+			android:layout_gravity="bottom|end">
+
+			<FrameLayout
+				android:layout_width="match_parent"
+				android:layout_height="wrap_content"
+				android:id="@+id/game_menu_fragment_container"
+				app:layout_constraintTop_toTopOf="parent" />
+
+			<TextView
+				android:id="@+id/embedded_game_state_label"
+				android:layout_width="wrap_content"
+				android:layout_height="wrap_content"
+				android:gravity="center"
+				android:text="@string/embedded_game_not_running_message"
+				android:drawableBottom="@drawable/play_48dp"
+				android:textColor="@android:color/white"
+				android:textSize="18sp"
+				android:textStyle="bold"
+				app:layout_constraintTop_toBottomOf="@+id/game_menu_fragment_container"
+				app:layout_constraintBottom_toBottomOf="parent"
+				app:layout_constraintStart_toStartOf="parent"
+				app:layout_constraintEnd_toEndOf="parent"
+				/>
+		</androidx.constraintlayout.widget.ConstraintLayout>
+	</FrameLayout>
 
 	<ProgressBar
 		style="@android:style/Widget.Holo.ProgressBar.Large"

+ 23 - 16
platform/android/java/editor/src/main/res/layout/godot_game_layout.xml

@@ -1,25 +1,32 @@
 <?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-	xmlns:tools="http://schemas.android.com/tools"
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 	android:layout_width="match_parent"
 	android:layout_height="match_parent">
 
+	<FrameLayout
+		android:id="@+id/game_menu_fragment_container"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_alignParentTop="true"
+		/>
+
 	<FrameLayout
 		android:id="@+id/godot_fragment_container"
 		android:layout_width="match_parent"
-		android:layout_height="match_parent"/>
+		android:layout_height="match_parent"
+		android:layout_below="@+id/game_menu_fragment_container"/>
 
-	<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" />
+	<ImageButton
+		android:id="@+id/game_menu_expand_button"
+		style="?android:attr/borderlessButtonStyle"
+		android:layout_width="48dp"
+		android:layout_height="48dp"
+		android:background="@drawable/expand_more_bg"
+		android:src="@drawable/baseline_expand_more_48"
+		android:layout_below="@+id/game_menu_fragment_container"
+		android:layout_alignParentEnd="true"
+		android:layout_marginEnd="24dp"
+		android:layout_marginTop="24dp"
+		/>
 
-</FrameLayout>
+</RelativeLayout>

+ 11 - 0
platform/android/java/editor/src/main/res/layout/godot_xr_game_layout.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	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"/>
+
+</FrameLayout>

+ 56 - 0
platform/android/java/editor/src/main/res/menu/options_menu.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+	<group android:id="@+id/group_menu_embed_options">
+		<item
+			android:id="@+id/menu_embed_game_on_play"
+			android:checkable="true"
+			android:checked="true"
+			android:title="@string/menu_embed_game_on_play_label" />
+		<item android:id="@+id/menu_embed_game_keep_on_top"
+			android:checkable="true"
+			android:checked="false"
+			android:enabled="false"
+			android:title="@string/menu_keep_embed_game_on_top_label"
+			android:icon="@drawable/baseline_push_pin_24" />
+	</group>
+
+	<group android:id="@+id/group_menu_camera_options">
+		<item
+			android:id="@+id/menu_camera_override"
+			android:checkable="true"
+			android:checked="false"
+			android:icon="@drawable/camera"
+			android:title="@string/menu_camera_override_label" />
+		<item
+			android:id="@+id/menu_camera_options"
+			android:icon="@drawable/camera"
+			android:enabled="false"
+			android:title="@string/menu_camera_label">
+			<menu>
+				<group android:id="@+id/group_menu_camera_reset_mode">
+					<item
+						android:id="@+id/menu_reset_2d_camera"
+						android:title="@string/menu_reset_2d_camera_label" />
+
+					<item
+						android:id="@+id/menu_reset_3d_camera"
+						android:title="@string/menu_reset_3d_camera_label" />
+				</group>
+
+				<group
+					android:id="@+id/group_menu_camera_manipulation"
+					android:checkableBehavior="single">
+					<item
+						android:id="@+id/menu_manipulate_camera_in_game"
+						android:checked="true"
+						android:title="@string/menu_manipulate_in_game_label" />
+					<item
+						android:id="@+id/menu_manipulate_camera_from_editors"
+						android:title="@string/menu_manipulate_from_editors_label" />
+				</group>
+			</menu>
+		</item>
+	</group>
+
+</menu>

+ 8 - 0
platform/android/java/editor/src/main/res/values/colors.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+	<color name="game_menu_icon_default_color">#e0e0e0</color>
+	<color name="game_menu_icon_disabled_color">@android:color/darker_gray</color>
+	<color name="game_menu_icon_activated_color">@android:color/holo_blue_light</color>
+	<color name="game_menu_default_bg">@android:color/transparent</color>
+	<color name="game_menu_divider_color">@android:color/darker_gray</color>
+</resources>

+ 4 - 0
platform/android/java/editor/src/main/res/values/dimens.xml

@@ -2,4 +2,8 @@
 <resources>
 	<dimen name="editor_default_window_height">720dp</dimen>
 	<dimen name="editor_default_window_width">1024dp</dimen>
+	<dimen name="game_menu_vseparator_vertical_margin">8dp</dimen>
+	<dimen name="game_menu_vseparator_horizontal_margin">1dp</dimen>
+	<dimen name="embed_game_window_default_width">640dp</dimen>
+	<dimen name="embed_game_window_default_height">360dp</dimen>
 </resources>

+ 17 - 1
platform/android/java/editor/src/main/res/values/strings.xml

@@ -3,5 +3,21 @@
 	<string name="godot_game_activity_name">Godot Play window</string>
 	<string name="denied_storage_permission_error_msg">Missing storage access permission!</string>
 	<string name="denied_install_packages_permission_error_msg">Missing install packages permission!</string>
-    <string name="pip_button_description">Button used to toggle picture-in-picture mode for the Play window</string>
+	<string name="game_menu_input_event_joypad_motion_label">Input</string>
+	<string name="game_menu_nodes_2d_button_label">2D</string>
+	<string name="game_menu_node_3d_button_label">3D</string>
+	<string name="menu_reset_2d_camera_label">Reset 2D Camera</string>
+	<string name="menu_reset_3d_camera_label">Reset 3D Camera</string>
+	<string name="menu_manipulate_in_game_label">Manipulate In-Game</string>
+	<string name="menu_manipulate_from_editors_label">Manipulate From Editors</string>
+    <string name="menu_embed_game_on_play_label">Embed Game On Play</string>
+	<string name="menu_camera_label">Camera Options</string>
+	<string name="menu_camera_override_label">Override Camera</string>
+	<string name="embedded_game_not_running_message">Press play to start the game.</string>
+	<string name="running_game_not_embedded_message">Game running not embedded.</string>
+	<string name="menu_keep_embed_game_on_top_label">Keep on Top using PiP</string>
+	<string name="dont_show_again_message">Don\'t show again</string>
+	<string name="show_game_resume_hint">Tap on \'Game\' to resume</string>
+	<string name="restart_embed_game_hint">Restart game to embed</string>
+	<string name="restart_non_embedded_game_hint">Restart Game to disable embedding</string>
 </resources>

+ 3 - 0
platform/android/java/editor/src/main/res/values/themes.xml

@@ -9,4 +9,7 @@
 		screen. This is required. -->
 		<item name="postSplashScreenTheme">@style/GodotEditorTheme</item>
 	</style>
+
+	<style name="GodotEmbeddedGameTheme" parent="@android:style/Theme.DeviceDefault.Panel">
+	</style>
 </resources>

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

@@ -333,7 +333,7 @@ class Godot(private val context: Context) {
 	 * Toggle immersive mode.
 	 * Must be called from the UI thread.
 	 */
-	private fun enableImmersiveMode(enabled: Boolean, override: Boolean = false) {
+	fun enableImmersiveMode(enabled: Boolean, override: Boolean = false) {
 		val activity = getActivity() ?: return
 		val window = activity.window ?: return
 
@@ -1068,6 +1068,16 @@ class Godot(private val context: Context) {
 		return PermissionsUtil.getGrantedPermissions(getActivity())
 	}
 
+	/**
+	 * Returns true if this is the Godot editor.
+	 */
+	fun isEditorHint() = isEditorBuild() && GodotLib.isEditorHint()
+
+	/**
+	 * Returns true if this is the Godot project manager.
+	 */
+	fun isProjectManagerHint() = isEditorBuild() && GodotLib.isProjectManagerHint()
+
 	/**
 	 * Return true if the given feature is supported.
 	 */
@@ -1177,4 +1187,9 @@ class Godot(private val context: Context) {
 		val verifyResult = primaryHost?.verifyApk(apkPath) ?: Error.ERR_UNAVAILABLE
 		return verifyResult.toNativeValue()
 	}
+
+	@Keep
+	private fun nativeOnEditorWorkspaceSelected(workspace: String) {
+		primaryHost?.onEditorWorkspaceSelected(workspace)
+	}
 }

+ 7 - 0
platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java

@@ -509,4 +509,11 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
 		}
 		return false;
 	}
+
+	@Override
+	public void onEditorWorkspaceSelected(String workspace) {
+		if (parentHost != null) {
+			parentHost.onEditorWorkspaceSelected(workspace);
+		}
+	}
 }

+ 5 - 0
platform/android/java/lib/src/org/godotengine/godot/GodotHost.java

@@ -145,4 +145,9 @@ public interface GodotHost {
 	default boolean supportsFeature(String featureTag) {
 		return false;
 	}
+
+	/**
+	 * Invoked on the render thread when an editor workspace has been selected.
+	 */
+	default void onEditorWorkspaceSelected(String workspace) {}
 }

+ 28 - 0
platform/android/java/lib/src/org/godotengine/godot/GodotLib.java

@@ -196,6 +196,30 @@ public class GodotLib {
 	 */
 	public static native String getEditorSetting(String settingKey);
 
+	/**
+	 * Update the 'key' editor setting with the given data. Must be called on the render thread.
+	 * @param key
+	 * @param data
+	 */
+	public static native void setEditorSetting(String key, Object data);
+
+	/**
+	 * Used to access project metadata from the editor settings. Must be accessed on the render thread.
+	 * @param section
+	 * @param key
+	 * @param defaultValue
+	 * @return
+	 */
+	public static native Object getEditorProjectMetadata(String section, String key, Object defaultValue);
+
+	/**
+	 * Set the project metadata to the editor settings. Must be accessed on the render thread.
+	 * @param section
+	 * @param key
+	 * @param data
+	 */
+	public static native void setEditorProjectMetadata(String section, String key, Object data);
+
 	/**
 	 * Invoke method |p_method| on the Godot object specified by |p_id|
 	 * @param p_id Id of the Godot object to invoke
@@ -267,4 +291,8 @@ public class GodotLib {
 	 * @return the project resource directory
 	 */
 	public static native String getProjectResourceDir();
+
+	static native boolean isEditorHint();
+
+	static native boolean isProjectManagerHint();
 }

+ 3 - 3
platform/android/java/lib/src/org/godotengine/godot/utils/DialogUtils.kt

@@ -52,7 +52,7 @@ import kotlin.math.abs
 /**
  * Utility class for managing dialogs.
  */
-internal class DialogUtils {
+class DialogUtils {
 	companion object {
 		private val TAG = DialogUtils::class.java.simpleName
 
@@ -79,7 +79,7 @@ internal class DialogUtils {
 		 * @param message The message displayed in the dialog.
 		 * @param buttons An array of button labels to display.
 		 */
-		fun showDialog(activity: Activity, title: String, message: String, buttons: Array<String>) {
+		internal fun showDialog(activity: Activity, title: String, message: String, buttons: Array<String>) {
 			var dismissDialog: () -> Unit = {} // Helper to dismiss the Dialog when a button is clicked.
 			activity.runOnUiThread {
 				val builder = AlertDialog.Builder(activity)
@@ -174,7 +174,7 @@ internal class DialogUtils {
 		 * @param message The message displayed in the input dialog.
 		 * @param existingText The existing text that will be pre-filled in the input field.
 		 */
-		fun showInputDialog(activity: Activity, title: String, message: String, existingText: String) {
+		internal fun showInputDialog(activity: Activity, title: String, message: String, existingText: String) {
 			val inputField = EditText(activity)
 			val paddingHorizontal = activity.resources.getDimensionPixelSize(R.dimen.dialog_padding_horizontal)
 			val paddingVertical = activity.resources.getDimensionPixelSize(R.dimen.dialog_padding_vertical)

+ 117 - 0
platform/android/java/lib/src/org/godotengine/godot/utils/GameMenuUtils.kt

@@ -0,0 +1,117 @@
+/**************************************************************************/
+/*  GameMenuUtils.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.utils
+
+import android.util.Log
+import org.godotengine.godot.GodotLib
+
+/**
+ * Utility class for accessing and using game menu APIs.
+ */
+object GameMenuUtils {
+	private val TAG = GameMenuUtils::class.java.simpleName
+
+	/**
+	 * Enum representing the "run/window_placement/game_embed_mode" editor settings.
+ 	 */
+	enum class GameEmbedMode(internal val nativeValue: Int) {
+		DISABLED(-1), AUTO(0), ENABLED(1);
+
+		companion object {
+			internal const val SETTING_KEY = "run/window_placement/game_embed_mode"
+
+			@JvmStatic
+			internal fun fromNativeValue(nativeValue: Int): GameEmbedMode? {
+				for (mode in GameEmbedMode.entries) {
+					if (mode.nativeValue == nativeValue) {
+						return mode
+					}
+				}
+				return null
+			}
+		}
+	}
+
+	@JvmStatic
+	external fun setSuspend(enabled: Boolean)
+
+	@JvmStatic
+	external fun nextFrame()
+
+	@JvmStatic
+	external fun setNodeType(type: Int)
+
+	@JvmStatic
+	external fun setSelectMode(mode: Int)
+
+	@JvmStatic
+	external fun setSelectionVisible(visible: Boolean)
+
+	@JvmStatic
+	external fun setCameraOverride(enabled: Boolean)
+
+	@JvmStatic
+	external fun setCameraManipulateMode(mode: Int)
+
+	@JvmStatic
+	external fun resetCamera2DPosition()
+
+	@JvmStatic
+	external fun resetCamera3DPosition()
+
+	@JvmStatic
+	external fun playMainScene()
+
+	/**
+	 * Returns [GameEmbedMode] stored in the editor settings.
+	 *
+	 * Must be called on the render thread.
+	 */
+	fun fetchGameEmbedMode(): GameEmbedMode {
+		try {
+			val gameEmbedModeValue = Integer.parseInt(GodotLib.getEditorSetting(GameEmbedMode.SETTING_KEY))
+			val gameEmbedMode = GameEmbedMode.fromNativeValue(gameEmbedModeValue) ?: GameEmbedMode.AUTO
+			return gameEmbedMode
+		} catch (e: Exception) {
+			Log.w(TAG, "Unable to retrieve game embed mode", e)
+			return GameEmbedMode.AUTO
+		}
+	}
+
+	/**
+	 * Update the 'game_embed_mode' editor setting.
+	 *
+	 * Must be called on the render thread.
+	 */
+	fun saveGameEmbedMode(gameEmbedMode: GameEmbedMode) {
+		GodotLib.setEditorSetting(GameEmbedMode.SETTING_KEY, gameEmbedMode.nativeValue)
+	}
+}

+ 58 - 0
platform/android/java_godot_lib_jni.cpp

@@ -487,6 +487,49 @@ JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getEditorSetting(J
 	return env->NewStringUTF(editor_setting_value.utf8().get_data());
 }
 
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setEditorSetting(JNIEnv *env, jclass clazz, jstring p_key, jobject p_data) {
+#ifdef TOOLS_ENABLED
+	if (EditorSettings::get_singleton() != nullptr) {
+		String key = jstring_to_string(p_key, env);
+		Variant data = _jobject_to_variant(env, p_data);
+		EditorSettings::get_singleton()->set(key, data);
+	}
+#else
+	WARN_PRINT("Access to the Editor Settings in only available on Editor builds");
+#endif
+}
+
+JNIEXPORT jobject JNICALL Java_org_godotengine_godot_GodotLib_getEditorProjectMetadata(JNIEnv *env, jclass clazz, jstring p_section, jstring p_key, jobject p_default_value) {
+	jvalret result;
+
+#ifdef TOOLS_ENABLED
+	if (EditorSettings::get_singleton() != nullptr) {
+		String section = jstring_to_string(p_section, env);
+		String key = jstring_to_string(p_key, env);
+		Variant default_value = _jobject_to_variant(env, p_default_value);
+		Variant data = EditorSettings::get_singleton()->get_project_metadata(section, key, default_value);
+		result = _variant_to_jvalue(env, data.get_type(), &data, true);
+	}
+#else
+	WARN_PRINT("Access to the Editor Settings Project Metadata is only available on Editor builds");
+#endif
+
+	return result.obj;
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setEditorProjectMetadata(JNIEnv *env, jclass clazz, jstring p_section, jstring p_key, jobject p_data) {
+#ifdef TOOLS_ENABLED
+	if (EditorSettings::get_singleton() != nullptr) {
+		String section = jstring_to_string(p_section, env);
+		String key = jstring_to_string(p_key, env);
+		Variant data = _jobject_to_variant(env, p_data);
+		EditorSettings::get_singleton()->set_project_metadata(section, key, data);
+	}
+#else
+	WARN_PRINT("Access to the Editor Settings Project Metadata is only available on Editor builds");
+#endif
+}
+
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz) {
 	DisplayServerAndroid *ds = (DisplayServerAndroid *)DisplayServer::get_singleton();
 	if (ds) {
@@ -555,4 +598,19 @@ JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getProjectResource
 	const String resource_dir = OS::get_singleton()->get_resource_dir();
 	return env->NewStringUTF(resource_dir.utf8().get_data());
 }
+JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_isEditorHint(JNIEnv *env, jclass clazz) {
+	Engine *engine = Engine::get_singleton();
+	if (engine) {
+		return engine->is_editor_hint();
+	}
+	return false;
+}
+
+JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_isProjectManagerHint(JNIEnv *env, jclass clazz) {
+	Engine *engine = Engine::get_singleton();
+	if (engine) {
+		return engine->is_project_manager_hint();
+	}
+	return false;
+}
 }

+ 5 - 0
platform/android/java_godot_lib_jni.h

@@ -62,6 +62,9 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env,
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusout(JNIEnv *env, jclass clazz);
 JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getGlobal(JNIEnv *env, jclass clazz, jstring path);
 JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getEditorSetting(JNIEnv *env, jclass clazz, jstring p_setting_key);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setEditorSetting(JNIEnv *env, jclass clazz, jstring p_key, jobject p_data);
+JNIEXPORT jobject JNICALL Java_org_godotengine_godot_GodotLib_getEditorProjectMetadata(JNIEnv *env, jclass clazz, jstring p_section, jstring p_key, jobject p_default_value);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setEditorProjectMetadata(JNIEnv *env, jclass clazz, jstring p_section, jstring p_key, jobject p_data);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHeight(JNIEnv *env, jclass clazz, jint p_height);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz);
@@ -70,6 +73,8 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNI
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz);
 JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInputToRenderThread(JNIEnv *env, jclass clazz);
 JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getProjectResourceDir(JNIEnv *env, jclass clazz);
+JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_isEditorHint(JNIEnv *env, jclass clazz);
+JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_isProjectManagerHint(JNIEnv *env, jclass clazz);
 }
 
 #endif // JAVA_GODOT_LIB_JNI_H

+ 11 - 0
platform/android/java_godot_wrapper.cpp

@@ -93,6 +93,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
 	_verify_apk = p_env->GetMethodID(godot_class, "nativeVerifyApk", "(Ljava/lang/String;)I");
 	_enable_immersive_mode = p_env->GetMethodID(godot_class, "nativeEnableImmersiveMode", "(Z)V");
 	_is_in_immersive_mode = p_env->GetMethodID(godot_class, "isInImmersiveMode", "()Z");
+	_on_editor_workspace_selected = p_env->GetMethodID(godot_class, "nativeOnEditorWorkspaceSelected", "(Ljava/lang/String;)V");
 }
 
 GodotJavaWrapper::~GodotJavaWrapper() {
@@ -588,3 +589,13 @@ bool GodotJavaWrapper::is_in_immersive_mode() {
 		return false;
 	}
 }
+
+void GodotJavaWrapper::on_editor_workspace_selected(const String &p_workspace) {
+	if (_on_editor_workspace_selected) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_NULL(env);
+
+		jstring j_workspace = env->NewStringUTF(p_workspace.utf8().get_data());
+		env->CallVoidMethod(godot_instance, _on_editor_workspace_selected, j_workspace);
+	}
+}

+ 3 - 0
platform/android/java_godot_wrapper.h

@@ -84,6 +84,7 @@ private:
 	jmethodID _verify_apk = nullptr;
 	jmethodID _enable_immersive_mode = nullptr;
 	jmethodID _is_in_immersive_mode = nullptr;
+	jmethodID _on_editor_workspace_selected = nullptr;
 
 public:
 	GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance);
@@ -137,6 +138,8 @@ public:
 
 	void enable_immersive_mode(bool p_enabled);
 	bool is_in_immersive_mode();
+
+	void on_editor_workspace_selected(const String &p_workspace);
 };
 
 #endif // JAVA_GODOT_WRAPPER_H

+ 30 - 0
platform/android/os_android.cpp

@@ -43,6 +43,10 @@
 #include "core/io/xml_parser.h"
 #include "drivers/unix/dir_access_unix.h"
 #include "drivers/unix/file_access_unix.h"
+#ifdef TOOLS_ENABLED
+#include "editor/editor_node.h"
+#include "editor/plugins/game_view_plugin.h"
+#endif
 #include "main/main.h"
 #include "scene/main/scene_tree.h"
 #include "servers/rendering_server.h"
@@ -331,6 +335,15 @@ void OS_Android::main_loop_begin() {
 	if (main_loop) {
 		main_loop->initialize();
 	}
+
+#ifdef TOOLS_ENABLED
+	if (Engine::get_singleton()->is_editor_hint()) {
+		GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+		if (game_view_plugin != nullptr) {
+			game_view_plugin->connect("main_screen_changed", callable_mp_static(&OS_Android::_on_main_screen_changed));
+		}
+	}
+#endif
 }
 
 bool OS_Android::main_loop_iterate(bool *r_should_swap_buffers) {
@@ -353,6 +366,15 @@ bool OS_Android::main_loop_iterate(bool *r_should_swap_buffers) {
 }
 
 void OS_Android::main_loop_end() {
+#ifdef TOOLS_ENABLED
+	if (Engine::get_singleton()->is_editor_hint()) {
+		GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+		if (game_view_plugin != nullptr) {
+			game_view_plugin->disconnect("main_screen_changed", callable_mp_static(&OS_Android::_on_main_screen_changed));
+		}
+	}
+#endif
+
 	if (main_loop) {
 		SceneTree *scene_tree = Object::cast_to<SceneTree>(main_loop);
 		if (scene_tree) {
@@ -362,6 +384,14 @@ void OS_Android::main_loop_end() {
 	}
 }
 
+#ifdef TOOLS_ENABLED
+void OS_Android::_on_main_screen_changed(const String &p_screen_name) {
+	if (OS_Android::get_singleton() != nullptr && OS_Android::get_singleton()->get_godot_java() != nullptr) {
+		OS_Android::get_singleton()->get_godot_java()->on_editor_workspace_selected(p_screen_name);
+	}
+}
+#endif
+
 void OS_Android::main_loop_focusout() {
 	DisplayServerAndroid::get_singleton()->send_window_event(DisplayServer::WINDOW_EVENT_FOCUS_OUT);
 	if (OS::get_singleton()->get_main_loop()) {

+ 4 - 0
platform/android/os_android.h

@@ -187,6 +187,10 @@ private:
 	String get_dynamic_libraries_path() const;
 	// Copy a dynamic library to the given location to make it accessible for loading.
 	bool copy_dynamic_library(const String &p_library_path, const String &p_target_dir, String *r_copy_path = nullptr);
+
+#ifdef TOOLS_ENABLED
+	static void _on_main_screen_changed(const String &p_screen_name);
+#endif
 };
 
 #endif // OS_ANDROID_H