From 9dc0543da7323e970f4349e65b416ef31056df20 Mon Sep 17 00:00:00 2001 From: Fredia Huya-Kouadio Date: Sat, 20 Apr 2024 10:24:11 -0700 Subject: Improve support for XR projects --- platform/android/java/app/build.gradle | 1 + platform/android/java/app/settings.gradle | 1 + platform/android/java/build.gradle | 45 +- platform/android/java/editor/build.gradle | 43 +- .../java/org/godotengine/editor/GodotEditor.kt | 39 ++ .../java/org/godotengine/editor/BaseGodotEditor.kt | 514 +++++++++++++++++++++ .../godotengine/editor/EditorMessageDispatcher.kt | 10 +- .../java/org/godotengine/editor/GodotEditor.kt | 498 -------------------- .../main/java/org/godotengine/editor/GodotGame.kt | 57 ++- .../java/editor/src/meta/AndroidManifest.xml | 99 ++++ .../java/editor/src/meta/assets/vr_splash.png | Bin 0 -> 14766 bytes .../java/org/godotengine/editor/GodotEditor.kt | 94 ++++ .../java/org/godotengine/editor/GodotXRGame.kt | 71 +++ platform/android/java/lib/build.gradle | 4 +- .../src/org/godotengine/godot/GodotRenderView.java | 7 +- .../godotengine/godot/GodotVulkanRenderView.java | 7 +- .../src/org/godotengine/godot/utils/DeviceUtils.kt | 52 +++ .../android/java/nativeSrcsConfigs/CMakeLists.txt | 4 +- platform/android/java/settings.gradle | 1 + 19 files changed, 1012 insertions(+), 535 deletions(-) create mode 100644 platform/android/java/editor/src/google/java/org/godotengine/editor/GodotEditor.kt create mode 100644 platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt delete mode 100644 platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt create mode 100644 platform/android/java/editor/src/meta/AndroidManifest.xml create mode 100644 platform/android/java/editor/src/meta/assets/vr_splash.png create mode 100644 platform/android/java/editor/src/meta/java/org/godotengine/editor/GodotEditor.kt create mode 100644 platform/android/java/editor/src/meta/java/org/godotengine/editor/GodotXRGame.kt create mode 100644 platform/android/java/lib/src/org/godotengine/godot/utils/DeviceUtils.kt (limited to 'platform/android/java') diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index 05b4f379b3..b9d15deec9 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -12,6 +12,7 @@ allprojects { mavenCentral() gradlePluginPortal() maven { url "https://plugins.gradle.org/m2/" } + maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/"} // Godot user plugins custom maven repos String[] mavenRepos = getGodotPluginsMavenRepos() diff --git a/platform/android/java/app/settings.gradle b/platform/android/java/app/settings.gradle index dcac44e393..e758d4e99a 100644 --- a/platform/android/java/app/settings.gradle +++ b/platform/android/java/app/settings.gradle @@ -11,6 +11,7 @@ pluginManagement { mavenCentral() gradlePluginPortal() maven { url "https://plugins.gradle.org/m2/" } + maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/"} } } diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index 771bda6948..974f072c18 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -18,12 +18,14 @@ allprojects { mavenCentral() gradlePluginPortal() maven { url "https://plugins.gradle.org/m2/" } + maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/"} } } ext { supportedAbis = ["arm32", "arm64", "x86_32", "x86_64"] supportedFlavors = ["editor", "template"] + supportedEditorVendors = ["google", "meta"] supportedFlavorsBuildTypes = [ "editor": ["dev", "debug", "release"], "template": ["dev", "debug", "release"] @@ -92,15 +94,20 @@ def templateExcludedBuildTask() { /** * Generates the build tasks for the given flavor * @param flavor Must be one of the supported flavors ('template' / 'editor') + * @param editorVendor Must be one of the supported editor vendors ('google' / 'meta') */ -def generateBuildTasks(String flavor = "template") { +def generateBuildTasks(String flavor = "template", String editorVendor = "google") { if (!supportedFlavors.contains(flavor)) { throw new GradleException("Invalid build flavor: $flavor") } + if (!supportedEditorVendors.contains(editorVendor)) { + throw new GradleException("Invalid editor vendor: $editorVendor") + } + String capitalizedEditorVendor = editorVendor.capitalize() def buildTasks = [] - // Only build the apks and aar files for which we have native shared libraries unless we intend + // Only build the binary files for which we have native shared libraries unless we intend // to run the scons build tasks. boolean excludeSconsBuildTasks = excludeSconsBuildTasks() boolean isTemplate = flavor == "template" @@ -163,28 +170,28 @@ def generateBuildTasks(String flavor = "template") { } } else { // Copy the generated editor apk to the bin directory. - String copyEditorApkTaskName = "copyEditor${capitalizedTarget}ApkToBin" + String copyEditorApkTaskName = "copyEditor${capitalizedEditorVendor}${capitalizedTarget}ApkToBin" if (tasks.findByName(copyEditorApkTaskName) != null) { buildTasks += tasks.getByName(copyEditorApkTaskName) } else { buildTasks += tasks.create(name: copyEditorApkTaskName, type: Copy) { - dependsOn ":editor:assemble${capitalizedTarget}" - from("editor/build/outputs/apk/${target}") + dependsOn ":editor:assemble${capitalizedEditorVendor}${capitalizedTarget}" + from("editor/build/outputs/apk/${editorVendor}/${target}") into(androidEditorBuildsDir) - include("android_editor-${target}*.apk") + include("android_editor-${editorVendor}-${target}*.apk") } } // Copy the generated editor aab to the bin directory. - String copyEditorAabTaskName = "copyEditor${capitalizedTarget}AabToBin" + String copyEditorAabTaskName = "copyEditor${capitalizedEditorVendor}${capitalizedTarget}AabToBin" if (tasks.findByName(copyEditorAabTaskName) != null) { buildTasks += tasks.getByName(copyEditorAabTaskName) } else { buildTasks += tasks.create(name: copyEditorAabTaskName, type: Copy) { - dependsOn ":editor:bundle${capitalizedTarget}" - from("editor/build/outputs/bundle/${target}") + dependsOn ":editor:bundle${capitalizedEditorVendor}${capitalizedTarget}" + from("editor/build/outputs/bundle/${editorVendor}${capitalizedTarget}") into(androidEditorBuildsDir) - include("android_editor-${target}*.aab") + include("android_editor-${editorVendor}-${target}*.aab") } } } @@ -197,15 +204,27 @@ def generateBuildTasks(String flavor = "template") { } /** - * Generate the Godot Editor Android apk. + * Generate the Godot Editor Android binaries. * * Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries * must have been generated (via scons) prior to running this gradle task. - * The task will only build the apk(s) for which the shared libraries is available. + * The task will only build the binaries for which the shared libraries is available. */ task generateGodotEditor { gradle.startParameter.excludedTaskNames += templateExcludedBuildTask() - dependsOn = generateBuildTasks("editor") + dependsOn = generateBuildTasks("editor", "google") +} + +/** + * Generate the Godot Editor Android binaries for Meta devices. + * + * Note: Unless the 'generateNativeLibs` argument is specified, the Godot 'tools' shared libraries + * must have been generated (via scons) prior to running this gradle task. + * The task will only build the binaries for which the shared libraries is available. + */ +task generateGodotMetaEditor { + gradle.startParameter.excludedTaskNames += templateExcludedBuildTask() + dependsOn = generateBuildTasks("editor", "meta") } /** diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle index b8b4233636..54d6b9b5f3 100644 --- a/platform/android/java/editor/build.gradle +++ b/platform/android/java/editor/build.gradle @@ -5,16 +5,6 @@ plugins { id 'base' } -dependencies { - implementation "androidx.fragment:fragment:$versions.fragmentVersion" - implementation project(":lib") - - implementation "androidx.window:window:1.3.0" - implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion" - implementation "androidx.constraintlayout:constraintlayout:2.1.4" - implementation "org.bouncycastle:bcprov-jdk15to18:1.77" -} - ext { // Retrieve the build number from the environment variable; default to 0 if none is specified. // The build number is added as a suffix to the version code for upload to the Google Play store. @@ -154,4 +144,37 @@ android { doNotStrip '**/*.so' } } + + flavorDimensions = ["vendor"] + productFlavors { + google { + dimension "vendor" + missingDimensionStrategy 'products', 'editor' + } + meta { + dimension "vendor" + missingDimensionStrategy 'products', 'editor' + ndk { + //noinspection ChromeOsAbiSupport + abiFilters "arm64-v8a" + } + applicationIdSuffix ".meta" + versionNameSuffix "-meta" + minSdkVersion 23 + targetSdkVersion 32 + } + } +} + +dependencies { + implementation "androidx.fragment:fragment:$versions.fragmentVersion" + implementation project(":lib") + + implementation "androidx.window:window:1.3.0" + implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion" + implementation "androidx.constraintlayout:constraintlayout:2.1.4" + implementation "org.bouncycastle:bcprov-jdk15to18:1.77" + + // Meta dependencies + metaImplementation "org.godotengine:godot-openxr-vendors-meta:3.0.0-stable" } diff --git a/platform/android/java/editor/src/google/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/google/java/org/godotengine/editor/GodotEditor.kt new file mode 100644 index 0000000000..f15d9f7768 --- /dev/null +++ b/platform/android/java/editor/src/google/java/org/godotengine/editor/GodotEditor.kt @@ -0,0 +1,39 @@ +/**************************************************************************/ +/* GodotEditor.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 + +/** + * Primary window of the Godot Editor. + * + * This is the implementation of the editor used when running on regular Android devices. + */ +open class GodotEditor : BaseGodotEditor() { +} diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt new file mode 100644 index 0000000000..50daf44d2a --- /dev/null +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt @@ -0,0 +1,514 @@ +/**************************************************************************/ +/* BaseGodotEditor.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.app.ActivityManager +import android.app.ActivityOptions +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.* +import android.util.Log +import android.view.View +import android.view.WindowManager +import android.widget.Toast +import androidx.annotation.CallSuper +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.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.isNativeXRDevice +import java.util.* +import kotlin.math.min + +/** + * Base class for the Godot Android Editor activities. + * + * This provides the basic templates for the activities making up this application. + * 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() { + + companion object { + private val TAG = BaseGodotEditor::class.java.simpleName + + private const val WAIT_FOR_DEBUGGER = false + + @JvmStatic + protected val EXTRA_COMMAND_LINE_PARAMS = "command_line_params" + @JvmStatic + protected val EXTRA_PIP_AVAILABLE = "pip_available" + @JvmStatic + protected val EXTRA_LAUNCH_IN_PIP = "launch_in_pip_requested" + + // 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" + internal const val XR_MODE_ARG = "--xr-mode" + + // 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) + + /** + * Sets of constants to specify the window to use to run the project. + * + * Should match the values in 'editor/editor_settings.cpp' for the + * 'run/window_placement/android_window' setting. + */ + 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 + } + + private val editorMessageDispatcher = EditorMessageDispatcher(this) + private val commandLineParams = ArrayList() + private val editorLoadingIndicator: View? by lazy { findViewById(R.id.editor_loading_indicator) } + + override fun getGodotAppLayout() = R.layout.godot_editor_layout + + internal open fun getEditorWindowInfo() = EDITOR_MAIN_INFO + + /** + * Set of permissions to be excluded when requesting all permissions at startup. + * + * The permissions in this set will be requested on demand based on use cases. + */ + @CallSuper + protected open fun getExcludedPermissions(): MutableSet { + return mutableSetOf( + // The RECORD_AUDIO permission is requested when the "audio/driver/enable_input" project + // setting is enabled. + Manifest.permission.RECORD_AUDIO + ) + } + + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + + // Prevent the editor window from showing in the display cutout + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && getEditorWindowInfo() == EDITOR_MAIN_INFO) { + window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER + } + + // We exclude certain permissions from the set we request at startup, as they'll be + // requested on demand based on use cases. + PermissionsUtil.requestManifestPermissions(this, getExcludedPermissions()) + + val params = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS) + Log.d(TAG, "Starting intent $intent with parameters ${params.contentToString()}") + updateCommandLineParams(params?.asList() ?: emptyList()) + + editorMessageDispatcher.parseStartIntent(packageManager, intent) + + if (BuildConfig.BUILD_TYPE == "dev" && WAIT_FOR_DEBUGGER) { + Debug.waitForDebugger() + } + + super.onCreate(savedInstanceState) + } + + override fun onGodotSetupCompleted() { + super.onGodotSetupCompleted() + val longPressEnabled = enableLongPressGestures() + val panScaleEnabled = enablePanAndScaleGestures() + + runOnUiThread { + // Enable long press, panning and scaling gestures + godotFragment?.godot?.renderView?.inputHandler?.apply { + enableLongPress(longPressEnabled) + enablePanningAndScalingGestures(panScaleEnabled) + } + } + } + + override fun onGodotMainLoopStarted() { + super.onGodotMainLoopStarted() + runOnUiThread { + // Hide the loading indicator + editorLoadingIndicator?.visibility = View.GONE + } + } + + @CallSuper + protected open fun updateCommandLineParams(args: List) { + // Update the list of command line params with the new args + commandLineParams.clear() + if (args.isNotEmpty()) { + commandLineParams.addAll(args) + } + if (BuildConfig.BUILD_TYPE == "dev") { + commandLineParams.add("--benchmark") + } + } + + final override fun getCommandLine() = commandLineParams + + protected open fun retrieveEditorWindowInfo(args: Array): EditorWindowInfo { + var hasEditor = 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 + } + } + + return if (hasEditor) { + EDITOR_MAIN_INFO + } else { + RUN_GAME_INFO + } + } + + protected open fun getEditorWindowInfoForInstanceId(instanceId: Int): EditorWindowInfo? { + return when (instanceId) { + RUN_GAME_INFO.windowId -> RUN_GAME_INFO + EDITOR_MAIN_INFO.windowId -> EDITOR_MAIN_INFO + else -> null + } + } + + protected fun getNewGodotInstanceIntent(editorWindowInfo: EditorWindowInfo, args: Array): Intent { + 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 + } + + val newInstance = Intent() + .setComponent(ComponentName(this, editorWindowInfo.windowClassName)) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .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): Int { + val editorWindowInfo = retrieveEditorWindowInfo(args) + + // Launch a new activity + val sourceView = godotFragment?.view + val activityOptions = if (sourceView == null) { + null + } else { + val startX = sourceView.width / 2 + val startY = sourceView.height / 2 + ActivityOptions.makeScaleUpAnimation(sourceView, startX, startY, 0, 0) + } + + val newInstance = getNewGodotInstanceIntent(editorWindowInfo, args) + if (editorWindowInfo.windowClassName == javaClass.name) { + Log.d(TAG, "Restarting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}") + val godot = godot + if (godot != null) { + godot.destroyAndKillProcess { + ProcessPhoenix.triggerRebirth(this, activityOptions?.toBundle(), newInstance) + } + } else { + ProcessPhoenix.triggerRebirth(this, activityOptions?.toBundle(), newInstance) + } + } else { + Log.d(TAG, "Starting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}") + newInstance.putExtra(EXTRA_NEW_LAUNCH, true) + .putExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD, editorMessageDispatcher.getMessageDispatcherPayload()) + startActivity(newInstance, activityOptions?.toBundle()) + } + return editorWindowInfo.windowId + } + + final override fun onGodotForceQuit(godotInstanceId: Int): Boolean { + val editorWindowInfo = getEditorWindowInfoForInstanceId(godotInstanceId) ?: return super.onGodotForceQuit(godotInstanceId) + + if (editorWindowInfo.windowClassName == javaClass.name) { + Log.d(TAG, "Force quitting ${editorWindowInfo.windowClassName}") + ProcessPhoenix.forceQuit(this) + return true + } + + // Send an inter-process message to request the target editor window to force quit. + if (editorMessageDispatcher.requestForceQuit(editorWindowInfo.windowId)) { + return true + } + + // Fallback to killing the target process. + val processName = packageName + editorWindowInfo.processNameSuffix + val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val runningProcesses = activityManager.runningAppProcesses + for (runningProcess in runningProcesses) { + if (runningProcess.processName == processName) { + // Killing process directly + Log.v(TAG, "Killing Godot process ${runningProcess.processName}") + Process.killProcess(runningProcess.pid) + return true + } + } + + return super.onGodotForceQuit(godotInstanceId) + } + + // Get the screen's density scale + private val isLargeScreen: Boolean + // Get the minimum window size // Correspond to the EXPANDED window size class. + get() { + val metrics = WindowMetricsCalculator.getOrCreate().computeMaximumWindowMetrics(this) + + // Get the screen's density scale + val scale = resources.displayMetrics.density + + // Get the minimum window size + val minSize = min(metrics.bounds.width(), metrics.bounds.height()).toFloat() + val minSizeDp = minSize / scale + return minSizeDp >= 840f // Correspond to the EXPANDED window size class. + } + + override fun setRequestedOrientation(requestedOrientation: Int) { + if (!overrideOrientationRequest()) { + super.setRequestedOrientation(requestedOrientation) + } + } + + /** + * The Godot Android Editor sets its own orientation via its AndroidManifest + */ + protected open fun overrideOrientationRequest() = true + + /** + * Enable long press gestures for the Godot Android editor. + */ + protected open fun enableLongPressGestures() = + java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/enable_long_press_as_right_click")) + + /** + * Enable pan and scale gestures for the Godot Android editor. + */ + 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 + } + } + + /** + * 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.PIP] 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 (isHorizonOSDevice()) { + // Horizon OS UX is more desktop-like and has support for launching adjacent + // windows. So we always want to launch in adjacent mode when auto is selected. + 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 + } + } + } catch (e: NumberFormatException) { + Log.w(TAG, "Error parsing the Android window placement editor setting", e) + // Fall-back to the default launch policy + defaultLaunchPolicy + } + } + } + + else -> { + policy + } + } + } + + /** + * 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) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + // Check if we got the MANAGE_EXTERNAL_STORAGE permission + if (requestCode == PermissionsUtil.REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (!Environment.isExternalStorageManager()) { + Toast.makeText( + this, + R.string.denied_storage_permission_error_msg, + Toast.LENGTH_LONG + ).show() + } + } + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + // Check if we got access to the necessary storage permissions + if (requestCode == PermissionsUtil.REQUEST_ALL_PERMISSION_REQ_CODE) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + var hasReadAccess = false + var hasWriteAccess = false + for (i in permissions.indices) { + if (Manifest.permission.READ_EXTERNAL_STORAGE == permissions[i] && grantResults[i] == PackageManager.PERMISSION_GRANTED) { + hasReadAccess = true + } + if (Manifest.permission.WRITE_EXTERNAL_STORAGE == permissions[i] && grantResults[i] == PackageManager.PERMISSION_GRANTED) { + hasWriteAccess = true + } + } + if (!hasReadAccess || !hasWriteAccess) { + Toast.makeText( + this, + R.string.denied_storage_permission_error_msg, + Toast.LENGTH_LONG + ).show() + } + } + } + } + + override fun signApk( + inputPath: String, + outputPath: String, + keystorePath: String, + keystoreUser: String, + keystorePassword: String + ): Error { + val godot = godot ?: return Error.ERR_UNCONFIGURED + return signApk(godot.fileAccessHandler, inputPath, outputPath, keystorePath, keystoreUser, keystorePassword) + } + + override fun verifyApk(apkPath: String): Error { + val godot = godot ?: return Error.ERR_UNCONFIGURED + return verifyApk(godot.fileAccessHandler, apkPath) + } +} diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt index b16e62149a..f5a6ed7dab 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt @@ -42,9 +42,9 @@ import android.util.Log import java.util.concurrent.ConcurrentHashMap /** - * Used by the [GodotEditor] classes to dispatch messages across processes. + * Used by the [BaseGodotEditor] classes to dispatch messages across processes. */ -internal class EditorMessageDispatcher(private val editor: GodotEditor) { +internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) { companion object { private val TAG = EditorMessageDispatcher::class.java.simpleName @@ -173,7 +173,11 @@ internal class EditorMessageDispatcher(private val editor: GodotEditor) { // to the sender. val senderId = messengerBundle.getInt(KEY_EDITOR_ID) val senderMessenger: Messenger? = messengerBundle.getParcelable(KEY_EDITOR_MESSENGER) - registerMessenger(senderId, senderMessenger) + registerMessenger(senderId, senderMessenger) { + // Terminate current instance when parent is no longer available. + Log.d(TAG, "Terminating current editor instance because parent is no longer available") + editor.finish() + } // Register ourselves to the sender so that it can communicate with us. registerSelfTo(pm, senderMessenger, editor.getEditorWindowInfo().windowId) diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt deleted file mode 100644 index 405b2fb57f..0000000000 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt +++ /dev/null @@ -1,498 +0,0 @@ -/**************************************************************************/ -/* GodotEditor.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.app.ActivityManager -import android.app.ActivityOptions -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.os.* -import android.util.Log -import android.view.View -import android.view.WindowManager -import android.widget.Toast -import androidx.annotation.CallSuper -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.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 java.util.* -import kotlin.math.min - -/** - * Base class for the Godot Android Editor activities. - * - * This provides the basic templates for the activities making up this application. - * 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. - * - * It also plays the role of the primary editor window. - */ -open class GodotEditor : GodotActivity() { - - companion object { - private val TAG = GodotEditor::class.java.simpleName - - private const val WAIT_FOR_DEBUGGER = false - - @JvmStatic - protected val EXTRA_COMMAND_LINE_PARAMS = "command_line_params" - @JvmStatic - protected val EXTRA_PIP_AVAILABLE = "pip_available" - @JvmStatic - protected val EXTRA_LAUNCH_IN_PIP = "launch_in_pip_requested" - - // Command line arguments - private const val FULLSCREEN_ARG = "--fullscreen" - private const val FULLSCREEN_ARG_SHORT = "-f" - 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" - private const val BREAKPOINTS_ARG = "--breakpoints" - private const val BREAKPOINTS_ARG_SHORT = "-b" - - // 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) - - /** - * Sets of constants to specify the window to use to run the project. - * - * Should match the values in 'editor/editor_settings.cpp' for the - * 'run/window_placement/android_window' setting. - */ - 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 - } - - private val editorMessageDispatcher = EditorMessageDispatcher(this) - private val commandLineParams = ArrayList() - private val editorLoadingIndicator: View? by lazy { findViewById(R.id.editor_loading_indicator) } - - override fun getGodotAppLayout() = R.layout.godot_editor_layout - - internal open fun getEditorWindowInfo() = EDITOR_MAIN_INFO - - override fun onCreate(savedInstanceState: Bundle?) { - installSplashScreen() - - // Prevent the editor window from showing in the display cutout - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && getEditorWindowInfo() == EDITOR_MAIN_INFO) { - window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER - } - - // We exclude certain permissions from the set we request at startup, as they'll be - // requested on demand based on use-cases. - PermissionsUtil.requestManifestPermissions(this, setOf(Manifest.permission.RECORD_AUDIO)) - - val params = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS) - Log.d(TAG, "Starting intent $intent with parameters ${params.contentToString()}") - updateCommandLineParams(params?.asList() ?: emptyList()) - - editorMessageDispatcher.parseStartIntent(packageManager, intent) - - if (BuildConfig.BUILD_TYPE == "dev" && WAIT_FOR_DEBUGGER) { - Debug.waitForDebugger() - } - - super.onCreate(savedInstanceState) - } - - override fun onGodotSetupCompleted() { - super.onGodotSetupCompleted() - val longPressEnabled = enableLongPressGestures() - val panScaleEnabled = enablePanAndScaleGestures() - - checkForProjectPermissionsToEnable() - - runOnUiThread { - // Enable long press, panning and scaling gestures - godotFragment?.godot?.renderView?.inputHandler?.apply { - enableLongPress(longPressEnabled) - enablePanningAndScalingGestures(panScaleEnabled) - } - } - } - - override fun onGodotMainLoopStarted() { - super.onGodotMainLoopStarted() - runOnUiThread { - // Hide the loading indicator - editorLoadingIndicator?.visibility = View.GONE - } - } - - /** - * Check for project permissions to enable - */ - protected open fun checkForProjectPermissionsToEnable() { - // Check for RECORD_AUDIO permission - val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input")) - if (audioInputEnabled) { - PermissionsUtil.requestPermission(Manifest.permission.RECORD_AUDIO, this) - } - } - - @CallSuper - protected open fun updateCommandLineParams(args: List) { - // Update the list of command line params with the new args - commandLineParams.clear() - if (args.isNotEmpty()) { - commandLineParams.addAll(args) - } - if (BuildConfig.BUILD_TYPE == "dev") { - commandLineParams.add("--benchmark") - } - } - - final override fun getCommandLine() = commandLineParams - - protected open fun getEditorWindowInfo(args: Array): EditorWindowInfo { - var hasEditor = 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 - } - } - - return if (hasEditor) { - EDITOR_MAIN_INFO - } else { - RUN_GAME_INFO - } - } - - protected open fun getEditorWindowInfoForInstanceId(instanceId: Int): EditorWindowInfo? { - return when (instanceId) { - RUN_GAME_INFO.windowId -> RUN_GAME_INFO - EDITOR_MAIN_INFO.windowId -> EDITOR_MAIN_INFO - else -> null - } - } - - protected fun getNewGodotInstanceIntent(editorWindowInfo: EditorWindowInfo, args: Array): Intent { - 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 - } - - val newInstance = Intent() - .setComponent(ComponentName(this, editorWindowInfo.windowClassName)) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .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): Int { - val editorWindowInfo = getEditorWindowInfo(args) - - // Launch a new activity - val sourceView = godotFragment?.view - val activityOptions = if (sourceView == null) { - null - } else { - val startX = sourceView.width / 2 - val startY = sourceView.height / 2 - ActivityOptions.makeScaleUpAnimation(sourceView, startX, startY, 0, 0) - } - - val newInstance = getNewGodotInstanceIntent(editorWindowInfo, args) - if (editorWindowInfo.windowClassName == javaClass.name) { - Log.d(TAG, "Restarting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}") - val godot = godot - if (godot != null) { - godot.destroyAndKillProcess { - ProcessPhoenix.triggerRebirth(this, activityOptions?.toBundle(), newInstance) - } - } else { - ProcessPhoenix.triggerRebirth(this, activityOptions?.toBundle(), newInstance) - } - } else { - Log.d(TAG, "Starting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}") - newInstance.putExtra(EXTRA_NEW_LAUNCH, true) - .putExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD, editorMessageDispatcher.getMessageDispatcherPayload()) - startActivity(newInstance, activityOptions?.toBundle()) - } - return editorWindowInfo.windowId - } - - final override fun onGodotForceQuit(godotInstanceId: Int): Boolean { - val editorWindowInfo = getEditorWindowInfoForInstanceId(godotInstanceId) ?: return super.onGodotForceQuit(godotInstanceId) - - if (editorWindowInfo.windowClassName == javaClass.name) { - Log.d(TAG, "Force quitting ${editorWindowInfo.windowClassName}") - ProcessPhoenix.forceQuit(this) - return true - } - - // Send an inter-process message to request the target editor window to force quit. - if (editorMessageDispatcher.requestForceQuit(editorWindowInfo.windowId)) { - return true - } - - // Fallback to killing the target process. - val processName = packageName + editorWindowInfo.processNameSuffix - val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - val runningProcesses = activityManager.runningAppProcesses - for (runningProcess in runningProcesses) { - if (runningProcess.processName == processName) { - // Killing process directly - Log.v(TAG, "Killing Godot process ${runningProcess.processName}") - Process.killProcess(runningProcess.pid) - return true - } - } - - return super.onGodotForceQuit(godotInstanceId) - } - - // Get the screen's density scale - private val isLargeScreen: Boolean - // Get the minimum window size // Correspond to the EXPANDED window size class. - get() { - val metrics = WindowMetricsCalculator.getOrCreate().computeMaximumWindowMetrics(this) - - // Get the screen's density scale - val scale = resources.displayMetrics.density - - // Get the minimum window size - val minSize = min(metrics.bounds.width(), metrics.bounds.height()).toFloat() - val minSizeDp = minSize / scale - return minSizeDp >= 840f // Correspond to the EXPANDED window size class. - } - - override fun setRequestedOrientation(requestedOrientation: Int) { - if (!overrideOrientationRequest()) { - super.setRequestedOrientation(requestedOrientation) - } - } - - /** - * The Godot Android Editor sets its own orientation via its AndroidManifest - */ - protected open fun overrideOrientationRequest() = true - - /** - * Enable long press gestures for the Godot Android editor. - */ - protected open fun enableLongPressGestures() = - java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/enable_long_press_as_right_click")) - - /** - * Enable pan and scale gestures for the Godot Android editor. - */ - 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 - } - } - - /** - * 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.PIP] 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 -> { - 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 - } - } - } catch (e: NumberFormatException) { - Log.w(TAG, "Error parsing the Android window placement editor setting", e) - // Fall-back to the default launch policy - defaultLaunchPolicy - } - } - - else -> { - policy - } - } - } - - /** - * Returns true the if the device supports picture-in-picture (PiP) - */ - protected open fun hasPiPSystemFeature() = 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 - if (requestCode == PermissionsUtil.REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - if (!Environment.isExternalStorageManager()) { - Toast.makeText( - this, - R.string.denied_storage_permission_error_msg, - Toast.LENGTH_LONG - ).show() - } - } - } - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - // Check if we got access to the necessary storage permissions - if (requestCode == PermissionsUtil.REQUEST_ALL_PERMISSION_REQ_CODE) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - var hasReadAccess = false - var hasWriteAccess = false - for (i in permissions.indices) { - if (Manifest.permission.READ_EXTERNAL_STORAGE == permissions[i] && grantResults[i] == PackageManager.PERMISSION_GRANTED) { - hasReadAccess = true - } - if (Manifest.permission.WRITE_EXTERNAL_STORAGE == permissions[i] && grantResults[i] == PackageManager.PERMISSION_GRANTED) { - hasWriteAccess = true - } - } - if (!hasReadAccess || !hasWriteAccess) { - Toast.makeText( - this, - R.string.denied_storage_permission_error_msg, - Toast.LENGTH_LONG - ).show() - } - } - } - } - - override fun signApk( - inputPath: String, - outputPath: String, - keystorePath: String, - keystoreUser: String, - keystorePassword: String - ): Error { - val godot = godot ?: return Error.ERR_UNCONFIGURED - return signApk(godot.fileAccessHandler, inputPath, outputPath, keystorePath, keystoreUser, keystorePassword) - } - - override fun verifyApk(apkPath: String): Error { - val godot = godot ?: return Error.ERR_UNCONFIGURED - return verifyApk(godot.fileAccessHandler, apkPath) - } -} diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt index 6b4bf255f2..e52d566347 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt @@ -30,6 +30,7 @@ package org.godotengine.editor +import android.Manifest import android.annotation.SuppressLint import android.app.PictureInPictureParams import android.content.Intent @@ -38,12 +39,15 @@ 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 org.godotengine.godot.utils.ProcessPhoenix /** * Drives the 'run project' window of the Godot Editor. */ -class GodotGame : GodotEditor() { +open class GodotGame : GodotEditor() { companion object { private val TAG = GodotGame::class.java.simpleName @@ -136,8 +140,53 @@ class GodotGame : GodotEditor() { override fun enablePanAndScaleGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures")) - override fun checkForProjectPermissionsToEnable() { - // Nothing to do.. by the time we get here, the project permissions will have already - // been requested by the Editor window. + 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 + } + } + + // 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 { + val permissionsToEnable = mutableListOf() + + // 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 } } diff --git a/platform/android/java/editor/src/meta/AndroidManifest.xml b/platform/android/java/editor/src/meta/AndroidManifest.xml new file mode 100644 index 0000000000..06442ac4e6 --- /dev/null +++ b/platform/android/java/editor/src/meta/AndroidManifest.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/platform/android/java/editor/src/meta/assets/vr_splash.png b/platform/android/java/editor/src/meta/assets/vr_splash.png new file mode 100644 index 0000000000..7bddd4325a Binary files /dev/null and b/platform/android/java/editor/src/meta/assets/vr_splash.png differ diff --git a/platform/android/java/editor/src/meta/java/org/godotengine/editor/GodotEditor.kt b/platform/android/java/editor/src/meta/java/org/godotengine/editor/GodotEditor.kt new file mode 100644 index 0000000000..9f0440e87d --- /dev/null +++ b/platform/android/java/editor/src/meta/java/org/godotengine/editor/GodotEditor.kt @@ -0,0 +1,94 @@ +/**************************************************************************/ +/* GodotEditor.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 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 Meta 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") + + internal const val USE_ANCHOR_API_PERMISSION = "com.oculus.permission.USE_ANCHOR_API" + internal const val USE_SCENE_PERMISSION = "com.oculus.permission.USE_SCENE" + } + + override fun getExcludedPermissions(): MutableSet { + val excludedPermissions = super.getExcludedPermissions() + // The USE_ANCHOR_API and USE_SCENE permissions are requested when the "xr/openxr/enabled" + // project setting is enabled. + excludedPermissions.add(USE_ANCHOR_API_PERMISSION) + excludedPermissions.add(USE_SCENE_PERMISSION) + return excludedPermissions + } + + override fun retrieveEditorWindowInfo(args: Array): 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) + } + } +} diff --git a/platform/android/java/editor/src/meta/java/org/godotengine/editor/GodotXRGame.kt b/platform/android/java/editor/src/meta/java/org/godotengine/editor/GodotXRGame.kt new file mode 100644 index 0000000000..d71fbb53f2 --- /dev/null +++ b/platform/android/java/editor/src/meta/java/org/godotengine/editor/GodotXRGame.kt @@ -0,0 +1,71 @@ +/*************************************************************************/ +/* 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.utils.PermissionsUtil +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) { + val updatedArgs = ArrayList() + 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 + + override fun getProjectPermissionsToEnable(): MutableList { + val permissionsToEnable = super.getProjectPermissionsToEnable() + + val openxrEnabled = GodotLib.getGlobal("xr/openxr/enabled").toBoolean() + if (openxrEnabled) { + permissionsToEnable.add(USE_ANCHOR_API_PERMISSION) + permissionsToEnable.add(USE_SCENE_PERMISSION) + } + + return permissionsToEnable + } +} diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index 81ab598b90..f6aee434e5 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -51,7 +51,7 @@ android { } } - flavorDimensions "products" + flavorDimensions = ["products"] productFlavors { editor {} template {} @@ -104,7 +104,7 @@ android { } boolean devBuild = buildType == "dev" - boolean debugSymbols = devBuild || isAndroidStudio() + boolean debugSymbols = devBuild boolean runTests = devBuild boolean productionBuild = !devBuild boolean storeRelease = buildType == "release" diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java index 30821eaa8e..9db9ef6080 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java @@ -31,6 +31,7 @@ package org.godotengine.godot; import org.godotengine.godot.input.GodotInputHandler; +import org.godotengine.godot.utils.DeviceUtils; import android.view.SurfaceView; @@ -63,7 +64,11 @@ public interface GodotRenderView { void setPointerIcon(int pointerType); + /** + * @return true if pointer capture is supported. + */ default boolean canCapturePointer() { - return getInputHandler().canCapturePointer(); + // Pointer capture is not supported on Horizon OS + return !DeviceUtils.isHorizonOSDevice() && getInputHandler().canCapturePointer(); } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java index d5b05913d8..8b3880d32d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java @@ -68,6 +68,7 @@ class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView { setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); } setFocusableInTouchMode(true); + setClickable(false); } @Override @@ -132,17 +133,17 @@ class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView { @Override public boolean onKeyUp(final int keyCode, KeyEvent event) { - return mInputHandler.onKeyUp(keyCode, event); + return mInputHandler.onKeyUp(keyCode, event) || super.onKeyUp(keyCode, event); } @Override public boolean onKeyDown(final int keyCode, KeyEvent event) { - return mInputHandler.onKeyDown(keyCode, event); + return mInputHandler.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event); } @Override public boolean onGenericMotionEvent(MotionEvent event) { - return mInputHandler.onGenericMotionEvent(event); + return mInputHandler.onGenericMotionEvent(event) || super.onGenericMotionEvent(event); } @Override diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/DeviceUtils.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/DeviceUtils.kt new file mode 100644 index 0000000000..abe0c5f885 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/DeviceUtils.kt @@ -0,0 +1,52 @@ +/**************************************************************************/ +/* DeviceUtils.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +/** + * Contains utility methods for detecting specific devices. + */ +@file:JvmName("DeviceUtils") + +package org.godotengine.godot.utils + +import android.os.Build + +/** + * Returns true if running on Meta's Horizon OS. + */ +fun isHorizonOSDevice(): Boolean { + return "Oculus".equals(Build.BRAND, true) +} + +/** + * Returns true if running on a native Android XR device. + */ +fun isNativeXRDevice(): Boolean { + return isHorizonOSDevice() +} diff --git a/platform/android/java/nativeSrcsConfigs/CMakeLists.txt b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt index 96b6dfc9f3..a7d2774db5 100644 --- a/platform/android/java/nativeSrcsConfigs/CMakeLists.txt +++ b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt @@ -8,6 +8,7 @@ set(CMAKE_CXX_EXTENSIONS OFF) set(GODOT_ROOT_DIR ../../../..) set(ANDROID_ROOT_DIR "${GODOT_ROOT_DIR}/platform/android" CACHE STRING "") +set(OPENXR_INCLUDE_DIR "${GODOT_ROOT_DIR}/thirdparty/openxr/include" CACHE STRING "") # Get sources file(GLOB_RECURSE SOURCES ${GODOT_ROOT_DIR}/*.c**) @@ -17,6 +18,7 @@ add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS}) target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC ${GODOT_ROOT_DIR} - ${ANDROID_ROOT_DIR}) + ${ANDROID_ROOT_DIR} + ${OPENXR_INCLUDE_DIR}) add_definitions(-DUNIX_ENABLED -DVULKAN_ENABLED -DANDROID_ENABLED -DGLES3_ENABLED -DTOOLS_ENABLED) diff --git a/platform/android/java/settings.gradle b/platform/android/java/settings.gradle index 3137e74244..41418a4a45 100644 --- a/platform/android/java/settings.gradle +++ b/platform/android/java/settings.gradle @@ -14,6 +14,7 @@ pluginManagement { mavenCentral() gradlePluginPortal() maven { url "https://plugins.gradle.org/m2/" } + maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots/"} } } -- cgit v1.2.3