Clean up the XR editor logic

- Coalesce common logic into the `main` flavor to avoid duplication
- Code cleanup
This commit is contained in:
Fredia Huya-Kouadio
2025-01-16 09:40:30 -08:00
parent d33da79d3f
commit b4f25b1863
13 changed files with 86 additions and 214 deletions

View File

@ -2840,6 +2840,13 @@ void EditorExportPlatformAndroid::get_command_line_flags(const Ref<EditorExportP
command_line_strings.push_back("--xr_mode_openxr");
} else { // XRMode.REGULAR is the default.
command_line_strings.push_back("--xr_mode_regular");
// Also override the 'xr/openxr/enabled' project setting.
// This is useful for multi-platforms projects supporting both XR and non-XR devices. The project would need
// to enable openxr for development, and would create multiple XR and non-XR export presets.
// These command line args ensure that the non-XR export presets will have openxr disabled.
command_line_strings.push_back("--xr-mode");
command_line_strings.push_back("off");
}
bool immersive = p_preset->get("screen/immersive_mode");

View File

@ -35,5 +35,4 @@ package org.godotengine.editor
*
* This is the implementation of the editor used when running on regular Android devices.
*/
open class GodotEditor : BaseGodotEditor() {
}
open class GodotEditor : BaseGodotEditor()

View File

@ -57,15 +57,8 @@
<activity
android:name=".GodotXRGame"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
android:process=":GodotXRGame"
android:launchMode="singleTask"
android:icon="@mipmap/ic_play_window"
android:label="@string/godot_game_activity_name"
android:exported="false"
android:screenOrientation="landscape"
android:resizeableActivity="false"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
tools:node="merge">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />

View File

@ -30,9 +30,6 @@
package org.godotengine.editor
import org.godotengine.godot.GodotLib
import org.godotengine.godot.utils.isNativeXRDevice
/**
* Primary window of the Godot Editor.
*
@ -40,66 +37,10 @@ import org.godotengine.godot.utils.isNativeXRDevice
*/
open class GodotEditor : BaseGodotEditor() {
companion object {
private val TAG = GodotEditor::class.java.simpleName
/** Default behavior, means we check project settings **/
private const val XR_MODE_DEFAULT = "default"
/**
* Ignore project settings, OpenXR is disabled
*/
private const val XR_MODE_OFF = "off"
/**
* Ignore project settings, OpenXR is enabled
*/
private const val XR_MODE_ON = "on"
internal val XR_RUN_GAME_INFO = EditorWindowInfo(GodotXRGame::class.java, 1667, ":GodotXRGame")
internal val USE_SCENE_PERMISSIONS = listOf("com.oculus.permission.USE_SCENE", "horizonos.permission.USE_SCENE")
}
override fun getExcludedPermissions(): MutableSet<String> {
val excludedPermissions = super.getExcludedPermissions()
// The USE_SCENE permission is requested when the "xr/openxr/enabled" project setting
// is enabled.
excludedPermissions.addAll(USE_SCENE_PERMISSIONS)
return excludedPermissions
}
override fun retrieveEditorWindowInfo(args: Array<String>): EditorWindowInfo {
var hasEditor = false
var xrMode = XR_MODE_DEFAULT
var i = 0
while (i < args.size) {
when (args[i++]) {
EDITOR_ARG, EDITOR_ARG_SHORT, EDITOR_PROJECT_MANAGER_ARG, EDITOR_PROJECT_MANAGER_ARG_SHORT -> hasEditor = true
XR_MODE_ARG -> {
xrMode = args[i++]
}
}
}
return if (hasEditor) {
EDITOR_MAIN_INFO
} else {
val openxrEnabled = xrMode == XR_MODE_ON ||
(xrMode == XR_MODE_DEFAULT && GodotLib.getGlobal("xr/openxr/enabled").toBoolean())
if (openxrEnabled && isNativeXRDevice()) {
XR_RUN_GAME_INFO
} else {
RUN_GAME_INFO
}
}
}
override fun getEditorWindowInfoForInstanceId(instanceId: Int): EditorWindowInfo? {
return when (instanceId) {
XR_RUN_GAME_INFO.windowId -> XR_RUN_GAME_INFO
else -> super.getEditorWindowInfoForInstanceId(instanceId)
}
override fun getXRRuntimePermissions(): MutableSet<String> {
val xrRuntimePermissions = super.getXRRuntimePermissions()
xrRuntimePermissions.add("com.oculus.permission.USE_SCENE")
xrRuntimePermissions.add("horizonos.permission.USE_SCENE")
return xrRuntimePermissions
}
}

View File

@ -71,6 +71,18 @@
android:defaultWidth="@dimen/editor_default_window_width"
android:defaultHeight="@dimen/editor_default_window_height" />
</activity>
<activity
android:name=".GodotXRGame"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
android:process=":GodotXRGame"
android:launchMode="singleTask"
android:icon="@mipmap/ic_play_window"
android:label="@string/godot_game_activity_name"
android:exported="false"
android:screenOrientation="landscape"
android:resizeableActivity="false"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
</activity>
</application>
</manifest>

View File

@ -47,13 +47,12 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.window.layout.WindowMetricsCalculator
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.PermissionsUtil
import org.godotengine.godot.utils.ProcessPhoenix
import org.godotengine.godot.utils.isHorizonOSDevice
import org.godotengine.godot.utils.isPicoOSDevice
import org.godotengine.godot.utils.isNativeXRDevice
import java.util.*
import kotlin.math.min
@ -93,6 +92,20 @@ abstract class BaseGodotEditor : GodotActivity() {
// 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 XR_RUN_GAME_INFO = EditorWindowInfo(GodotXRGame::class.java, 1667, ":GodotXRGame")
/** Default behavior, means we check project settings **/
private const val XR_MODE_DEFAULT = "default"
/**
* Ignore project settings, OpenXR is disabled
*/
private const val XR_MODE_OFF = "off"
/**
* Ignore project settings, OpenXR is enabled
*/
private const val XR_MODE_ON = "on"
/**
* Sets of constants to specify the window to use to run the project.
@ -129,8 +142,7 @@ abstract class BaseGodotEditor : GodotActivity() {
*
* The permissions in this set will be requested on demand based on use cases.
*/
@CallSuper
protected open fun getExcludedPermissions(): MutableSet<String> {
private fun getExcludedPermissions(): MutableSet<String> {
val excludedPermissions = mutableSetOf(
// The RECORD_AUDIO permission is requested when the "audio/driver/enable_input" project
// setting is enabled.
@ -144,9 +156,21 @@ abstract class BaseGodotEditor : GodotActivity() {
Manifest.permission.REQUEST_INSTALL_PACKAGES,
)
}
// XR runtime permissions should only be requested when the "xr/openxr/enabled" project setting
// is enabled.
excludedPermissions.addAll(getXRRuntimePermissions())
return excludedPermissions
}
/**
* Set of permissions to request when the "xr/openxr/enabled" project setting is enabled.
*/
@CallSuper
protected open fun getXRRuntimePermissions(): MutableSet<String> {
return mutableSetOf()
}
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
@ -208,27 +232,38 @@ abstract class BaseGodotEditor : GodotActivity() {
final override fun getCommandLine() = commandLineParams
protected open fun retrieveEditorWindowInfo(args: Array<String>): EditorWindowInfo {
protected fun retrieveEditorWindowInfo(args: Array<String>): EditorWindowInfo {
var hasEditor = false
var xrMode = XR_MODE_DEFAULT
var i = 0
while (i < args.size) {
when (args[i++]) {
EDITOR_ARG, EDITOR_ARG_SHORT, EDITOR_PROJECT_MANAGER_ARG, EDITOR_PROJECT_MANAGER_ARG_SHORT -> hasEditor = true
XR_MODE_ARG -> {
xrMode = args[i++]
}
}
}
return if (hasEditor) {
EDITOR_MAIN_INFO
} else {
RUN_GAME_INFO
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
}
}
}
protected open fun getEditorWindowInfoForInstanceId(instanceId: Int): EditorWindowInfo? {
private fun getEditorWindowInfoForInstanceId(instanceId: Int): EditorWindowInfo? {
return when (instanceId) {
RUN_GAME_INFO.windowId -> RUN_GAME_INFO
EDITOR_MAIN_INFO.windowId -> EDITOR_MAIN_INFO
XR_RUN_GAME_INFO.windowId -> XR_RUN_GAME_INFO
else -> null
}
}
@ -417,8 +452,8 @@ abstract class BaseGodotEditor : GodotActivity() {
return when (policy) {
LaunchPolicy.AUTO -> {
if (isHorizonOSDevice()) {
// Horizon OS UX is more desktop-like and has support for launching adjacent
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.
LaunchPolicy.ADJACENT
} else {
@ -450,12 +485,6 @@ abstract class BaseGodotEditor : GodotActivity() {
* Returns true the if the device supports picture-in-picture (PiP)
*/
protected open fun hasPiPSystemFeature(): Boolean {
if (isNativeXRDevice()) {
// Known native XR devices do not support PiP.
// Will need to revisit as they update their OS.
return false
}
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
}
@ -534,15 +563,15 @@ abstract class BaseGodotEditor : GodotActivity() {
override fun supportsFeature(featureTag: String): Boolean {
if (featureTag == "xr_editor") {
return isNativeXRDevice()
return isNativeXRDevice(applicationContext)
}
if (featureTag == "horizonos") {
return isHorizonOSDevice()
return BuildConfig.FLAVOR == "horizonos"
}
if (featureTag == "picoos") {
return isPicoOSDevice()
return BuildConfig.FLAVOR == "picoos"
}
return false

View File

@ -59,8 +59,8 @@ open class GodotXRGame: GodotGame() {
override fun getProjectPermissionsToEnable(): MutableList<String> {
val permissionsToEnable = super.getProjectPermissionsToEnable()
val openxrEnabled = GodotLib.getGlobal("xr/openxr/enabled").toBoolean()
if (openxrEnabled) {
val xrRuntimePermission = getXRRuntimePermissions()
if (xrRuntimePermission.isNotEmpty() && GodotLib.getGlobal("xr/openxr/enabled").toBoolean()) {
// We only request permissions when the `automatically_request_runtime_permissions`
// project setting is enabled.
// If the project setting is not defined, we fall-back to the default behavior which is
@ -69,7 +69,7 @@ open class GodotXRGame: GodotGame() {
val automaticPermissionsRequestEnabled = automaticallyRequestPermissionsSetting.isNullOrEmpty() ||
automaticallyRequestPermissionsSetting.toBoolean()
if (automaticPermissionsRequestEnabled) {
permissionsToEnable.addAll(USE_SCENE_PERMISSIONS)
permissionsToEnable.addAll(xrRuntimePermission)
}
}

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="editor_default_window_height">640dp</dimen>
<dimen name="editor_default_window_height">720dp</dimen>
<dimen name="editor_default_window_width">1024dp</dimen>
</resources>

View File

@ -29,15 +29,8 @@
<activity
android:name=".GodotXRGame"
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
android:process=":GodotXRGame"
android:launchMode="singleTask"
android:icon="@mipmap/ic_play_window"
android:label="@string/godot_game_activity_name"
android:exported="false"
android:screenOrientation="landscape"
android:resizeableActivity="false"
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
tools:node="merge">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.DEFAULT" />

View File

@ -30,53 +30,9 @@
package org.godotengine.editor
import org.godotengine.godot.GodotLib
import org.godotengine.godot.utils.isNativeXRDevice
/**
* Primary window of the Godot Editor.
*
* This is the implementation of the editor used when running on PicoOS devices.
*/
open class GodotEditor : BaseGodotEditor() {
companion object {
private val TAG = GodotEditor::class.java.simpleName
internal val XR_RUN_GAME_INFO = EditorWindowInfo(GodotXRGame::class.java, 1667, ":GodotXRGame")
}
override fun retrieveEditorWindowInfo(args: Array<String>): EditorWindowInfo {
var hasEditor = false
var xrModeOn = false
var i = 0
while (i < args.size) {
when (args[i++]) {
EDITOR_ARG, EDITOR_ARG_SHORT, EDITOR_PROJECT_MANAGER_ARG, EDITOR_PROJECT_MANAGER_ARG_SHORT -> hasEditor = true
XR_MODE_ARG -> {
val argValue = args[i++]
xrModeOn = xrModeOn || ("on" == argValue)
}
}
}
return if (hasEditor) {
EDITOR_MAIN_INFO
} else {
val openxrEnabled = GodotLib.getGlobal("xr/openxr/enabled").toBoolean()
if (openxrEnabled && isNativeXRDevice()) {
XR_RUN_GAME_INFO
} else {
RUN_GAME_INFO
}
}
}
override fun getEditorWindowInfoForInstanceId(instanceId: Int): EditorWindowInfo? {
return when (instanceId) {
XR_RUN_GAME_INFO.windowId -> XR_RUN_GAME_INFO
else -> super.getEditorWindowInfoForInstanceId(instanceId)
}
}
}
open class GodotEditor : BaseGodotEditor()

View File

@ -1,59 +0,0 @@
/*************************************************************************/
/* GodotXRGame.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.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. */
/*************************************************************************/
package org.godotengine.editor
import org.godotengine.godot.GodotLib
import org.godotengine.godot.xr.XRMode
/**
* Provide support for running XR apps / games from the editor window.
*/
open class GodotXRGame: GodotGame() {
override fun overrideOrientationRequest() = true
override fun updateCommandLineParams(args: List<String>) {
val updatedArgs = ArrayList<String>()
if (!args.contains(XRMode.OPENXR.cmdLineArg)) {
updatedArgs.add(XRMode.OPENXR.cmdLineArg)
}
if (!args.contains(XR_MODE_ARG)) {
updatedArgs.add(XR_MODE_ARG)
updatedArgs.add("on")
}
updatedArgs.addAll(args)
super.updateCommandLineParams(updatedArgs)
}
override fun getEditorWindowInfo() = XR_RUN_GAME_INFO
}

View File

@ -68,7 +68,7 @@ public interface GodotRenderView {
* @return true if pointer capture is supported.
*/
default boolean canCapturePointer() {
// Pointer capture is not supported on Horizon OS
return !DeviceUtils.isHorizonOSDevice() && getInputHandler().canCapturePointer();
// Pointer capture is not supported on native XR devices.
return !DeviceUtils.isNativeXRDevice(getView().getContext()) && getInputHandler().canCapturePointer();
}
}

View File

@ -35,13 +35,14 @@
package org.godotengine.godot.utils
import android.content.Context
import android.os.Build
/**
* Returns true if running on Meta Horizon OS.
*/
fun isHorizonOSDevice(): Boolean {
return "Oculus".equals(Build.BRAND, true)
fun isHorizonOSDevice(context: Context): Boolean {
return context.packageManager.hasSystemFeature("oculus.hardware.standalone_vr")
}
/**
@ -54,6 +55,6 @@ fun isPicoOSDevice(): Boolean {
/**
* Returns true if running on a native Android XR device.
*/
fun isNativeXRDevice(): Boolean {
return isHorizonOSDevice() || isPicoOSDevice()
fun isNativeXRDevice(context: Context): Boolean {
return isHorizonOSDevice(context) || isPicoOSDevice()
}