Skip to content

Commit

Permalink
Merge pull request #245 from dsnopek/meta-hybrid-apps
Browse files Browse the repository at this point in the history
Support making hybrid apps for Meta headsets
  • Loading branch information
dsnopek authored Jan 21, 2025
2 parents ebb81f4 + deb9867 commit 7df9262
Show file tree
Hide file tree
Showing 35 changed files with 2,952 additions and 6 deletions.
4 changes: 3 additions & 1 deletion config.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ ext {
kotlinVersion : '1.9.20',
cmakeVersion : '3.22.1',
ndkVersion : '23.2.8568313',
openxrVersion : '1.0.34'
openxrVersion : '1.0.34',
fragmentVersion : '1.7.1',
splashscreenVersion : '1.0.1',
]

libraries = [
Expand Down
3 changes: 3 additions & 0 deletions plugin/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ android {
dependencies {
compileOnly libraries.godotAndroidLib

implementation "androidx.fragment:fragment:$versions.fragmentVersion"
implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion"

// Khronos dependencies
khronosImplementation "org.khronos.openxr:openxr_loader_for_android:$versions.openxrVersion"

Expand Down
115 changes: 115 additions & 0 deletions plugin/src/main/cpp/classes/openxr_hybrid_app.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**************************************************************************/
/* openxr_hybrid_app.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT XR */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2022-present Godot XR contributors (see CONTRIBUTORS.md) */
/* */
/* 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 "classes/openxr_hybrid_app.h"

#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/os.hpp>
#include <godot_cpp/core/class_db.hpp>

OpenXRHybridApp *OpenXRHybridApp::singleton = nullptr;

void OpenXRHybridApp::_bind_methods() {
ClassDB::bind_method(D_METHOD("is_hybrid_app"), &OpenXRHybridApp::is_hybrid_app);
ClassDB::bind_method(D_METHOD("get_mode"), &OpenXRHybridApp::get_mode);
ClassDB::bind_method(D_METHOD("switch_mode", "mode", "data"), &OpenXRHybridApp::switch_mode, DEFVAL(String()));
ClassDB::bind_method(D_METHOD("get_launch_data"), &OpenXRHybridApp::get_launch_data);

BIND_ENUM_CONSTANT(HYBRID_MODE_NONE);
BIND_ENUM_CONSTANT(HYBRID_MODE_IMMERSIVE);
BIND_ENUM_CONSTANT(HYBRID_MODE_PANEL);
}

bool OpenXRHybridApp::is_hybrid_app() const {
OS *os = OS::get_singleton();
ERR_FAIL_NULL_V(os, false);

return os->has_feature("godot_openxr_hybrid_app");
}

OpenXRHybridApp::HybridMode OpenXRHybridApp::get_mode() const {
OS *os = OS::get_singleton();
ERR_FAIL_NULL_V(os, HYBRID_MODE_NONE);

if (!is_hybrid_app()) {
return HYBRID_MODE_NONE;
}

if (os->has_feature("godot_openxr_panel_app")) {
return HYBRID_MODE_PANEL;
}

return HYBRID_MODE_IMMERSIVE;
}

bool OpenXRHybridApp::switch_mode(HybridMode p_mode, const String &p_data) {
ERR_FAIL_COND_V(p_mode == HYBRID_MODE_NONE, false);

if (!is_hybrid_app()) {
return false;
}

Object *godot_openxr_singleton = Engine::get_singleton()->get_singleton("GodotOpenXRHybridAppInternal");
if (!godot_openxr_singleton) {
return false;
}

return godot_openxr_singleton->call("hybridAppSwitchTo", p_mode, p_data);
}

String OpenXRHybridApp::get_launch_data() const {
if (!is_hybrid_app()) {
return String();
}

Object *godot_openxr_singleton = Engine::get_singleton()->get_singleton("GodotOpenXRHybridAppInternal");
if (!godot_openxr_singleton) {
return String();
}

return godot_openxr_singleton->call("getHybridAppLaunchData");
}

OpenXRHybridApp *OpenXRHybridApp::get_singleton() {
if (singleton == nullptr) {
singleton = memnew(OpenXRHybridApp());
}
return singleton;
}

OpenXRHybridApp::OpenXRHybridApp() :
Object() {
ERR_FAIL_COND_MSG(singleton != nullptr, "An OpenXRHybridApp singleton already exists.");

singleton = this;
}

OpenXRHybridApp::~OpenXRHybridApp() {
singleton = nullptr;
}
45 changes: 45 additions & 0 deletions plugin/src/main/cpp/export/export_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include "export/export_plugin.h"

#include <godot_cpp/classes/editor_export_platform_android.hpp>
#include <godot_cpp/classes/project_settings.hpp>

using namespace godot;

Expand Down Expand Up @@ -90,10 +91,38 @@ Dictionary OpenXREditorExportPlugin::_get_vendor_toggle_option(const String &ven
false);
}

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_openxr_enabled() const {
return _get_int_option("xr_features/xr_mode", REGULAR_MODE_VALUE) == OPENXR_MODE_VALUE;
}

String OpenXREditorExportPlugin::_bool_to_string(bool p_value) const {
return p_value ? "true" : "false";
}

String OpenXREditorExportPlugin::_get_android_orientation_label(DisplayServer::ScreenOrientation screen_orientation) const {
switch (screen_orientation) {
case DisplayServer::SCREEN_PORTRAIT:
return "portrait";
case DisplayServer::SCREEN_REVERSE_LANDSCAPE:
return "reverseLandscape";
case DisplayServer::SCREEN_REVERSE_PORTRAIT:
return "reversePortrait";
case DisplayServer::SCREEN_SENSOR_LANDSCAPE:
return "userLandscape";
case DisplayServer::SCREEN_SENSOR_PORTRAIT:
return "userPortrait";
case DisplayServer::SCREEN_SENSOR:
return "fullUser";
case DisplayServer::SCREEN_LANDSCAPE:
default:
return "landscape";
}
}

TypedArray<Dictionary> OpenXREditorExportPlugin::_get_export_options(const Ref<EditorExportPlatform> &platform) const {
TypedArray<Dictionary> export_options;
if (!_supports_platform(platform)) {
Expand Down Expand Up @@ -128,6 +157,22 @@ String OpenXREditorExportPlugin::_get_export_option_warning(const Ref<EditorExpo
return "";
}

Dictionary OpenXREditorExportPlugin::_get_export_options_overrides(const Ref<EditorExportPlatform> &platform) const {
Dictionary overrides;
if (!_supports_platform(platform) || !_is_vendor_plugin_enabled()) {
return overrides;
}

HybridType hybrid_type = _get_hybrid_app_setting_value();
if (hybrid_type != HYBRID_TYPE_DISABLED) {
// Unless this is a hybrid app that launches as a panel, then we want to force this option on.
// Otherwise, it needs to be off, so that the panel activity can be the one that's launched by default.
overrides["package/show_in_app_library"] = (hybrid_type != HYBRID_TYPE_START_AS_PANEL);
}

return overrides;
}

bool OpenXREditorExportPlugin::_supports_platform(const Ref<EditorExportPlatform> &platform) const {
return platform->is_class(EditorExportPlatformAndroid::get_class_static());
}
Expand Down
5 changes: 3 additions & 2 deletions plugin/src/main/cpp/export/khronos_export_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,12 @@ TypedArray<Dictionary> KhronosEditorExportPlugin::_get_export_options(const Ref<
}

Dictionary KhronosEditorExportPlugin::_get_export_options_overrides(const Ref<godot::EditorExportPlatform> &p_platform) const {
Dictionary overrides;
if (!_supports_platform(p_platform)) {
return overrides;
return Dictionary();
}

Dictionary overrides = OpenXREditorExportPlugin::_get_export_options_overrides(p_platform);

if (!_is_vendor_plugin_enabled()) {
overrides["khronos_xr_features/vendors"] = KHRONOS_VENDOR_OTHER;
}
Expand Down
5 changes: 3 additions & 2 deletions plugin/src/main/cpp/export/magicleap_export_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,12 @@ TypedArray<Dictionary> MagicleapEditorExportPlugin::_get_export_options(const Re
}

Dictionary MagicleapEditorExportPlugin::_get_export_options_overrides(const Ref<godot::EditorExportPlatform> &p_platform) const {
Dictionary overrides;
if (!_supports_platform(p_platform)) {
return overrides;
return Dictionary();
}

Dictionary overrides = OpenXREditorExportPlugin::_get_export_options_overrides(p_platform);

if (!_is_vendor_plugin_enabled()) {
overrides["magicleap_xr_features/hand_tracking"] = MANIFEST_FALSE_VALUE;
}
Expand Down
39 changes: 39 additions & 0 deletions plugin/src/main/cpp/export/meta_export_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@ PackedStringArray MetaEditorExportPlugin::_get_export_features(const Ref<EditorE
features.append(EYE_GAZE_INTERACTION_FEATURE);
}

// Add a feature to indicate that this is a hybrid app.
if (_is_openxr_enabled() && _get_hybrid_app_setting_value() != HYBRID_TYPE_DISABLED) {
features.append(HYBRID_APP_FEATURE);
}

return features;
}

Expand Down Expand Up @@ -475,6 +480,40 @@ String MetaEditorExportPlugin::_get_android_manifest_application_element_content
contents += " <meta-data android:name=\"com.oculus.ossplash.background\" android:value=\"passthrough-contextual\" />\n";
}

HybridType hybrid_type = _get_hybrid_app_setting_value();
if (hybrid_type != HYBRID_TYPE_DISABLED) {
ProjectSettings *project_settings = ProjectSettings::get_singleton();

contents += vformat(
" <activity android:name=\"org.godotengine.openxr.vendors.GodotPanelApp\" "
"android:process=\":GodotPanelApp\" "
"android:theme=\"@style/GodotPanelAppSplashTheme\" "
"android:launchMode=\"singleInstancePerTask\" "
"android:exported=\"true\" "
"android:excludeFromRecents=\"%s\" "
"android:screenOrientation=\"%s\" "
"android:resizeableActivity=\"%s\" "
"android:configChanges=\"orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode\" "
"tools:ignore=\"UnusedAttribute\">\n",
_bool_to_string(_get_bool_option("package/exclude_from_recents")),
_get_android_orientation_label((DisplayServer::ScreenOrientation)(int)project_settings->get_setting_with_override("display/window/handheld/orientation")),
_bool_to_string(project_settings->get_setting_with_override("display/window/size/resizable")));

contents +=
" <intent-filter>\n"
" <action android:name=\"android.intent.action.MAIN\" />\n"
" <category android:name=\"android.intent.category.DEFAULT\" />\n"
" <category android:name=\"com.oculus.intent.category.2D\" />\n";

if (hybrid_type == HYBRID_TYPE_START_AS_PANEL) {
contents += " <category android:name=\"android.intent.category.LAUNCHER\" />\n";
}

contents +=
" </intent-filter>\n"
" </activity>\n";
}

return contents;
}

Expand Down
65 changes: 65 additions & 0 deletions plugin/src/main/cpp/include/classes/openxr_hybrid_app.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**************************************************************************/
/* openxr_hybrid_app.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT XR */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2022-present Godot XR contributors (see CONTRIBUTORS.md) */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

#pragma once

#include <godot_cpp/classes/object.hpp>
#include <godot_cpp/core/binder_common.hpp>

using namespace godot;

// Singleton providing an API for hybrid apps.
class OpenXRHybridApp : public Object {
GDCLASS(OpenXRHybridApp, Object);

static OpenXRHybridApp *singleton;

protected:
static void _bind_methods();

public:
enum HybridMode {
HYBRID_MODE_NONE = -1,
HYBRID_MODE_IMMERSIVE = 0,
HYBRID_MODE_PANEL = 1,
};

static OpenXRHybridApp *get_singleton();

bool is_hybrid_app() const;
HybridMode get_mode() const;

bool switch_mode(HybridMode p_mode, const String &p_data);
String get_launch_data() const;

OpenXRHybridApp();
virtual ~OpenXRHybridApp();
};

VARIANT_ENUM_CAST(OpenXRHybridApp::HybridMode);
Loading

0 comments on commit 7df9262

Please sign in to comment.