diff --git a/plugin/src/main/cpp/export/export_plugin.cpp b/plugin/src/main/cpp/export/export_plugin.cpp index 6245de4d..e365f273 100644 --- a/plugin/src/main/cpp/export/export_plugin.cpp +++ b/plugin/src/main/cpp/export/export_plugin.cpp @@ -91,8 +91,15 @@ Dictionary OpenXREditorExportPlugin::_get_vendor_toggle_option(const String &ven true); } -OpenXREditorExportPlugin::HybridType OpenXREditorExportPlugin::_get_hybrid_app_setting_value() const { - return (HybridType)(int)ProjectSettings::get_singleton()->get_setting_with_override("xr/openxr/hybrid_app"); +bool OpenXREditorExportPlugin::_is_hybrid_app_enabled() const { + return ProjectSettings::get_singleton()->get_setting_with_override("xr/hybrid_app/enabled"); +} + +OpenXRHybridApp::HybridMode OpenXREditorExportPlugin::_get_hybrid_app_launch_mode() const { + if (!_is_hybrid_app_enabled()) { + return OpenXRHybridApp::HYBRID_MODE_NONE; + } + return (OpenXRHybridApp::HybridMode)(int)ProjectSettings::get_singleton()->get_setting_with_override("xr/hybrid_app/launch_mode"); } String OpenXREditorExportPlugin::_get_opening_activity_tag_for_panel_app() const { @@ -127,6 +134,12 @@ String OpenXREditorExportPlugin::_get_common_activity_intent_filter_contents() c )"; } + + if (_is_hybrid_app_enabled()) { + contents += R"( + +)"; + } return contents; } @@ -198,11 +211,11 @@ Dictionary OpenXREditorExportPlugin::_get_export_options_overrides(const Ref\n"; } - HybridType hybrid_type = _get_hybrid_app_setting_value(); - if (hybrid_type != HYBRID_TYPE_DISABLED) { + OpenXRHybridApp::HybridMode hybrid_launch_mode = _get_hybrid_app_launch_mode(); + if (hybrid_launch_mode != OpenXRHybridApp::HYBRID_MODE_NONE) { contents += _get_opening_activity_tag_for_panel_app(); contents += " \n" " \n" " \n" + " \n" " \n"; - if (hybrid_type == HYBRID_TYPE_START_AS_PANEL) { + if (hybrid_launch_mode == OpenXRHybridApp::HYBRID_MODE_PANEL) { contents += " \n"; } @@ -545,6 +546,8 @@ String MetaEditorExportPlugin::_get_android_manifest_activity_element_contents(c + + diff --git a/plugin/src/main/cpp/include/export/export_plugin.h b/plugin/src/main/cpp/include/export/export_plugin.h index 39f0332a..15bb2686 100644 --- a/plugin/src/main/cpp/include/export/export_plugin.h +++ b/plugin/src/main/cpp/include/export/export_plugin.h @@ -29,6 +29,8 @@ #pragma once +#include "classes/openxr_hybrid_app.h" + #include #include #include @@ -70,12 +72,6 @@ class OpenXREditorExportPlugin : public EditorExportPlugin { GDCLASS(OpenXREditorExportPlugin, EditorExportPlugin) public: - enum HybridType { - HYBRID_TYPE_DISABLED, - HYBRID_TYPE_START_AS_IMMERSIVE, - HYBRID_TYPE_START_AS_PANEL, - }; - OpenXREditorExportPlugin(); String _get_name() const override; @@ -114,7 +110,8 @@ class OpenXREditorExportPlugin : public EditorExportPlugin { Dictionary _get_vendor_toggle_option(const String &vendor_name) const; - HybridType _get_hybrid_app_setting_value() const; + bool _is_hybrid_app_enabled() const; + OpenXRHybridApp::HybridMode _get_hybrid_app_launch_mode() const; String _get_opening_activity_tag_for_panel_app() const; diff --git a/plugin/src/main/cpp/register_types.cpp b/plugin/src/main/cpp/register_types.cpp index 580f4968..dc774e4e 100644 --- a/plugin/src/main/cpp/register_types.cpp +++ b/plugin/src/main/cpp/register_types.cpp @@ -291,19 +291,32 @@ void add_plugin_project_settings() { } { - String hybrid_app_setting = "xr/openxr/hybrid_app"; - if (!project_settings->has_setting(hybrid_app_setting)) { - project_settings->set_setting(hybrid_app_setting, OpenXRHybridApp::HYBRID_MODE_NONE); + String hybrid_app_enabled_setting = "xr/hybrid_app/enabled"; + if (!project_settings->has_setting(hybrid_app_enabled_setting)) { + project_settings->set_setting(hybrid_app_enabled_setting, false); } - project_settings->set_initial_value(hybrid_app_setting, OpenXRHybridApp::HYBRID_MODE_NONE); - project_settings->set_as_basic(hybrid_app_setting, false); - Dictionary property_info; - property_info["name"] = hybrid_app_setting; - property_info["type"] = Variant::Type::INT; - property_info["hint"] = PROPERTY_HINT_ENUM; - property_info["hint_string"] = "Disabled:-1,Start As Immersive:0,Start As Panel:1"; - project_settings->add_property_info(property_info); + project_settings->set_initial_value(hybrid_app_enabled_setting, false); + project_settings->set_as_basic(hybrid_app_enabled_setting, true); + Dictionary hybrid_app_enabled_property_info; + hybrid_app_enabled_property_info["name"] = hybrid_app_enabled_setting; + hybrid_app_enabled_property_info["type"] = Variant::Type::BOOL; + hybrid_app_enabled_property_info["hint"] = PROPERTY_HINT_NONE; + project_settings->add_property_info(hybrid_app_enabled_property_info); + + String hybrid_app_launch_mode_setting = "xr/hybrid_app/launch_mode"; + if (!project_settings->has_setting(hybrid_app_launch_mode_setting)) { + project_settings->set_setting(hybrid_app_launch_mode_setting, OpenXRHybridApp::HYBRID_MODE_IMMERSIVE); + } + + project_settings->set_initial_value(hybrid_app_launch_mode_setting, OpenXRHybridApp::HYBRID_MODE_IMMERSIVE); + project_settings->set_as_basic(hybrid_app_launch_mode_setting, true); + Dictionary hybrid_app_launch_mode_property_info; + hybrid_app_launch_mode_property_info["name"] = hybrid_app_launch_mode_setting; + hybrid_app_launch_mode_property_info["type"] = Variant::Type::INT; + hybrid_app_launch_mode_property_info["hint"] = PROPERTY_HINT_ENUM; + hybrid_app_launch_mode_property_info["hint_string"] = "Start As Immersive:0,Start As Panel:1"; + project_settings->add_property_info(hybrid_app_launch_mode_property_info); } } diff --git a/plugin/src/main/java/org/godotengine/openxr/vendors/GodotOpenXRHybridAppInternal.kt b/plugin/src/main/java/org/godotengine/openxr/vendors/GodotOpenXRHybridAppInternal.kt index 060e0fa0..24af0184 100644 --- a/plugin/src/main/java/org/godotengine/openxr/vendors/GodotOpenXRHybridAppInternal.kt +++ b/plugin/src/main/java/org/godotengine/openxr/vendors/GodotOpenXRHybridAppInternal.kt @@ -30,13 +30,16 @@ package org.godotengine.openxr.vendors import android.app.Activity -import android.content.ComponentName +import android.app.PendingIntent import android.content.Intent import android.util.Log -import android.view.View; +import android.view.View import org.godotengine.godot.Godot +import org.godotengine.godot.GodotHost +import org.godotengine.godot.GodotLib import org.godotengine.godot.plugin.GodotPlugin import org.godotengine.godot.plugin.UsedByGodot +import org.godotengine.godot.utils.isHorizonOSDevice import org.godotengine.godot.utils.isNativeXRDevice /** @@ -44,55 +47,126 @@ import org.godotengine.godot.utils.isNativeXRDevice */ class GodotOpenXRHybridAppInternal(godot: Godot?) : GodotPlugin(godot) { companion object { - protected val TAG = GodotOpenXRHybridAppInternal::class.java.simpleName + private val TAG = GodotOpenXRHybridAppInternal::class.java.simpleName - private const val IMMERSIVE_ACTIVITY = "com.godot.game.GodotApp" - private const val PANEL_ACTIVITY = "org.godotengine.openxr.vendors.GodotPanelApp" + private const val EXTRA_HYBRID_LAUNCH_DATA = "godot_openxr_vendors_hybrid_launch_data" - private const val IMMERSIVE_MODE = 0 - private const val PANEL_MODE = 1 + // TODO: Duplicate from the Godot Android library; should remove once the lib dependency + // is updated + const val HYBRID_APP_PANEL_FEATURE = "godot_openxr_panel_app" + const val HYBRID_APP_PANEL_CATEGORY = "org.godotengine.xr.hybrid.PANEL" + const val HYBRID_APP_IMMERSIVE_CATEGORY = "org.godotengine.xr.hybrid.IMMERSIVE" - private const val INTENT_EXTRA_DATA = "godot_openxr_vendors_data" + private fun getHybridMode(activity: Activity?): HybridMode { + if (activity !is GodotHost) { + return HybridMode.NONE + } + + // Check if hybrid is enabled + val hybridEnabled = GodotLib.getGlobal("xr/hybrid_app/enabled").toBoolean() + if (!hybridEnabled) { + return HybridMode.NONE + } + + if (activity.supportsFeature(HYBRID_APP_PANEL_FEATURE)) { + return HybridMode.PANEL + } + + return HybridMode.IMMERSIVE + } + } + + /** + * Should match OpenXRHybridApp#HybridMode. + */ + private enum class HybridMode(private val nativeValue: Int) { + NONE( -1), + IMMERSIVE(0), + PANEL(1); + + companion object { + fun fromNative(nativeValue: Int): HybridMode { + for (mode in HybridMode.entries) { + if (mode.nativeValue == nativeValue) { + return mode + } + } + return NONE + } + } } - private var hybridMode: Int = IMMERSIVE_MODE + private var hybridMode = HybridMode.IMMERSIVE private var hybridLaunchData: String = "" override fun getPluginName() = "GodotOpenXRHybridAppInternal" override fun onMainCreate(activity: Activity): View? { - if (activity::class.qualifiedName == PANEL_ACTIVITY) { - hybridMode = PANEL_MODE - } else { - hybridMode = IMMERSIVE_MODE - } - - hybridLaunchData = activity.intent.getStringExtra(INTENT_EXTRA_DATA) ?: "" - + hybridLaunchData = activity.intent.getStringExtra(EXTRA_HYBRID_LAUNCH_DATA) ?: "" return null } + override fun onGodotSetupCompleted() { + super.onGodotSetupCompleted() + hybridMode = getHybridMode(activity) + } + @UsedByGodot - fun hybridAppSwitchTo(mode: Int, data: String = ""): Boolean { + fun hybridAppSwitchTo(modeValue: Int, data: String = ""): Boolean { + val mode = HybridMode.fromNative(modeValue) if (hybridMode == mode) return false - val context = getActivity() ?: return false + val context = activity ?: return false if (!isNativeXRDevice(context)) return false - val activityName = if (mode == IMMERSIVE_MODE) IMMERSIVE_ACTIVITY else PANEL_ACTIVITY - val newInstance = Intent() - .setComponent(ComponentName(context, activityName)) + val hybridCategory = if (mode == HybridMode.IMMERSIVE) HYBRID_APP_IMMERSIVE_CATEGORY else HYBRID_APP_PANEL_CATEGORY + val hybridLaunchIntent = Intent() + .addCategory(hybridCategory) + .setPackage(context.packageName) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(INTENT_EXTRA_DATA, data) + .putExtra(EXTRA_HYBRID_LAUNCH_DATA, data) + + // Resolve the component needed for the launch + val hybridLaunchComponentName = hybridLaunchIntent.resolveActivity(context.packageManager) + if (hybridLaunchComponentName == null) { + Log.e(TAG, "Unable to resolve hybrid mode launch intent $hybridLaunchIntent") + return false + } else { + Log.d(TAG, "Resolved hybrid launch component: $hybridLaunchComponentName") + hybridLaunchIntent.setComponent(hybridLaunchComponentName) + } + + val launchIntent = if (mode == HybridMode.PANEL && isHorizonOSDevice(context)) { + // HorizonOS has a different launch flow for panel mode. + // Wrap the created Intent in a PendingIntent object + val pendingPanelIntent = + PendingIntent.getActivity( + context, + 0, + hybridLaunchIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + + // Create and send the Intent to launch the Home environment, providing the + // PendingIntent object as extra parameters + val homeIntent = + Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_HOME) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra("extra_launch_in_home_pending_intent", pendingPanelIntent) + + homeIntent + } else { + hybridLaunchIntent + } val godot = godot if (godot != null) { godot.destroyAndKillProcess { - context.startActivity(newInstance) + context.startActivity(launchIntent) } } else { - context.startActivity(newInstance) + context.startActivity(launchIntent) context.finish() } diff --git a/plugin/src/main/java/org/godotengine/openxr/vendors/GodotPanelApp.kt b/plugin/src/main/java/org/godotengine/openxr/vendors/GodotPanelApp.kt index 4303104b..3b2589d8 100644 --- a/plugin/src/main/java/org/godotengine/openxr/vendors/GodotPanelApp.kt +++ b/plugin/src/main/java/org/godotengine/openxr/vendors/GodotPanelApp.kt @@ -89,7 +89,7 @@ class GodotPanelApp : GodotActivity() { } override fun supportsFeature(featureTag: String): Boolean { - if ("godot_openxr_panel_app" == featureTag) { + if (GodotOpenXRHybridAppInternal.HYBRID_APP_PANEL_FEATURE == featureTag) { return true } diff --git a/samples/.gitignore b/samples/.gitignore index fbf95ea5..32e1a3a8 100644 --- a/samples/.gitignore +++ b/samples/.gitignore @@ -2,3 +2,4 @@ */.godot/ */addons/godotopenxrvendors */android/ +builds/ diff --git a/samples/hybrid-app-sample/export_presets.cfg b/samples/hybrid-app-sample/export_presets.cfg index 6d6deecd..ea8039e5 100644 --- a/samples/hybrid-app-sample/export_presets.cfg +++ b/samples/hybrid-app-sample/export_presets.cfg @@ -50,7 +50,7 @@ launcher_icons/adaptive_background_432x432="" launcher_icons/adaptive_monochrome_432x432="" graphics/opengl_debug=false xr_features/xr_mode=1 -wear_os/swipe_to_dismiss=true +gesture/swipe_to_dismiss=false screen/immersive_mode=true screen/support_small=true screen/support_normal=true @@ -245,3 +245,4 @@ pico_xr_features/face_tracking=0 pico_xr_features/hand_tracking=0 xr_features/enable_magicleap_plugin=false magicleap_xr_features/hand_tracking=0 +meta_xr_features/instant_splash_screen=false diff --git a/samples/hybrid-app-sample/project.godot b/samples/hybrid-app-sample/project.godot index a626014b..09954e48 100644 --- a/samples/hybrid-app-sample/project.godot +++ b/samples/hybrid-app-sample/project.godot @@ -37,8 +37,10 @@ textures/vram_compression/import_etc2_astc=true openxr/enabled=true openxr/reference_space=2 +openxr/environment_blend_mode=2 openxr/foveation_level=3 openxr/foveation_dynamic=true openxr/startup_alert=false shaders/enabled=true -openxr/hybrid_app=1 +hybrid_app/enabled=true +hybrid_app/launch_mode=1