diff options
Diffstat (limited to 'platform/android/java')
35 files changed, 839 insertions, 482 deletions
diff --git a/platform/android/java/app/build.gradle b/platform/android/java/app/build.gradle index 7797f4bc9d..01d5d9ef92 100644 --- a/platform/android/java/app/build.gradle +++ b/platform/android/java/app/build.gradle @@ -32,6 +32,7 @@ configurations { dependencies { implementation "androidx.fragment:fragment:$versions.fragmentVersion" + implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion" if (rootProject.findProject(":lib")) { implementation project(":lib") @@ -124,6 +125,12 @@ android { // - https://stackoverflow.com/a/44704840 useLegacyPackaging shouldUseLegacyPackaging() } + + // Always select Godot's version of libc++_shared.so in case deps have their own + pickFirst 'lib/x86/libc++_shared.so' + pickFirst 'lib/x86_64/libc++_shared.so' + pickFirst 'lib/armeabi-v7a/libc++_shared.so' + pickFirst 'lib/arm64-v8a/libc++_shared.so' } signingConfigs { @@ -205,36 +212,66 @@ android { } task copyAndRenameDebugApk(type: Copy) { + // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files + // and directories. Otherwise this check may cause permissions access failures on Windows + // machines. + doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") + from "$buildDir/outputs/apk/debug/android_debug.apk" into getExportPath() rename "android_debug.apk", getExportFilename() } task copyAndRenameDevApk(type: Copy) { + // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files + // and directories. Otherwise this check may cause permissions access failures on Windows + // machines. + doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") + from "$buildDir/outputs/apk/dev/android_dev.apk" into getExportPath() rename "android_dev.apk", getExportFilename() } task copyAndRenameReleaseApk(type: Copy) { + // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files + // and directories. Otherwise this check may cause permissions access failures on Windows + // machines. + doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") + from "$buildDir/outputs/apk/release/android_release.apk" into getExportPath() rename "android_release.apk", getExportFilename() } task copyAndRenameDebugAab(type: Copy) { + // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files + // and directories. Otherwise this check may cause permissions access failures on Windows + // machines. + doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") + from "$buildDir/outputs/bundle/debug/build-debug.aab" into getExportPath() rename "build-debug.aab", getExportFilename() } task copyAndRenameDevAab(type: Copy) { + // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files + // and directories. Otherwise this check may cause permissions access failures on Windows + // machines. + doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") + from "$buildDir/outputs/bundle/dev/build-dev.aab" into getExportPath() rename "build-dev.aab", getExportFilename() } task copyAndRenameReleaseAab(type: Copy) { + // The 'doNotTrackState' is added to disable gradle's up-to-date checks for output files + // and directories. Otherwise this check may cause permissions access failures on Windows + // machines. + doNotTrackState("No need for up-to-date checks for the copy-and-rename operation") + from "$buildDir/outputs/bundle/release/build-release.aab" into getExportPath() rename "build-release.aab", getExportFilename() diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index f2c4a5d1b6..01759a1b2f 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -11,7 +11,8 @@ ext.versions = [ nexusPublishVersion: '1.3.0', javaVersion : JavaVersion.VERSION_17, // Also update 'platform/android/detect.py#get_ndk_version()' when this is updated. - ndkVersion : '23.2.8568313' + ndkVersion : '23.2.8568313', + splashscreenVersion: '1.0.1' ] @@ -194,17 +195,17 @@ final String VALUE_SEPARATOR_REGEX = "\\|" // get the list of ABIs the project should be exported to ext.getExportEnabledABIs = { -> - String enabledABIs = project.hasProperty("export_enabled_abis") ? project.property("export_enabled_abis") : ""; + String enabledABIs = project.hasProperty("export_enabled_abis") ? project.property("export_enabled_abis") : "" if (enabledABIs == null || enabledABIs.isEmpty()) { enabledABIs = "armeabi-v7a|arm64-v8a|x86|x86_64|" } - Set<String> exportAbiFilter = []; + Set<String> exportAbiFilter = [] for (String abi_name : enabledABIs.split(VALUE_SEPARATOR_REGEX)) { if (!abi_name.trim().isEmpty()){ - exportAbiFilter.add(abi_name); + exportAbiFilter.add(abi_name) } } - return exportAbiFilter; + return exportAbiFilter } ext.getExportPath = { @@ -330,8 +331,7 @@ ext.getReleaseKeyAlias = { -> } ext.isAndroidStudio = { -> - def sysProps = System.getProperties() - return sysProps != null && sysProps['idea.platform.prefix'] != null + return project.hasProperty('android.injected.invoked.from.ide') } ext.shouldZipAlign = { -> diff --git a/platform/android/java/app/res/drawable-nodpi/splash.png b/platform/android/java/app/res/drawable-nodpi/splash.png Binary files differdeleted file mode 100644 index 7bddd4325a..0000000000 --- a/platform/android/java/app/res/drawable-nodpi/splash.png +++ /dev/null diff --git a/platform/android/java/app/res/drawable-nodpi/splash_bg_color.png b/platform/android/java/app/res/drawable-nodpi/splash_bg_color.png Binary files differdeleted file mode 100644 index 004b6fd508..0000000000 --- a/platform/android/java/app/res/drawable-nodpi/splash_bg_color.png +++ /dev/null diff --git a/platform/android/java/app/res/drawable/splash_drawable.xml b/platform/android/java/app/res/drawable/splash_drawable.xml deleted file mode 100644 index 30627b998c..0000000000 --- a/platform/android/java/app/res/drawable/splash_drawable.xml +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> - - <item android:drawable="@drawable/splash_bg_color" /> - - <item> - <bitmap - android:gravity="center" - android:filter="false" - android:src="@drawable/splash" /> - </item> -</layer-list> diff --git a/platform/android/java/app/res/values/themes.xml b/platform/android/java/app/res/values/themes.xml index d64b50ca45..3ab8401928 100644 --- a/platform/android/java/app/res/values/themes.xml +++ b/platform/android/java/app/res/values/themes.xml @@ -3,8 +3,17 @@ <style name="GodotAppMainTheme" parent="@android:style/Theme.Black.NoTitleBar"/> - <style name="GodotAppSplashTheme" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen"> - <item name="android:windowBackground">@drawable/splash_drawable</item> - <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item> + <style name="GodotAppSplashTheme" parent="Theme.SplashScreen"> + <!-- Set the splash screen background, animated icon, and animation + duration. --> + <item name="android:windowSplashScreenBackground">@mipmap/icon_background</item> + + <!-- Use windowSplashScreenAnimatedIcon to add a drawable or an animated + drawable. One of these is required. --> + <item name="windowSplashScreenAnimatedIcon">@mipmap/icon_foreground</item> + + <!-- Set the theme of the Activity that directly follows your splash + screen. This is required. --> + <item name="postSplashScreenTheme">@style/GodotAppMainTheme</item> </style> </resources> diff --git a/platform/android/java/app/src/com/godot/game/GodotApp.java b/platform/android/java/app/src/com/godot/game/GodotApp.java index 9142d767b4..22e617f6e7 100644 --- a/platform/android/java/app/src/com/godot/game/GodotApp.java +++ b/platform/android/java/app/src/com/godot/game/GodotApp.java @@ -34,6 +34,8 @@ import org.godotengine.godot.GodotActivity; import android.os.Bundle; +import androidx.core.splashscreen.SplashScreen; + /** * Template activity for Godot Android builds. * Feel free to extend and modify this class for your custom logic. @@ -41,7 +43,7 @@ import android.os.Bundle; public class GodotApp extends GodotActivity { @Override public void onCreate(Bundle savedInstanceState) { - setTheme(R.style.GodotAppMainTheme); + SplashScreen.installSplashScreen(this); super.onCreate(savedInstanceState); } } diff --git a/platform/android/java/build.gradle b/platform/android/java/build.gradle index c609b33ef4..b91b023ce6 100644 --- a/platform/android/java/build.gradle +++ b/platform/android/java/build.gradle @@ -232,11 +232,6 @@ def generateBuildTasks(String flavor = "template") { return tasks } -def isAndroidStudio() { - def sysProps = System.getProperties() - return sysProps != null && sysProps['idea.platform.prefix'] != null -} - task copyEditorReleaseApkToBin(type: Copy) { dependsOn ':editor:assembleRelease' from('editor/build/outputs/apk/release') diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle index 0f7ffeecae..55fe2a22fe 100644 --- a/platform/android/java/editor/build.gradle +++ b/platform/android/java/editor/build.gradle @@ -10,6 +10,8 @@ dependencies { implementation project(":lib") implementation "androidx.window:window:1.2.0" + implementation "androidx.core:core-splashscreen:$versions.splashscreenVersion" + implementation "androidx.constraintlayout:constraintlayout:2.1.4" } ext { @@ -20,7 +22,7 @@ ext { String versionStatus = System.getenv("GODOT_VERSION_STATUS") if (versionStatus != null && !versionStatus.isEmpty()) { try { - buildNumber = Integer.parseInt(versionStatus.replaceAll("[^0-9]", "")); + buildNumber = Integer.parseInt(versionStatus.replaceAll("[^0-9]", "")) } catch (NumberFormatException ignored) { buildNumber = 0 } @@ -92,6 +94,10 @@ android { targetSdkVersion versions.targetSdk missingDimensionStrategy 'products', 'editor' + manifestPlaceholders += [ + editorAppName: "Godot Editor 4", + editorBuildSuffix: "" + ] } base { @@ -124,11 +130,13 @@ android { dev { initWith debug applicationIdSuffix ".dev" + manifestPlaceholders += [editorBuildSuffix: " (dev)"] } debug { initWith release applicationIdSuffix ".debug" + manifestPlaceholders += [editorBuildSuffix: " (debug)"] signingConfig signingConfigs.debug } diff --git a/platform/android/java/editor/src/debug/res/values/strings.xml b/platform/android/java/editor/src/debug/res/values/strings.xml deleted file mode 100644 index 09ee2d77e1..0000000000 --- a/platform/android/java/editor/src/debug/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <string name="godot_editor_name_string">Godot Editor 4 (debug)</string> -</resources> diff --git a/platform/android/java/editor/src/dev/res/values/strings.xml b/platform/android/java/editor/src/dev/res/values/strings.xml deleted file mode 100644 index 215f2c7d0a..0000000000 --- a/platform/android/java/editor/src/dev/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<resources> - <string name="godot_editor_name_string">Godot Editor 4 (dev)</string> -</resources> diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml index 78dcddac0e..c7d14a3f49 100644 --- a/platform/android/java/editor/src/main/AndroidManifest.xml +++ b/platform/android/java/editor/src/main/AndroidManifest.xml @@ -13,12 +13,15 @@ android:glEsVersion="0x00030000" android:required="true" /> - <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" + <uses-permission + android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> - <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" - android:maxSdkVersion="29"/> - <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" - android:maxSdkVersion="29"/> + <uses-permission + android:name="android.permission.WRITE_EXTERNAL_STORAGE" + android:maxSdkVersion="29" /> + <uses-permission + android:name="android.permission.READ_EXTERNAL_STORAGE" + android:maxSdkVersion="29" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.VIBRATE" /> @@ -26,52 +29,44 @@ <application android:allowBackup="false" android:icon="@mipmap/icon" - android:label="@string/godot_editor_name_string" - tools:ignore="GoogleAppIndexingWarning" - android:theme="@style/GodotEditorTheme" - android:requestLegacyExternalStorage="true"> + android:label="${editorAppName}${editorBuildSuffix}" + android:requestLegacyExternalStorage="true" + android:theme="@style/GodotEditorSplashScreenTheme" + tools:ignore="GoogleAppIndexingWarning"> + <profileable + android:shell="true" + android:enabled="true" + tools:targetApi="29" /> <activity - android:name=".GodotProjectManager" + android:name=".GodotEditor" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" - android:launchMode="singleTask" - android:screenOrientation="userLandscape" android:exported="true" - android:process=":GodotProjectManager"> - - <layout android:defaultHeight="@dimen/editor_default_window_height" - android:defaultWidth="@dimen/editor_default_window_width" /> + android:launchMode="singleTask" + android:screenOrientation="userLandscape"> + <layout + android:defaultWidth="@dimen/editor_default_window_width" + android:defaultHeight="@dimen/editor_default_window_height" /> <intent-filter> <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> - - <activity - android:name=".GodotEditor" - android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" - android:process=":GodotEditor" - android:launchMode="singleTask" - android:screenOrientation="userLandscape" - android:exported="false"> - <layout android:defaultHeight="@dimen/editor_default_window_height" - android:defaultWidth="@dimen/editor_default_window_width" /> - </activity> - <activity android:name=".GodotGame" android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode" + android:exported="false" android:label="@string/godot_project_name_string" - android:process=":GodotGame" android:launchMode="singleTask" - android:exported="false" + android:process=":GodotGame" android:screenOrientation="userLandscape"> - <layout android:defaultHeight="@dimen/editor_default_window_height" - android:defaultWidth="@dimen/editor_default_window_width" /> + <layout + android:defaultWidth="@dimen/editor_default_window_width" + android:defaultHeight="@dimen/editor_default_window_height" /> </activity> - </application> </manifest> diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt new file mode 100644 index 0000000000..0da1d01aed --- /dev/null +++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt @@ -0,0 +1,68 @@ +/**************************************************************************/ +/* EditorWindowInfo.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 + +/** + * Specifies the policy for adjacent launches. + */ +enum class LaunchAdjacentPolicy { + /** + * Adjacent launches are disabled. + */ + DISABLED, + + /** + * Adjacent launches are enabled / disabled based on the device and screen metrics. + */ + AUTO, + + /** + * Adjacent launches are enabled. + */ + ENABLED +} + +/** + * Describe the editor window to launch + */ +data class EditorWindowInfo( + val windowClassName: String, + val windowId: Int, + val processNameSuffix: String, + val launchAdjacentPolicy: LaunchAdjacentPolicy = LaunchAdjacentPolicy.DISABLED +) { + constructor( + windowClass: Class<*>, + windowId: Int, + processNameSuffix: String, + launchAdjacentPolicy: LaunchAdjacentPolicy = LaunchAdjacentPolicy.DISABLED + ) : this(windowClass.name, windowId, processNameSuffix, launchAdjacentPolicy) +} 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 index caf64bc933..5515347bd6 100644 --- 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 @@ -32,12 +32,16 @@ package org.godotengine.editor import android.Manifest import android.app.ActivityManager +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.widget.Toast +import androidx.annotation.CallSuper +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.window.layout.WindowMetricsCalculator import org.godotengine.godot.GodotActivity import org.godotengine.godot.GodotLib @@ -64,18 +68,15 @@ open class GodotEditor : GodotActivity() { private const val EXTRA_COMMAND_LINE_PARAMS = "command_line_params" - private const val EDITOR_ID = 777 + // Command line arguments private const val EDITOR_ARG = "--editor" private const val EDITOR_ARG_SHORT = "-e" - private const val EDITOR_PROCESS_NAME_SUFFIX = ":GodotEditor" + private const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager" + private const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p" - private const val GAME_ID = 667 - private const val GAME_PROCESS_NAME_SUFFIX = ":GodotGame" - - private const val PROJECT_MANAGER_ID = 555 - private const val PROJECT_MANAGER_ARG = "--project-manager" - private const val PROJECT_MANAGER_ARG_SHORT = "-p" - private const val PROJECT_MANAGER_PROCESS_NAME_SUFFIX = ":GodotProjectManager" + // 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", LaunchAdjacentPolicy.AUTO) /** * Sets of constants to specify the window to use to run the project. @@ -89,15 +90,20 @@ open class GodotEditor : GodotActivity() { } private val commandLineParams = ArrayList<String>() + private val editorLoadingIndicator: View? by lazy { findViewById(R.id.editor_loading_indicator) } + + override fun getGodotAppLayout() = R.layout.godot_editor_layout override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + // 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, "Received parameters ${params.contentToString()}") - updateCommandLineParams(params) + Log.d(TAG, "Starting intent $intent with parameters ${params.contentToString()}") + updateCommandLineParams(params?.asList() ?: emptyList()) if (BuildConfig.BUILD_TYPE == "dev" && WAIT_FOR_DEBUGGER) { Debug.waitForDebugger() @@ -122,109 +128,104 @@ open class GodotEditor : GodotActivity() { } } + 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")); + val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input")) if (audioInputEnabled) { PermissionsUtil.requestPermission(Manifest.permission.RECORD_AUDIO, this) } } - private fun updateCommandLineParams(args: Array<String>?) { + @CallSuper + protected open fun updateCommandLineParams(args: List<String>) { // Update the list of command line params with the new args commandLineParams.clear() - if (!args.isNullOrEmpty()) { - commandLineParams.addAll(listOf(*args)) + if (args.isNotEmpty()) { + commandLineParams.addAll(args) } if (BuildConfig.BUILD_TYPE == "dev") { commandLineParams.add("--benchmark") } } - override fun getCommandLine() = commandLineParams + final override fun getCommandLine() = commandLineParams - override fun onNewGodotInstanceRequested(args: Array<String>): Int { - // Parse the arguments to figure out which activity to start. - var targetClass: Class<*> = GodotGame::class.java - var instanceId = GAME_ID - - // Whether we should launch the new godot instance in an adjacent window - // https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_LAUNCH_ADJACENT - var launchAdjacent = shouldGameLaunchAdjacent() - - for (arg in args) { - if (EDITOR_ARG == arg || EDITOR_ARG_SHORT == arg) { - targetClass = GodotEditor::class.java - launchAdjacent = false - instanceId = EDITOR_ID - break - } + protected open fun getEditorWindowInfo(args: Array<String>): EditorWindowInfo { + var hasEditor = false - if (PROJECT_MANAGER_ARG == arg || PROJECT_MANAGER_ARG_SHORT == arg) { - targetClass = GodotProjectManager::class.java - launchAdjacent = false - instanceId = PROJECT_MANAGER_ID - break + 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 + } + } + + override fun onNewGodotInstanceRequested(args: Array<String>): Int { + val editorWindowInfo = getEditorWindowInfo(args) + // Launch a new activity - val newInstance = Intent(this, targetClass) + val newInstance = Intent() + .setComponent(ComponentName(this, editorWindowInfo.windowClassName)) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .putExtra(EXTRA_COMMAND_LINE_PARAMS, args) - if (launchAdjacent) { - newInstance.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (editorWindowInfo.launchAdjacentPolicy == LaunchAdjacentPolicy.ENABLED || + (editorWindowInfo.launchAdjacentPolicy == LaunchAdjacentPolicy.AUTO && shouldGameLaunchAdjacent())) { + Log.v(TAG, "Adding flag for adjacent launch") + newInstance.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT) + } } - if (targetClass == javaClass) { - Log.d(TAG, "Restarting $targetClass with parameters ${args.contentToString()}") + if (editorWindowInfo.windowClassName == javaClass.name) { + Log.d(TAG, "Restarting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}") ProcessPhoenix.triggerRebirth(this, newInstance) } else { - Log.d(TAG, "Starting $targetClass with parameters ${args.contentToString()}") + Log.d(TAG, "Starting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}") newInstance.putExtra(EXTRA_NEW_LAUNCH, true) startActivity(newInstance) } - return instanceId + return editorWindowInfo.windowId } - override fun onGodotForceQuit(godotInstanceId: Int): Boolean { - val targetClass: Class<*>? - val processNameSuffix: String - when (godotInstanceId) { - GAME_ID -> { - processNameSuffix = GAME_PROCESS_NAME_SUFFIX - targetClass = GodotGame::class.java - } - EDITOR_ID -> { - processNameSuffix = EDITOR_PROCESS_NAME_SUFFIX - targetClass = GodotEditor::class.java - } - PROJECT_MANAGER_ID -> { - processNameSuffix = PROJECT_MANAGER_PROCESS_NAME_SUFFIX - targetClass = GodotProjectManager::class.java - } - else -> { - processNameSuffix = "" - targetClass = null - } - } + final override fun onGodotForceQuit(godotInstanceId: Int): Boolean { + val editorWindowInfo = getEditorWindowInfoForInstanceId(godotInstanceId) ?: return super.onGodotForceQuit(godotInstanceId) - if (targetClass == javaClass) { - Log.d(TAG, "Force quitting $targetClass") + if (editorWindowInfo.windowClassName == javaClass.name) { + Log.d(TAG, "Force quitting ${editorWindowInfo.windowClassName}") ProcessPhoenix.forceQuit(this) return true } - if (processNameSuffix.isBlank()) { - return false - } - + val processName = packageName + editorWindowInfo.processNameSuffix val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager val runningProcesses = activityManager.runningAppProcesses for (runningProcess in runningProcesses) { - if (runningProcess.processName.endsWith(processNameSuffix)) { + if (runningProcess.processName == processName) { // Killing process directly Log.v(TAG, "Killing Godot process ${runningProcess.processName}") Process.killProcess(runningProcess.pid) @@ -232,11 +233,11 @@ open class GodotEditor : GodotActivity() { } } - return false + return super.onGodotForceQuit(godotInstanceId) } // Get the screen's density scale - protected val isLargeScreen: Boolean + private val isLargeScreen: Boolean // Get the minimum window size // Correspond to the EXPANDED window size class. get() { val metrics = WindowMetricsCalculator.getOrCreate().computeMaximumWindowMetrics(this) @@ -273,6 +274,10 @@ open class GodotEditor : GodotActivity() { protected open fun enablePanAndScaleGestures() = java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/enable_pan_and_scale_gestures")) + /** + * Whether we should launch the new godot instance in an adjacent window + * @see https://developer.android.com/reference/android/content/Intent#FLAG_ACTIVITY_LAUNCH_ADJACENT + */ private fun shouldGameLaunchAdjacent(): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { try { 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 aa4d02b5b2..8e4e089211 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 @@ -34,6 +34,9 @@ package org.godotengine.editor * Drives the 'run project' window of the Godot Editor. */ class GodotGame : GodotEditor() { + + override fun getGodotAppLayout() = org.godotengine.godot.R.layout.godot_app_layout + override fun overrideOrientationRequest() = false override fun enableLongPressGestures() = false diff --git a/platform/android/java/editor/src/main/res/layout/godot_editor_layout.xml b/platform/android/java/editor/src/main/res/layout/godot_editor_layout.xml new file mode 100644 index 0000000000..431a468f29 --- /dev/null +++ b/platform/android/java/editor/src/main/res/layout/godot_editor_layout.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <FrameLayout + android:id="@+id/godot_fragment_container" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + <ProgressBar + style="@android:style/Widget.Holo.ProgressBar.Large" + android:id="@+id/editor_loading_indicator" + android:layout_width="80dp" + android:layout_height="80dp" + android:indeterminate="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintVertical_bias="0.80"/> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/platform/android/java/editor/src/main/res/values/strings.xml b/platform/android/java/editor/src/main/res/values/strings.xml index 216d02d9c7..909711ab18 100644 --- a/platform/android/java/editor/src/main/res/values/strings.xml +++ b/platform/android/java/editor/src/main/res/values/strings.xml @@ -1,6 +1,4 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <string name="godot_editor_name_string">Godot Editor 4</string> - <string name="denied_storage_permission_error_msg">Missing storage access permission!</string> </resources> diff --git a/platform/android/java/editor/src/main/res/values/themes.xml b/platform/android/java/editor/src/main/res/values/themes.xml index fda04d6dc7..2b352247db 100644 --- a/platform/android/java/editor/src/main/res/values/themes.xml +++ b/platform/android/java/editor/src/main/res/values/themes.xml @@ -2,4 +2,10 @@ <resources> <style name="GodotEditorTheme" parent="@android:style/Theme.Black.NoTitleBar.Fullscreen"> </style> + + <style name="GodotEditorSplashScreenTheme" parent="Theme.SplashScreen.IconBackground"> + <!-- Set the theme of the Activity that directly follows your splash + screen. This is required. --> + <item name="postSplashScreenTheme">@style/GodotEditorTheme</item> + </style> </resources> diff --git a/platform/android/java/lib/build.gradle b/platform/android/java/lib/build.gradle index ed967b9660..81ab598b90 100644 --- a/platform/android/java/lib/build.gradle +++ b/platform/android/java/lib/build.gradle @@ -11,6 +11,8 @@ apply from: "../scripts/publish-module.gradle" dependencies { implementation "androidx.fragment:fragment:$versions.fragmentVersion" + + testImplementation "junit:junit:4.13.2" } def pathToRootDir = "../../../../" @@ -74,6 +76,7 @@ android { main { manifest.srcFile 'AndroidManifest.xml' java.srcDirs = ['src'] + test.java.srcDirs = ['srcTest/java'] res.srcDirs = ['res'] aidl.srcDirs = ['aidl'] assets.srcDirs = ['assets'] @@ -118,7 +121,7 @@ android { case "dev": default: sconsTarget += "_debug" - break; + break } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt index e2e77e7796..290be727ab 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -38,7 +38,6 @@ import android.content.pm.PackageManager import android.content.res.Configuration import android.content.res.Resources import android.graphics.Color -import android.graphics.Rect import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener @@ -46,16 +45,19 @@ import android.hardware.SensorManager import android.os.* import android.util.Log import android.view.* -import android.view.ViewTreeObserver.OnGlobalLayoutListener import android.widget.FrameLayout import androidx.annotation.Keep import androidx.annotation.StringRes +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsAnimationCompat +import androidx.core.view.WindowInsetsCompat import com.google.android.vending.expansion.downloader.* import org.godotengine.godot.input.GodotEditText import org.godotengine.godot.io.directory.DirectoryAccessHandler import org.godotengine.godot.io.file.FileAccessHandler import org.godotengine.godot.plugin.GodotPluginRegistry import org.godotengine.godot.tts.GodotTTS +import org.godotengine.godot.utils.CommandLineFileParser import org.godotengine.godot.utils.GodotNetUtils import org.godotengine.godot.utils.PermissionsUtil import org.godotengine.godot.utils.PermissionsUtil.requestPermission @@ -68,7 +70,7 @@ import org.godotengine.godot.xr.XRMode import java.io.File import java.io.FileInputStream import java.io.InputStream -import java.nio.charset.StandardCharsets +import java.lang.Exception import java.security.MessageDigest import java.util.* @@ -84,6 +86,9 @@ class Godot(private val context: Context) : SensorEventListener { private val TAG = Godot::class.java.simpleName } + private val windowManager: WindowManager by lazy { + requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager + } private val pluginRegistry: GodotPluginRegistry by lazy { GodotPluginRegistry.getPluginRegistry() } @@ -120,6 +125,7 @@ class Godot(private val context: Context) : SensorEventListener { val directoryAccessHandler = DirectoryAccessHandler(context) val fileAccessHandler = FileAccessHandler(context) val netUtils = GodotNetUtils(context) + private val commandLineFileParser = CommandLineFileParser() /** * Tracks whether [onCreate] was completed successfully. @@ -150,7 +156,7 @@ class Godot(private val context: Context) : SensorEventListener { private var useApkExpansion = false private var useImmersive = false private var useDebugOpengl = false - private var darkMode = false; + private var darkMode = false private var containerLayout: FrameLayout? = null var renderView: GodotRenderView? = null @@ -290,7 +296,7 @@ class Godot(private val context: Context) : SensorEventListener { initializationStarted = false throw e } finally { - endBenchmarkMeasure("Startup", "Godot::onCreate"); + endBenchmarkMeasure("Startup", "Godot::onCreate") } } @@ -396,72 +402,62 @@ class Godot(private val context: Context) : SensorEventListener { } if (host == primaryHost) { - renderView!!.startRenderer() + renderView?.startRenderer() } - val view: View = renderView!!.view - containerLayout?.addView( - view, + + renderView?.let { + containerLayout?.addView( + it.view, ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) - ) + ) + } + editText.setView(renderView) io?.setEdit(editText) // Listeners for keyboard height. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - // Report the height of virtual keyboard as it changes during the animation. - val decorView = activity.window.decorView - decorView.setWindowInsetsAnimationCallback(object : WindowInsetsAnimation.Callback(DISPATCH_MODE_STOP) { - var startBottom = 0 - var endBottom = 0 - override fun onPrepare(animation: WindowInsetsAnimation) { - startBottom = decorView.rootWindowInsets.getInsets(WindowInsets.Type.ime()).bottom - } + val decorView = activity.window.decorView + // Report the height of virtual keyboard as it changes during the animation. + ViewCompat.setWindowInsetsAnimationCallback(decorView, object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) { + var startBottom = 0 + var endBottom = 0 + override fun onPrepare(animation: WindowInsetsAnimationCompat) { + startBottom = ViewCompat.getRootWindowInsets(decorView)?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0 + } - override fun onStart(animation: WindowInsetsAnimation, bounds: WindowInsetsAnimation.Bounds): WindowInsetsAnimation.Bounds { - endBottom = decorView.rootWindowInsets.getInsets(WindowInsets.Type.ime()).bottom - return bounds - } + override fun onStart(animation: WindowInsetsAnimationCompat, bounds: WindowInsetsAnimationCompat.BoundsCompat): WindowInsetsAnimationCompat.BoundsCompat { + endBottom = ViewCompat.getRootWindowInsets(decorView)?.getInsets(WindowInsetsCompat.Type.ime())?.bottom ?: 0 + return bounds + } - override fun onProgress(windowInsets: WindowInsets, list: List<WindowInsetsAnimation>): WindowInsets { - // Find the IME animation. - var imeAnimation: WindowInsetsAnimation? = null - for (animation in list) { - if (animation.typeMask and WindowInsets.Type.ime() != 0) { - imeAnimation = animation - break - } - } - // Update keyboard height based on IME animation. - if (imeAnimation != null) { - val interpolatedFraction = imeAnimation.interpolatedFraction - // Linear interpolation between start and end values. - val keyboardHeight = startBottom * (1.0f - interpolatedFraction) + endBottom * interpolatedFraction - GodotLib.setVirtualKeyboardHeight(keyboardHeight.toInt()) + override fun onProgress(windowInsets: WindowInsetsCompat, animationsList: List<WindowInsetsAnimationCompat>): WindowInsetsCompat { + // Find the IME animation. + var imeAnimation: WindowInsetsAnimationCompat? = null + for (animation in animationsList) { + if (animation.typeMask and WindowInsetsCompat.Type.ime() != 0) { + imeAnimation = animation + break } - return windowInsets } - override fun onEnd(animation: WindowInsetsAnimation) {} - }) - } else { - // Infer the virtual keyboard height using visible area. - view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener { - // Don't allocate a new Rect every time the callback is called. - val visibleSize = Rect() - override fun onGlobalLayout() { - val surfaceView = renderView!!.view - surfaceView.getWindowVisibleDisplayFrame(visibleSize) - val keyboardHeight = surfaceView.height - visibleSize.bottom - GodotLib.setVirtualKeyboardHeight(keyboardHeight) + // Update keyboard height based on IME animation. + if (imeAnimation != null) { + val interpolatedFraction = imeAnimation.interpolatedFraction + // Linear interpolation between start and end values. + val keyboardHeight = startBottom * (1.0f - interpolatedFraction) + endBottom * interpolatedFraction + GodotLib.setVirtualKeyboardHeight(keyboardHeight.toInt()) } - }) - } + return windowInsets + } + + override fun onEnd(animation: WindowInsetsAnimationCompat) {} + }) if (host == primaryHost) { - renderView!!.queueOnRenderThread { + renderView?.queueOnRenderThread { for (plugin in pluginRegistry.allPlugins) { plugin.onRegisterPluginWithGodotNative() } @@ -495,7 +491,7 @@ class Godot(private val context: Context) : SensorEventListener { return } - renderView!!.onActivityStarted() + renderView?.onActivityStarted() } fun onResume(host: GodotHost) { @@ -503,7 +499,7 @@ class Godot(private val context: Context) : SensorEventListener { return } - renderView!!.onActivityResumed() + renderView?.onActivityResumed() if (mAccelerometer != null) { mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME) } @@ -535,7 +531,7 @@ class Godot(private val context: Context) : SensorEventListener { return } - renderView!!.onActivityPaused() + renderView?.onActivityPaused() mSensorManager.unregisterListener(this) for (plugin in pluginRegistry.allPlugins) { plugin.onMainPause() @@ -547,7 +543,7 @@ class Godot(private val context: Context) : SensorEventListener { return } - renderView!!.onActivityStopped() + renderView?.onActivityStopped() } fun onDestroy(primaryHost: GodotHost) { @@ -569,7 +565,7 @@ class Godot(private val context: Context) : SensorEventListener { * Configuration change callback */ fun onConfigurationChanged(newConfig: Configuration) { - var newDarkMode = newConfig.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + val newDarkMode = newConfig.uiMode.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES if (darkMode != newDarkMode) { darkMode = newDarkMode GodotLib.onNightModeChanged() @@ -613,7 +609,7 @@ class Godot(private val context: Context) : SensorEventListener { // These properties are defined after Godot setup completion, so we retrieve them here. val longPressEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click")) val panScaleEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures")) - val rotaryInputAxis = java.lang.Integer.parseInt(GodotLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis")); + val rotaryInputAxis = java.lang.Integer.parseInt(GodotLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis")) runOnUiThread { renderView?.inputHandler?.apply { @@ -686,9 +682,7 @@ class Godot(private val context: Context) : SensorEventListener { * This must be called after the render thread has started. */ fun runOnRenderThread(action: Runnable) { - if (renderView != null) { - renderView!!.queueOnRenderThread(action) - } + renderView?.queueOnRenderThread(action) } /** @@ -765,7 +759,7 @@ class Godot(private val context: Context) : SensorEventListener { return mClipboard.hasPrimaryClip() } - fun getClipboard(): String? { + fun getClipboard(): String { val clipData = mClipboard.primaryClip ?: return "" val text = clipData.getItemAt(0).text ?: return "" return text.toString() @@ -782,15 +776,14 @@ class Godot(private val context: Context) : SensorEventListener { @Keep private fun forceQuit(instanceId: Int): Boolean { - if (primaryHost == null) { - return false - } - return if (instanceId == 0) { - primaryHost!!.onGodotForceQuit(this) - true - } else { - primaryHost!!.onGodotForceQuit(instanceId) - } + primaryHost?.let { + if (instanceId == 0) { + it.onGodotForceQuit(this) + return true + } else { + return it.onGodotForceQuit(instanceId) + } + } ?: return false } fun onBackPressed(host: GodotHost) { @@ -804,20 +797,17 @@ class Godot(private val context: Context) : SensorEventListener { shouldQuit = false } } - if (shouldQuit && renderView != null) { - renderView!!.queueOnRenderThread { GodotLib.back() } + if (shouldQuit) { + renderView?.queueOnRenderThread { GodotLib.back() } } } private fun getRotatedValues(values: FloatArray?): FloatArray? { if (values == null || values.size != 3) { - return values + return null } - val display = - (requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay - val displayRotation = display.rotation val rotatedValues = FloatArray(3) - when (displayRotation) { + when (windowManager.defaultDisplay.rotation) { Surface.ROTATION_0 -> { rotatedValues[0] = values[0] rotatedValues[1] = values[1] @@ -846,37 +836,36 @@ class Godot(private val context: Context) : SensorEventListener { if (renderView == null) { return } + + val rotatedValues = getRotatedValues(event.values) + when (event.sensor.type) { Sensor.TYPE_ACCELEROMETER -> { - val rotatedValues = getRotatedValues(event.values) - renderView!!.queueOnRenderThread { - GodotLib.accelerometer( - -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2] - ) + rotatedValues?.let { + renderView?.queueOnRenderThread { + GodotLib.accelerometer(-it[0], -it[1], -it[2]) + } } } Sensor.TYPE_GRAVITY -> { - val rotatedValues = getRotatedValues(event.values) - renderView!!.queueOnRenderThread { - GodotLib.gravity( - -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2] - ) + rotatedValues?.let { + renderView?.queueOnRenderThread { + GodotLib.gravity(-it[0], -it[1], -it[2]) + } } } Sensor.TYPE_MAGNETIC_FIELD -> { - val rotatedValues = getRotatedValues(event.values) - renderView!!.queueOnRenderThread { - GodotLib.magnetometer( - -rotatedValues!![0], -rotatedValues[1], -rotatedValues[2] - ) + rotatedValues?.let { + renderView?.queueOnRenderThread { + GodotLib.magnetometer(-it[0], -it[1], -it[2]) + } } } Sensor.TYPE_GYROSCOPE -> { - val rotatedValues = getRotatedValues(event.values) - renderView!!.queueOnRenderThread { - GodotLib.gyroscope( - rotatedValues!![0], rotatedValues[1], rotatedValues[2] - ) + rotatedValues?.let { + renderView?.queueOnRenderThread { + GodotLib.gyroscope(it[0], it[1], it[2]) + } } } } @@ -890,16 +879,25 @@ class Godot(private val context: Context) : SensorEventListener { */ @SuppressLint("MissingPermission") @Keep - private fun vibrate(durationMs: Int) { + private fun vibrate(durationMs: Int, amplitude: Int) { if (durationMs > 0 && requestPermission("VIBRATE")) { val vibratorService = getActivity()?.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator? ?: return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - vibratorService.vibrate( - VibrationEffect.createOneShot( - durationMs.toLong(), - VibrationEffect.DEFAULT_AMPLITUDE + if (amplitude <= -1) { + vibratorService.vibrate( + VibrationEffect.createOneShot( + durationMs.toLong(), + VibrationEffect.DEFAULT_AMPLITUDE + ) ) - ) + } else { + vibratorService.vibrate( + VibrationEffect.createOneShot( + durationMs.toLong(), + amplitude + ) + ) + } } else { // deprecated in API 26 vibratorService.vibrate(durationMs.toLong()) @@ -908,47 +906,18 @@ class Godot(private val context: Context) : SensorEventListener { } private fun getCommandLine(): MutableList<String> { - val original: MutableList<String> = parseCommandLine() + val commandLine = try { + commandLineFileParser.parseCommandLine(requireActivity().assets.open("_cl_")) + } catch (ignored: Exception) { + mutableListOf() + } + val hostCommandLine = primaryHost?.commandLine if (!hostCommandLine.isNullOrEmpty()) { - original.addAll(hostCommandLine) + commandLine.addAll(hostCommandLine) } - return original - } - private fun parseCommandLine(): MutableList<String> { - val inputStream: InputStream - return try { - inputStream = requireActivity().assets.open("_cl_") - val len = ByteArray(4) - var r = inputStream.read(len) - if (r < 4) { - return mutableListOf() - } - val argc = - (len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF) - val cmdline = ArrayList<String>(argc) - for (i in 0 until argc) { - r = inputStream.read(len) - if (r < 4) { - return mutableListOf() - } - val strlen = - (len[3].toInt() and 0xFF) shl 24 or ((len[2].toInt() and 0xFF) shl 16) or ((len[1].toInt() and 0xFF) shl 8) or (len[0].toInt() and 0xFF) - if (strlen > 65535) { - return mutableListOf() - } - val arg = ByteArray(strlen) - r = inputStream.read(arg) - if (r == strlen) { - cmdline.add(String(arg, StandardCharsets.UTF_8)) - } - } - cmdline - } catch (e: Exception) { - // The _cl_ file can be missing with no adverse effect - mutableListOf() - } + return commandLine } /** @@ -1039,7 +1008,7 @@ class Godot(private val context: Context) : SensorEventListener { @Keep private fun initInputDevices() { - renderView!!.initInputDevices() + renderView?.initInputDevices() } @Keep diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt index e01c5481d5..4c5e857b7a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt @@ -36,6 +36,7 @@ import android.content.pm.PackageManager import android.os.Bundle import android.util.Log import androidx.annotation.CallSuper +import androidx.annotation.LayoutRes import androidx.fragment.app.FragmentActivity import org.godotengine.godot.utils.PermissionsUtil import org.godotengine.godot.utils.ProcessPhoenix @@ -65,7 +66,7 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.godot_app_layout) + setContentView(getGodotAppLayout()) handleStartIntent(intent, true) @@ -80,11 +81,15 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { } } + @LayoutRes + protected open fun getGodotAppLayout() = R.layout.godot_app_layout + override fun onDestroy() { Log.v(TAG, "Destroying Godot app...") super.onDestroy() - if (godotFragment != null) { - terminateGodotInstance(godotFragment!!.godot) + + godotFragment?.let { + terminateGodotInstance(it.godot) } } @@ -93,22 +98,26 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { } private fun terminateGodotInstance(instance: Godot) { - if (godotFragment != null && instance === godotFragment!!.godot) { - Log.v(TAG, "Force quitting Godot instance") - ProcessPhoenix.forceQuit(this) + godotFragment?.let { + if (instance === it.godot) { + Log.v(TAG, "Force quitting Godot instance") + ProcessPhoenix.forceQuit(this) + } } } override fun onGodotRestartRequested(instance: Godot) { runOnUiThread { - if (godotFragment != null && instance === godotFragment!!.godot) { - // It's very hard to properly de-initialize Godot on Android to restart the game - // from scratch. Therefore, we need to kill the whole app process and relaunch it. - // - // Restarting only the activity, wouldn't be enough unless it did proper cleanup (including - // releasing and reloading native libs or resetting their state somehow and clearing static data). - Log.v(TAG, "Restarting Godot instance...") - ProcessPhoenix.triggerRebirth(this) + godotFragment?.let { + if (instance === it.godot) { + // It's very hard to properly de-initialize Godot on Android to restart the game + // from scratch. Therefore, we need to kill the whole app process and relaunch it. + // + // Restarting only the activity, wouldn't be enough unless it did proper cleanup (including + // releasing and reloading native libs or resetting their state somehow and clearing static data). + Log.v(TAG, "Restarting Godot instance...") + ProcessPhoenix.triggerRebirth(this) + } } } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java index bd8c58ad69..c316812404 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java @@ -1955,4 +1955,3 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback private int mEGLContextClientVersion; private boolean mPreserveEGLContextOnPause; } - diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java index dc8a0e54bb..c085bb8886 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotEditText.java @@ -266,8 +266,13 @@ public class GodotEditText extends EditText { boolean hasHardwareKeyboard() { Configuration config = getResources().getConfiguration(); - return config.keyboard != Configuration.KEYBOARD_NOKEYS && + boolean hasHardwareKeyboardConfig = config.keyboard != Configuration.KEYBOARD_NOKEYS && config.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO; + if (hasHardwareKeyboardConfig) { + return true; + } + + return mRenderView.getInputHandler().hasHardwareKeyboard(); } // =========================================================== diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt index 89fbb9f580..49b34a5229 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt @@ -61,8 +61,11 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi private var contextClickInProgress = false private var pointerCaptureInProgress = false + private var lastDragX: Float = 0.0f + private var lastDragY: Float = 0.0f + override fun onDown(event: MotionEvent): Boolean { - GodotInputHandler.handleMotionEvent(event.source, MotionEvent.ACTION_DOWN, event.buttonState, event.x, event.y, nextDownIsDoubleTap) + GodotInputHandler.handleMotionEvent(event, MotionEvent.ACTION_DOWN, nextDownIsDoubleTap) nextDownIsDoubleTap = false return true } @@ -82,20 +85,14 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi } // Cancel the previous down event - GodotInputHandler.handleMotionEvent( - event.source, - MotionEvent.ACTION_CANCEL, - event.buttonState, - event.x, - event.y - ) + GodotInputHandler.handleMotionEvent(event, MotionEvent.ACTION_CANCEL) // Turn a context click into a single tap right mouse button click. GodotInputHandler.handleMouseEvent( + event, MotionEvent.ACTION_DOWN, MotionEvent.BUTTON_SECONDARY, - event.x, - event.y + false ) contextClickInProgress = true } @@ -107,16 +104,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi if (!hasCapture) { // Dispatch a mouse relative ACTION_UP event to signal the end of the capture - GodotInputHandler.handleMouseEvent( - MotionEvent.ACTION_UP, - 0, - 0f, - 0f, - 0f, - 0f, - false, - true - ) + GodotInputHandler.handleMouseEvent(MotionEvent.ACTION_UP, true) } pointerCaptureInProgress = hasCapture } @@ -139,32 +127,19 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi return true } - val sourceMouseRelative = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE) - } else { - false - } - if (pointerCaptureInProgress || dragInProgress || contextClickInProgress) { if (contextClickInProgress || GodotInputHandler.isMouseEvent(event)) { // This may be an ACTION_BUTTON_RELEASE event which we don't handle, // so we convert it to an ACTION_UP event. - GodotInputHandler.handleMouseEvent( - MotionEvent.ACTION_UP, - event.buttonState, - event.x, - event.y, - 0f, - 0f, - false, - sourceMouseRelative - ) + GodotInputHandler.handleMouseEvent(event, MotionEvent.ACTION_UP) } else { GodotInputHandler.handleTouchEvent(event) } pointerCaptureInProgress = false dragInProgress = false contextClickInProgress = false + lastDragX = 0.0f + lastDragY = 0.0f return true } @@ -173,22 +148,19 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi private fun onActionMove(event: MotionEvent): Boolean { if (contextClickInProgress) { - val sourceMouseRelative = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE) - } else { - false - } - GodotInputHandler.handleMouseEvent( - event.actionMasked, - MotionEvent.BUTTON_SECONDARY, - event.x, - event.y, - 0f, - 0f, - false, - sourceMouseRelative - ) + GodotInputHandler.handleMouseEvent(event, event.actionMasked, MotionEvent.BUTTON_SECONDARY, false) return true + } else if (!scaleInProgress) { + // The 'onScroll' event is triggered with a long delay. + // Force the 'InputEventScreenDrag' event earlier here. + // We don't toggle 'dragInProgress' here so that the scaling logic can override the drag operation if needed. + // Once the 'onScroll' event kicks-in, 'dragInProgress' will be properly set. + if (lastDragX != event.getX(0) || lastDragY != event.getY(0)) { + lastDragX = event.getX(0) + lastDragY = event.getY(0) + GodotInputHandler.handleMotionEvent(event) + return true + } } return false } @@ -197,7 +169,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi if (event.actionMasked == MotionEvent.ACTION_UP) { nextDownIsDoubleTap = false GodotInputHandler.handleMotionEvent(event) - } else if (event.actionMasked == MotionEvent.ACTION_MOVE && panningAndScalingEnabled == false) { + } else if (event.actionMasked == MotionEvent.ACTION_MOVE && !panningAndScalingEnabled) { GodotInputHandler.handleMotionEvent(event) } @@ -216,18 +188,14 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi distanceY: Float ): Boolean { if (scaleInProgress) { - if (dragInProgress) { + if (dragInProgress || lastDragX != 0.0f || lastDragY != 0.0f) { if (originEvent != null) { // Cancel the drag - GodotInputHandler.handleMotionEvent( - originEvent.source, - MotionEvent.ACTION_CANCEL, - originEvent.buttonState, - originEvent.x, - originEvent.y - ) + GodotInputHandler.handleMotionEvent(originEvent, MotionEvent.ACTION_CANCEL) } dragInProgress = false + lastDragX = 0.0f + lastDragY = 0.0f } } @@ -235,8 +203,10 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi val y = terminusEvent.y if (terminusEvent.pointerCount >= 2 && panningAndScalingEnabled && !pointerCaptureInProgress && !dragInProgress) { GodotLib.pan(x, y, distanceX / 5f, distanceY / 5f) - } else if (!scaleInProgress){ + } else if (!scaleInProgress) { dragInProgress = true + lastDragX = terminusEvent.getX(0) + lastDragY = terminusEvent.getY(0) GodotInputHandler.handleMotionEvent(terminusEvent) } return true diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java index fe971cf442..83e76e49c9 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java @@ -62,6 +62,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { private final SparseIntArray mJoystickIds = new SparseIntArray(4); private final SparseArray<Joystick> mJoysticksDevices = new SparseArray<>(4); + private final HashSet<Integer> mHardwareKeyboardIds = new HashSet<>(); private final GodotRenderView mRenderView; private final InputManager mInputManager; @@ -114,6 +115,10 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { rotaryInputAxis = axis; } + boolean hasHardwareKeyboard() { + return !mHardwareKeyboardIds.isEmpty(); + } + private boolean isKeyEventGameDevice(int source) { // Note that keyboards are often (SOURCE_KEYBOARD | SOURCE_DPAD) if (source == (InputDevice.SOURCE_KEYBOARD | InputDevice.SOURCE_DPAD)) @@ -195,7 +200,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } public boolean onTouchEvent(final MotionEvent event) { - lastSeenToolType = event.getToolType(0); + lastSeenToolType = getEventToolType(event); this.scaleGestureDetector.onTouchEvent(event); if (this.gestureDetector.onTouchEvent(event)) { @@ -221,17 +226,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } public boolean onGenericMotionEvent(MotionEvent event) { - lastSeenToolType = event.getToolType(0); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && gestureDetector.onGenericMotionEvent(event)) { - // The gesture detector has handled the event. - return true; - } - - if (godotGestureHandler.onMotionEvent(event)) { - // The gesture handler has handled the event. - return true; - } + lastSeenToolType = getEventToolType(event); if (event.isFromSource(InputDevice.SOURCE_JOYSTICK) && event.getActionMasked() == MotionEvent.ACTION_MOVE) { // Check if the device exists @@ -268,11 +263,20 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } return true; } - } else { - return handleMouseEvent(event); + return false; } - return false; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && gestureDetector.onGenericMotionEvent(event)) { + // The gesture detector has handled the event. + return true; + } + + if (godotGestureHandler.onMotionEvent(event)) { + // The gesture handler has handled the event. + return true; + } + + return handleMouseEvent(event); } public void initInputDevices() { @@ -310,11 +314,17 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return; } - int sources = device.getSources(); + // Device may be an external keyboard; store the device id + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && + device.supportsSource(InputDevice.SOURCE_KEYBOARD) && + device.isExternal() && + device.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) { + mHardwareKeyboardIds.add(deviceId); + } // Device may not be a joystick or gamepad - if ((sources & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD && - (sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) { + if (!device.supportsSource(InputDevice.SOURCE_GAMEPAD) && + !device.supportsSource(InputDevice.SOURCE_JOYSTICK)) { return; } @@ -359,6 +369,8 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { @Override public void onInputDeviceRemoved(int deviceId) { + mHardwareKeyboardIds.remove(deviceId); + // Check if the device has not been already removed if (mJoystickIds.indexOfKey(deviceId) < 0) { return; @@ -440,50 +452,65 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return button; } - static boolean isMouseEvent(MotionEvent event) { - return isMouseEvent(event.getSource()); + private static int getEventToolType(MotionEvent event) { + return event.getPointerCount() > 0 ? event.getToolType(0) : MotionEvent.TOOL_TYPE_UNKNOWN; } - private static boolean isMouseEvent(int eventSource) { - boolean mouseSource = ((eventSource & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) || ((eventSource & InputDevice.SOURCE_STYLUS) == InputDevice.SOURCE_STYLUS); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - mouseSource = mouseSource || ((eventSource & InputDevice.SOURCE_MOUSE_RELATIVE) == InputDevice.SOURCE_MOUSE_RELATIVE); + static boolean isMouseEvent(MotionEvent event) { + int toolType = getEventToolType(event); + int eventSource = event.getSource(); + + switch (toolType) { + case MotionEvent.TOOL_TYPE_FINGER: + return false; + + case MotionEvent.TOOL_TYPE_MOUSE: + case MotionEvent.TOOL_TYPE_STYLUS: + case MotionEvent.TOOL_TYPE_ERASER: + return true; + + case MotionEvent.TOOL_TYPE_UNKNOWN: + default: + boolean mouseSource = + ((eventSource & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) || + ((eventSource & (InputDevice.SOURCE_TOUCHSCREEN | InputDevice.SOURCE_STYLUS)) == InputDevice.SOURCE_STYLUS); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + mouseSource = mouseSource || + ((eventSource & InputDevice.SOURCE_MOUSE_RELATIVE) == InputDevice.SOURCE_MOUSE_RELATIVE); + } + return mouseSource; } - return mouseSource; } static boolean handleMotionEvent(final MotionEvent event) { - if (isMouseEvent(event)) { - return handleMouseEvent(event); - } - - return handleTouchEvent(event); + return handleMotionEvent(event, event.getActionMasked()); } - static boolean handleMotionEvent(int eventSource, int eventAction, int buttonsMask, float x, float y) { - return handleMotionEvent(eventSource, eventAction, buttonsMask, x, y, false); + static boolean handleMotionEvent(final MotionEvent event, int eventActionOverride) { + return handleMotionEvent(event, eventActionOverride, false); } - static boolean handleMotionEvent(int eventSource, int eventAction, int buttonsMask, float x, float y, boolean doubleTap) { - return handleMotionEvent(eventSource, eventAction, buttonsMask, x, y, 0, 0, doubleTap); + static boolean handleMotionEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { + if (isMouseEvent(event)) { + return handleMouseEvent(event, eventActionOverride, doubleTap); + } + return handleTouchEvent(event, eventActionOverride, doubleTap); } - static boolean handleMotionEvent(int eventSource, int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleTap) { - if (isMouseEvent(eventSource)) { - return handleMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleTap, false); - } + private static float getEventTiltX(MotionEvent event) { + // Orientation is returned as a radian value between 0 to pi clockwise or 0 to -pi counterclockwise. + final float orientation = event.getOrientation(); - return handleTouchEvent(eventAction, x, y, doubleTap); - } + // Tilt is zero is perpendicular to the screen and pi/2 is flat on the surface. + final float tilt = event.getAxisValue(MotionEvent.AXIS_TILT); - static boolean handleMouseEvent(final MotionEvent event) { - final int eventAction = event.getActionMasked(); - final float x = event.getX(); - final float y = event.getY(); - final int buttonsMask = event.getButtonState(); + float tiltMult = (float)Math.sin(tilt); - final float pressure = event.getPressure(); + // To be consistent with expected tilt. + return (float)-Math.sin(orientation) * tiltMult; + } + private static float getEventTiltY(MotionEvent event) { // Orientation is returned as a radian value between 0 to pi clockwise or 0 to -pi counterclockwise. final float orientation = event.getOrientation(); @@ -493,8 +520,26 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { float tiltMult = (float)Math.sin(tilt); // To be consistent with expected tilt. - final float tiltX = (float)-Math.sin(orientation) * tiltMult; - final float tiltY = (float)Math.cos(orientation) * tiltMult; + return (float)Math.cos(orientation) * tiltMult; + } + + static boolean handleMouseEvent(final MotionEvent event) { + return handleMouseEvent(event, event.getActionMasked()); + } + + static boolean handleMouseEvent(final MotionEvent event, int eventActionOverride) { + return handleMouseEvent(event, eventActionOverride, false); + } + + static boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { + return handleMouseEvent(event, eventActionOverride, event.getButtonState(), doubleTap); + } + + static boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, int buttonMaskOverride, boolean doubleTap) { + final float x = event.getX(); + final float y = event.getY(); + + final float pressure = event.getPressure(); float verticalFactor = 0; float horizontalFactor = 0; @@ -516,15 +561,11 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { sourceMouseRelative = event.isFromSource(InputDevice.SOURCE_MOUSE_RELATIVE); } - return handleMouseEvent(eventAction, buttonsMask, x, y, horizontalFactor, verticalFactor, false, sourceMouseRelative, pressure, tiltX, tiltY); - } - - static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y) { - return handleMouseEvent(eventAction, buttonsMask, x, y, 0, 0, false, false); + return handleMouseEvent(eventActionOverride, buttonMaskOverride, x, y, horizontalFactor, verticalFactor, doubleTap, sourceMouseRelative, pressure, getEventTiltX(event), getEventTiltY(event)); } - static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative) { - return handleMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, 1, 0, 0); + static boolean handleMouseEvent(int eventAction, boolean sourceMouseRelative) { + return handleMouseEvent(eventAction, 0, 0f, 0f, 0f, 0f, false, sourceMouseRelative, 1f, 0f, 0f); } static boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) { @@ -563,37 +604,39 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } static boolean handleTouchEvent(final MotionEvent event) { + return handleTouchEvent(event, event.getActionMasked()); + } + + static boolean handleTouchEvent(final MotionEvent event, int eventActionOverride) { + return handleTouchEvent(event, eventActionOverride, false); + } + + static boolean handleTouchEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { final int pointerCount = event.getPointerCount(); if (pointerCount == 0) { return true; } - final float[] positions = new float[pointerCount * 3]; // pointerId1, x1, y1, pointerId2, etc... + final float[] positions = new float[pointerCount * 6]; // pointerId1, x1, y1, pressure1, tiltX1, tiltY1, pointerId2, etc... for (int i = 0; i < pointerCount; i++) { - positions[i * 3 + 0] = event.getPointerId(i); - positions[i * 3 + 1] = event.getX(i); - positions[i * 3 + 2] = event.getY(i); + positions[i * 6 + 0] = event.getPointerId(i); + positions[i * 6 + 1] = event.getX(i); + positions[i * 6 + 2] = event.getY(i); + positions[i * 6 + 3] = event.getPressure(i); + positions[i * 6 + 4] = getEventTiltX(event); + positions[i * 6 + 5] = getEventTiltY(event); } - final int action = event.getActionMasked(); final int actionPointerId = event.getPointerId(event.getActionIndex()); - return handleTouchEvent(action, actionPointerId, pointerCount, positions, false); - } - - static boolean handleTouchEvent(int eventAction, float x, float y, boolean doubleTap) { - return handleTouchEvent(eventAction, 0, 1, new float[] { 0, x, y }, doubleTap); - } - - static boolean handleTouchEvent(int eventAction, int actionPointerId, int pointerCount, float[] positions, boolean doubleTap) { - switch (eventAction) { + switch (eventActionOverride) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_POINTER_DOWN: { - GodotLib.dispatchTouchEvent(eventAction, actionPointerId, pointerCount, positions, doubleTap); + GodotLib.dispatchTouchEvent(eventActionOverride, actionPointerId, pointerCount, positions, doubleTap); return true; } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt index 0f447f0b05..11cf7b3566 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt @@ -36,7 +36,9 @@ import android.util.Log import org.godotengine.godot.io.StorageScope import java.io.IOException import java.nio.ByteBuffer +import java.nio.channels.ClosedChannelException import java.nio.channels.FileChannel +import java.nio.channels.NonWritableChannelException import kotlin.math.max /** @@ -135,6 +137,21 @@ internal abstract class DataAccess(private val filePath: String) { seek(positionFromBeginning) } + fun resize(length: Long): Int { + return try { + fileChannel.truncate(length) + FileErrors.OK.nativeValue + } catch (e: NonWritableChannelException) { + FileErrors.FILE_CANT_OPEN.nativeValue + } catch (e: ClosedChannelException) { + FileErrors.FILE_CANT_OPEN.nativeValue + } catch (e: IllegalArgumentException) { + FileErrors.INVALID_PARAMETER.nativeValue + } catch (e: IOException) { + FileErrors.FAILED.nativeValue + } + } + fun position(): Long { return try { fileChannel.position() diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt index 984bf607d0..1d773467e8 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt @@ -45,7 +45,6 @@ class FileAccessHandler(val context: Context) { companion object { private val TAG = FileAccessHandler::class.java.simpleName - private const val FILE_NOT_FOUND_ERROR_ID = -1 internal const val INVALID_FILE_ID = 0 private const val STARTING_FILE_ID = 1 @@ -56,7 +55,9 @@ class FileAccessHandler(val context: Context) { } return try { - DataAccess.fileExists(storageScope, context, path!!) + path?.let { + DataAccess.fileExists(storageScope, context, it) + } ?: false } catch (e: SecurityException) { false } @@ -69,20 +70,22 @@ class FileAccessHandler(val context: Context) { } return try { - DataAccess.removeFile(storageScope, context, path!!) + path?.let { + DataAccess.removeFile(storageScope, context, it) + } ?: false } catch (e: Exception) { false } } - internal fun renameFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, from: String?, to: String?): Boolean { + internal fun renameFile(context: Context, storageScopeIdentifier: StorageScope.Identifier, from: String, to: String): Boolean { val storageScope = storageScopeIdentifier.identifyStorageScope(from) if (storageScope == StorageScope.UNKNOWN) { return false } return try { - DataAccess.renameFile(storageScope, context, from!!, to!!) + DataAccess.renameFile(storageScope, context, from, to) } catch (e: Exception) { false } @@ -106,16 +109,18 @@ class FileAccessHandler(val context: Context) { return INVALID_FILE_ID } - try { - val dataAccess = DataAccess.generateDataAccess(storageScope, context, path!!, accessFlag) ?: return INVALID_FILE_ID + return try { + path?.let { + val dataAccess = DataAccess.generateDataAccess(storageScope, context, it, accessFlag) ?: return INVALID_FILE_ID - files.put(++lastFileId, dataAccess) - return lastFileId + files.put(++lastFileId, dataAccess) + lastFileId + } ?: INVALID_FILE_ID } catch (e: FileNotFoundException) { - return FILE_NOT_FOUND_ERROR_ID + FileErrors.FILE_NOT_FOUND.nativeValue } catch (e: Exception) { Log.w(TAG, "Error while opening $path", e) - return INVALID_FILE_ID + INVALID_FILE_ID } } @@ -176,12 +181,22 @@ class FileAccessHandler(val context: Context) { } return try { - DataAccess.fileLastModified(storageScope, context, filepath!!) + filepath?.let { + DataAccess.fileLastModified(storageScope, context, it) + } ?: 0L } catch (e: SecurityException) { 0L } } + fun fileResize(fileId: Int, length: Long): Int { + if (!hasFileId(fileId)) { + return FileErrors.FAILED.nativeValue + } + + return files[fileId].resize(length) + } + fun fileGetPosition(fileId: Int): Long { if (!hasFileId(fileId)) { return 0L diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt index d0e4279eeb..2df0195de7 100644 --- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotProjectManager.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt @@ -1,5 +1,5 @@ /**************************************************************************/ -/* GodotProjectManager.kt */ +/* FileErrors.kt */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ @@ -28,18 +28,26 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ -package org.godotengine.editor +package org.godotengine.godot.io.file /** - * Launcher activity for the Godot Android Editor. - * - * It presents the user with the project manager interface. - * Upon selection of a project, this activity (via its parent logic) starts the - * [GodotEditor] activity. + * Set of errors that may occur when performing data access. */ -class GodotProjectManager : GodotEditor() { - override fun checkForProjectPermissionsToEnable() { - // Nothing to do here.. we have yet to select a project to load. +internal enum class FileErrors(val nativeValue: Int) { + OK(0), + FAILED(-1), + FILE_NOT_FOUND(-2), + FILE_CANT_OPEN(-3), + INVALID_PARAMETER(-4); + + companion object { + fun fromNativeError(error: Int): FileErrors? { + for (fileError in entries) { + if (fileError.nativeValue == error) { + return fileError + } + } + return null + } } } - diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java index c0912ca4dc..c975c29e96 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java +++ b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPlugin.java @@ -112,19 +112,18 @@ public abstract class GodotPlugin { /** * Register the plugin with Godot native code. * <p> - * This method is invoked by the Godot Engine on the render thread. + * This method is invoked on the render thread to register the plugin on engine startup. */ public final void onRegisterPluginWithGodotNative() { - registeredSignals.putAll( - registerPluginWithGodotNative(this, getPluginName(), getPluginMethods(), getPluginSignals())); - } + final String pluginName = getPluginName(); + if (!nativeRegisterSingleton(pluginName, this)) { + return; + } - private static Map<String, SignalInfo> registerPluginWithGodotNative(Object pluginObject, - String pluginName, List<String> pluginMethods, Set<SignalInfo> pluginSignals) { - nativeRegisterSingleton(pluginName, pluginObject); + List<String> pluginMethods = getPluginMethods(); Set<Method> filteredMethods = new HashSet<>(); - Class<?> clazz = pluginObject.getClass(); + Class<?> clazz = getClass(); Method[] methods = clazz.getDeclaredMethods(); for (Method method : methods) { @@ -156,15 +155,14 @@ public abstract class GodotPlugin { nativeRegisterMethod(pluginName, method.getName(), method.getReturnType().getName(), pt); } + Set<SignalInfo> pluginSignals = getPluginSignals(); + // Register the signals for this plugin. - Map<String, SignalInfo> registeredSignals = new HashMap<>(); for (SignalInfo signalInfo : pluginSignals) { String signalName = signalInfo.getName(); nativeRegisterSignal(pluginName, signalName, signalInfo.getParamTypesNames()); registeredSignals.put(signalName, signalInfo); } - - return registeredSignals; } /** @@ -408,7 +406,7 @@ public abstract class GodotPlugin { * Used to setup a {@link GodotPlugin} instance. * @param p_name Name of the instance. */ - private static native void nativeRegisterSingleton(String p_name, Object object); + private static native boolean nativeRegisterSingleton(String p_name, Object object); /** * Used to complete registration of the {@link GodotPlugin} instance's methods. diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt new file mode 100644 index 0000000000..ce5c5b6714 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/CommandLineFileParser.kt @@ -0,0 +1,83 @@ +/**************************************************************************/ +/* CommandLineFileParser.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.godot.utils + +import java.io.InputStream +import java.nio.charset.StandardCharsets +import java.util.ArrayList + +/** + * A class that parses the content of file storing command line params. Usually, this file is saved + * in `assets/_cl_` on exporting an apk + * + * Returns a mutable list of command lines + */ +internal class CommandLineFileParser { + fun parseCommandLine(inputStream: InputStream): MutableList<String> { + return try { + val headerBytes = ByteArray(4) + var argBytes = inputStream.read(headerBytes) + if (argBytes < 4) { + return mutableListOf() + } + val argc = decodeHeaderIntValue(headerBytes) + + val cmdline = ArrayList<String>(argc) + for (i in 0 until argc) { + argBytes = inputStream.read(headerBytes) + if (argBytes < 4) { + return mutableListOf() + } + val strlen = decodeHeaderIntValue(headerBytes) + + if (strlen > 65535) { + return mutableListOf() + } + + val arg = ByteArray(strlen) + argBytes = inputStream.read(arg) + if (argBytes == strlen) { + cmdline.add(String(arg, StandardCharsets.UTF_8)) + } + } + cmdline + } catch (e: Exception) { + // The _cl_ file can be missing with no adverse effect + mutableListOf() + } + } + + private fun decodeHeaderIntValue(headerBytes: ByteArray): Int = + (headerBytes[3].toInt() and 0xFF) shl 24 or + ((headerBytes[2].toInt() and 0xFF) shl 16) or + ((headerBytes[1].toInt() and 0xFF) shl 8) or + (headerBytes[0].toInt() and 0xFF) +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java index 9df890e6bd..4e8e82a70a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java @@ -125,7 +125,7 @@ public final class PermissionsUtil { } activity.requestPermissions(requestedPermissions.toArray(new String[0]), REQUEST_ALL_PERMISSION_REQ_CODE); - return true; + return false; } /** @@ -281,8 +281,9 @@ public final class PermissionsUtil { public static boolean hasManifestPermission(Context context, String permission) { try { for (String p : getManifestPermissions(context)) { - if (permission.equals(p)) + if (permission.equals(p)) { return true; + } } } catch (PackageManager.NameNotFoundException ignored) { } @@ -299,8 +300,9 @@ public final class PermissionsUtil { public static ArrayList<String> getManifestPermissions(Context context) throws PackageManager.NameNotFoundException { PackageManager packageManager = context.getPackageManager(); PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), PackageManager.GET_PERMISSIONS); - if (packageInfo.requestedPermissions == null) - return new ArrayList<String>(); + if (packageInfo.requestedPermissions == null) { + return new ArrayList<>(); + } return new ArrayList<>(Arrays.asList(packageInfo.requestedPermissions)); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt index 4aba0c370d..8c0065b31e 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt @@ -142,7 +142,7 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk fun onSurfaceChanged(width: Int, height: Int) { lock.withLock { hasSurface = true - surfaceChanged = true; + surfaceChanged = true this.width = width this.height = height @@ -179,7 +179,7 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk // blocking the thread lifecycle by holding onto the lock. if (eventQueue.isNotEmpty()) { event = eventQueue.removeAt(0) - break; + break } if (readyToDraw) { @@ -199,7 +199,7 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk } // Break out of the loop so drawing can occur without holding onto the lock. - break; + break } else if (rendererResumed) { // If we aren't ready to draw but are resumed, that means we either lost a surface // or the app was paused. diff --git a/platform/android/java/lib/srcTest/java/org/godotengine/godot/utils/CommandLineFileParserTest.kt b/platform/android/java/lib/srcTest/java/org/godotengine/godot/utils/CommandLineFileParserTest.kt new file mode 100644 index 0000000000..8b0466848a --- /dev/null +++ b/platform/android/java/lib/srcTest/java/org/godotengine/godot/utils/CommandLineFileParserTest.kt @@ -0,0 +1,104 @@ +/**************************************************************************/ +/* CommandLineFileParserTest.kt */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +package org.godotengine.godot.utils + +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import java.io.ByteArrayInputStream +import java.io.InputStream + +// Godot saves command line params in the `assets/_cl_` file on exporting an apk. By default, +// without any other commands specified in `command_line/extra_args` in Export window, the content +// of that _cl_ file consists of only the `--xr_mode_regular` and `--use_immersive` flags. +// The `CL_` prefix here refers to that file +private val CL_DEFAULT_NO_EXTRA_ARGS = byteArrayOf(2, 0, 0, 0, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101) +private val CL_ONE_EXTRA_ARG = byteArrayOf(3, 0, 0, 0, 15, 0, 0, 0, 45, 45, 117, 110, 105, 116, 95, 116, 101, 115, 116, 95, 97, 114, 103, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101) +private val CL_TWO_EXTRA_ARGS = byteArrayOf(4, 0, 0, 0, 16, 0, 0, 0, 45, 45, 117, 110, 105, 116, 95, 116, 101, 115, 116, 95, 97, 114, 103, 49, 16, 0, 0, 0, 45, 45, 117, 110, 105, 116, 95, 116, 101, 115, 116, 95, 97, 114, 103, 50, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101) +private val CL_EMPTY = byteArrayOf() +private val CL_HEADER_TOO_SHORT = byteArrayOf(0, 0, 0) +private val CL_INCOMPLETE_FIRST_ARG = byteArrayOf(2, 0, 0, 0, 17, 0, 0) +private val CL_LENGTH_TOO_LONG_IN_FIRST_ARG = byteArrayOf(2, 0, 0, 0, 17, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 114, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101) +private val CL_MISMATCHED_ARG_LENGTH_AND_HEADER_ONE_ARG = byteArrayOf(2, 0, 0, 0, 10, 0, 0, 0, 45, 45, 120, 114) +private val CL_MISMATCHED_ARG_LENGTH_AND_HEADER_IN_FIRST_ARG = byteArrayOf(2, 0, 0, 0, 17, 0, 0, 0, 45, 45, 120, 114, 95, 109, 111, 100, 101, 95, 114, 101, 103, 117, 108, 97, 15, 0, 0, 0, 45, 45, 117, 115, 101, 95, 105, 109, 109, 101, 114, 115, 105, 118, 101) + +@RunWith(Parameterized::class) +class CommandLineFileParserTest( + private val inputStreamArg: InputStream, + private val expectedResult: List<String>, +) { + + private val commandLineFileParser = CommandLineFileParser() + + companion object { + @JvmStatic + @Parameterized.Parameters + fun data() = listOf( + arrayOf(ByteArrayInputStream(CL_EMPTY), listOf<String>()), + arrayOf(ByteArrayInputStream(CL_HEADER_TOO_SHORT), listOf<String>()), + + arrayOf(ByteArrayInputStream(CL_DEFAULT_NO_EXTRA_ARGS), listOf( + "--xr_mode_regular", + "--use_immersive", + )), + + arrayOf(ByteArrayInputStream(CL_ONE_EXTRA_ARG), listOf( + "--unit_test_arg", + "--xr_mode_regular", + "--use_immersive", + )), + + arrayOf(ByteArrayInputStream(CL_TWO_EXTRA_ARGS), listOf( + "--unit_test_arg1", + "--unit_test_arg2", + "--xr_mode_regular", + "--use_immersive", + )), + + arrayOf(ByteArrayInputStream(CL_INCOMPLETE_FIRST_ARG), listOf<String>()), + arrayOf(ByteArrayInputStream(CL_LENGTH_TOO_LONG_IN_FIRST_ARG), listOf<String>()), + arrayOf(ByteArrayInputStream(CL_MISMATCHED_ARG_LENGTH_AND_HEADER_ONE_ARG), listOf<String>()), + arrayOf(ByteArrayInputStream(CL_MISMATCHED_ARG_LENGTH_AND_HEADER_IN_FIRST_ARG), listOf<String>()), + ) + } + + @Test + fun `Given inputStream, When parsing command line, Then a correct list is returned`() { + // given + val inputStream = inputStreamArg + + // when + val result = commandLineFileParser.parseCommandLine(inputStream) + + // then + assert(result == expectedResult) { "Expected: $expectedResult Actual: $result" } + } +} diff --git a/platform/android/java/nativeSrcsConfigs/CMakeLists.txt b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt index e1534c7685..96b6dfc9f3 100644 --- a/platform/android/java/nativeSrcsConfigs/CMakeLists.txt +++ b/platform/android/java/nativeSrcsConfigs/CMakeLists.txt @@ -7,6 +7,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) set(GODOT_ROOT_DIR ../../../..) +set(ANDROID_ROOT_DIR "${GODOT_ROOT_DIR}/platform/android" CACHE STRING "") # Get sources file(GLOB_RECURSE SOURCES ${GODOT_ROOT_DIR}/*.c**) @@ -15,6 +16,7 @@ file(GLOB_RECURSE HEADERS ${GODOT_ROOT_DIR}/*.h**) add_executable(${PROJECT_NAME} ${SOURCES} ${HEADERS}) target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC - ${GODOT_ROOT_DIR}) + ${GODOT_ROOT_DIR} + ${ANDROID_ROOT_DIR}) add_definitions(-DUNIX_ENABLED -DVULKAN_ENABLED -DANDROID_ENABLED -DGLES3_ENABLED -DTOOLS_ENABLED) diff --git a/platform/android/java/scripts/publish-root.gradle b/platform/android/java/scripts/publish-root.gradle index ae88487c34..31338de21e 100644 --- a/platform/android/java/scripts/publish-root.gradle +++ b/platform/android/java/scripts/publish-root.gradle @@ -36,4 +36,3 @@ nexusPublishing { } } } - |
