diff options
Diffstat (limited to 'platform')
16 files changed, 392 insertions, 234 deletions
diff --git a/platform/SCsub b/platform/SCsub index e432cebd48..ca282e3e68 100644 --- a/platform/SCsub +++ b/platform/SCsub @@ -1,5 +1,7 @@ #!/usr/bin/env python +import methods + Import("env") env.platform_sources = [] @@ -18,12 +20,7 @@ reg_apis_inc += "\n" reg_apis += "}\n\n" unreg_apis += "}\n" -# NOTE: It is safe to generate this file here, since this is still execute serially -with open("register_platform_apis.gen.cpp", "w", encoding="utf-8", newline="\n") as f: - f.write(reg_apis_inc) - f.write(reg_apis) - f.write(unreg_apis) - +methods.write_file_if_needed("register_platform_apis.gen.cpp", reg_apis_inc + reg_apis + unreg_apis) env.add_source_files(env.platform_sources, "register_platform_apis.gen.cpp") lib = env.add_library("platform", env.platform_sources) diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 64ef1397ba..19e215bfaf 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -1920,7 +1920,15 @@ bool EditorExportPlatformAndroid::get_export_option_visibility(const EditorExpor bool advanced_options_enabled = p_preset->are_advanced_options_enabled(); if (p_option == "graphics/opengl_debug" || p_option == "command_line/extra_args" || - p_option == "permissions/custom_permissions") { + p_option == "permissions/custom_permissions" || + p_option == "gradle_build/compress_native_libraries" || + p_option == "package/retain_data_on_uninstall" || + p_option == "package/exclude_from_recents" || + p_option == "package/show_in_app_library" || + p_option == "package/show_as_launcher_app" || + p_option == "apk_expansion/enable" || + p_option == "apk_expansion/SALT" || + p_option == "apk_expansion/public_key") { return advanced_options_enabled; } if (p_option == "gradle_build/gradle_build_directory" || p_option == "gradle_build/android_source_template") { diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index f2c4a5d1b6..d27e75b07a 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -194,17 +194,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 = { diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle index 0f7ffeecae..c5ef086152 100644 --- a/platform/android/java/editor/build.gradle +++ b/platform/android/java/editor/build.gradle @@ -20,7 +20,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 } 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..c9a62d24b7 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 @@ -127,7 +127,7 @@ open class GodotEditor : GodotActivity() { */ 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) } 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..ce53aeebcb 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -56,6 +56,7 @@ 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 +69,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 +85,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 +124,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 +155,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 +295,7 @@ class Godot(private val context: Context) : SensorEventListener { initializationStarted = false throw e } finally { - endBenchmarkMeasure("Startup", "Godot::onCreate"); + endBenchmarkMeasure("Startup", "Godot::onCreate") } } @@ -396,16 +401,19 @@ 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) @@ -448,20 +456,23 @@ class Godot(private val context: Context) : SensorEventListener { }) } else { // Infer the virtual keyboard height using visible area. - view.viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener { + renderView?.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) + renderView?.let { + val surfaceView = it.view + + surfaceView.getWindowVisibleDisplayFrame(visibleSize) + val keyboardHeight = surfaceView.height - visibleSize.bottom + GodotLib.setVirtualKeyboardHeight(keyboardHeight) + } } }) } if (host == primaryHost) { - renderView!!.queueOnRenderThread { + renderView?.queueOnRenderThread { for (plugin in pluginRegistry.allPlugins) { plugin.onRegisterPluginWithGodotNative() } @@ -495,7 +506,7 @@ class Godot(private val context: Context) : SensorEventListener { return } - renderView!!.onActivityStarted() + renderView?.onActivityStarted() } fun onResume(host: GodotHost) { @@ -503,7 +514,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 +546,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 +558,7 @@ class Godot(private val context: Context) : SensorEventListener { return } - renderView!!.onActivityStopped() + renderView?.onActivityStopped() } fun onDestroy(primaryHost: GodotHost) { @@ -569,7 +580,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 +624,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 +697,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 +774,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 +791,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 +812,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 +851,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]) + } } } } @@ -908,47 +912,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 +1014,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..7b8fad8952 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt @@ -83,8 +83,9 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { override fun onDestroy() { Log.v(TAG, "Destroying Godot app...") super.onDestroy() - if (godotFragment != null) { - terminateGodotInstance(godotFragment!!.godot) + + godotFragment?.let { + terminateGodotInstance(it.godot) } } @@ -93,22 +94,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/io/file/FileAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt index 984bf607d0..50741c1aab 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 @@ -56,7 +56,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 +71,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 +110,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 + FILE_NOT_FOUND_ERROR_ID } catch (e: Exception) { Log.w(TAG, "Error while opening $path", e) - return INVALID_FILE_ID + INVALID_FILE_ID } } @@ -176,7 +182,9 @@ class FileAccessHandler(val context: Context) { } return try { - DataAccess.fileLastModified(storageScope, context, filepath!!) + filepath?.let { + DataAccess.fileLastModified(storageScope, context, it) + } ?: 0L } catch (e: SecurityException) { 0L } 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/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/ios/detect.py b/platform/ios/detect.py index cd303295ad..0c9b7b3204 100644 --- a/platform/ios/detect.py +++ b/platform/ios/detect.py @@ -140,7 +140,6 @@ def configure(env: "SConsEnvironment"): ) ) env.Append(ASFLAGS=["-arch", "arm64"]) - env.Append(CPPDEFINES=["NEED_LONG_INT"]) # Temp fix for ABS/MAX/MIN macros in iOS SDK blocking compilation env.Append(CCFLAGS=["-Wno-ambiguous-macro"]) diff --git a/platform/linuxbsd/freedesktop_portal_desktop.cpp b/platform/linuxbsd/freedesktop_portal_desktop.cpp index cdebed58b2..214725832f 100644 --- a/platform/linuxbsd/freedesktop_portal_desktop.cpp +++ b/platform/linuxbsd/freedesktop_portal_desktop.cpp @@ -367,6 +367,7 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo } ERR_FAIL_INDEX_V(int(p_mode), DisplayServer::FILE_DIALOG_MODE_SAVE_MAX, FAILED); + ERR_FAIL_NULL_V(monitor_connection, FAILED); Vector<String> filter_names; Vector<String> filter_exts; @@ -406,24 +407,16 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo Error rng_err = rng.get_random_bytes(uuid, 64); ERR_FAIL_COND_V_MSG(rng_err, rng_err, "Failed to generate unique token."); - fd.connection = dbus_bus_get(DBUS_BUS_SESSION, &err); - if (dbus_error_is_set(&err)) { - ERR_PRINT(vformat("Failed to open DBus connection: %s", err.message)); - dbus_error_free(&err); - unsupported = true; - return FAILED; - } - - String dbus_unique_name = String::utf8(dbus_bus_get_unique_name(fd.connection)); + String dbus_unique_name = String::utf8(dbus_bus_get_unique_name(monitor_connection)); String token = String::hex_encode_buffer(uuid, 64); String path = vformat("/org/freedesktop/portal/desktop/request/%s/%s", dbus_unique_name.replace(".", "_").replace(":", ""), token); - fd.path = vformat("type='signal',sender='org.freedesktop.portal.Desktop',path='%s',interface='org.freedesktop.portal.Request',member='Response',destination='%s'", path, dbus_unique_name); - dbus_bus_add_match(fd.connection, fd.path.utf8().get_data(), &err); + fd.path = path; + fd.filter = vformat("type='signal',sender='org.freedesktop.portal.Desktop',path='%s',interface='org.freedesktop.portal.Request',member='Response',destination='%s'", path, dbus_unique_name); + dbus_bus_add_match(monitor_connection, fd.filter.utf8().get_data(), &err); if (dbus_error_is_set(&err)) { ERR_PRINT(vformat("Failed to add DBus match: %s", err.message)); dbus_error_free(&err); - dbus_connection_unref(fd.connection); return FAILED; } @@ -460,14 +453,13 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo dbus_message_iter_close_container(&iter, &arr_iter); } - DBusMessage *reply = dbus_connection_send_with_reply_and_block(fd.connection, message, DBUS_TIMEOUT_INFINITE, &err); + DBusMessage *reply = dbus_connection_send_with_reply_and_block(monitor_connection, message, DBUS_TIMEOUT_INFINITE, &err); dbus_message_unref(message); if (!reply || dbus_error_is_set(&err)) { ERR_PRINT(vformat("Failed to send DBus message: %s", err.message)); dbus_error_free(&err); - dbus_bus_remove_match(fd.connection, fd.path.utf8().get_data(), &err); - dbus_connection_unref(fd.connection); + dbus_bus_remove_match(monitor_connection, fd.filter.utf8().get_data(), &err); return FAILED; } @@ -479,19 +471,17 @@ Error FreeDesktopPortalDesktop::file_dialog_show(DisplayServer::WindowID p_windo const char *new_path = nullptr; dbus_message_iter_get_basic(&iter, &new_path); if (String::utf8(new_path) != path) { - dbus_bus_remove_match(fd.connection, fd.path.utf8().get_data(), &err); + dbus_bus_remove_match(monitor_connection, fd.filter.utf8().get_data(), &err); if (dbus_error_is_set(&err)) { ERR_PRINT(vformat("Failed to remove DBus match: %s", err.message)); dbus_error_free(&err); - dbus_connection_unref(fd.connection); return FAILED; } - fd.path = String::utf8(new_path); - dbus_bus_add_match(fd.connection, fd.path.utf8().get_data(), &err); + fd.filter = String::utf8(new_path); + dbus_bus_add_match(monitor_connection, fd.filter.utf8().get_data(), &err); if (dbus_error_is_set(&err)) { ERR_PRINT(vformat("Failed to add DBus match: %s", err.message)); dbus_error_free(&err); - dbus_connection_unref(fd.connection); return FAILED; } } @@ -532,57 +522,9 @@ void FreeDesktopPortalDesktop::_thread_monitor(void *p_ud) { FreeDesktopPortalDesktop *portal = (FreeDesktopPortalDesktop *)p_ud; while (!portal->monitor_thread_abort.is_set()) { - { - MutexLock lock(portal->file_dialog_mutex); - for (int i = portal->file_dialogs.size() - 1; i >= 0; i--) { - bool remove = false; - { - FreeDesktopPortalDesktop::FileDialogData &fd = portal->file_dialogs.write[i]; - if (fd.connection) { - while (true) { - DBusMessage *msg = dbus_connection_pop_message(fd.connection); - if (!msg) { - break; - } else if (dbus_message_is_signal(msg, "org.freedesktop.portal.Request", "Response")) { - DBusMessageIter iter; - if (dbus_message_iter_init(msg, &iter)) { - bool cancel = false; - Vector<String> uris; - Dictionary options; - int index = 0; - file_chooser_parse_response(&iter, fd.filter_names, cancel, uris, index, options); - - if (fd.callback.is_valid()) { - callable_mp(portal, &FreeDesktopPortalDesktop::_file_dialog_callback).call_deferred(fd.callback, !cancel, uris, index, options, fd.opt_in_cb); - } - if (fd.prev_focus != DisplayServer::INVALID_WINDOW_ID) { - callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(fd.prev_focus); - } - } - dbus_message_unref(msg); - - DBusError err; - dbus_error_init(&err); - dbus_bus_remove_match(fd.connection, fd.path.utf8().get_data(), &err); - dbus_error_free(&err); - dbus_connection_unref(fd.connection); - remove = true; - break; - } - dbus_message_unref(msg); - } - dbus_connection_read_write(fd.connection, 0); - } - } - if (remove) { - portal->file_dialogs.remove_at(i); - } - } - } - - if (portal->theme_connection) { + if (portal->monitor_connection) { while (true) { - DBusMessage *msg = dbus_connection_pop_message(portal->theme_connection); + DBusMessage *msg = dbus_connection_pop_message(portal->monitor_connection); if (!msg) { break; } else if (dbus_message_is_signal(msg, "org.freedesktop.portal.Settings", "SettingChanged")) { @@ -599,12 +541,41 @@ void FreeDesktopPortalDesktop::_thread_monitor(void *p_ud) { callable_mp(portal, &FreeDesktopPortalDesktop::_system_theme_changed_callback).call_deferred(); } } - dbus_message_unref(msg); - break; + } else if (dbus_message_is_signal(msg, "org.freedesktop.portal.Request", "Response")) { + String path = String::utf8(dbus_message_get_path(msg)); + MutexLock lock(portal->file_dialog_mutex); + for (int i = 0; i < portal->file_dialogs.size(); i++) { + FreeDesktopPortalDesktop::FileDialogData &fd = portal->file_dialogs.write[i]; + if (fd.path == path) { + DBusMessageIter iter; + if (dbus_message_iter_init(msg, &iter)) { + bool cancel = false; + Vector<String> uris; + Dictionary options; + int index = 0; + file_chooser_parse_response(&iter, fd.filter_names, cancel, uris, index, options); + + if (fd.callback.is_valid()) { + callable_mp(portal, &FreeDesktopPortalDesktop::_file_dialog_callback).call_deferred(fd.callback, !cancel, uris, index, options, fd.opt_in_cb); + } + if (fd.prev_focus != DisplayServer::INVALID_WINDOW_ID) { + callable_mp(DisplayServer::get_singleton(), &DisplayServer::window_move_to_foreground).call_deferred(fd.prev_focus); + } + } + + DBusError err; + dbus_error_init(&err); + dbus_bus_remove_match(portal->monitor_connection, fd.filter.utf8().get_data(), &err); + dbus_error_free(&err); + + portal->file_dialogs.remove_at(i); + break; + } + } } dbus_message_unref(msg); } - dbus_connection_read_write(portal->theme_connection, 0); + dbus_connection_read_write(portal->monitor_connection, 0); } usleep(50000); @@ -647,18 +618,18 @@ FreeDesktopPortalDesktop::FreeDesktopPortalDesktop() { DBusError err; dbus_error_init(&err); - theme_connection = dbus_bus_get(DBUS_BUS_SESSION, &err); + monitor_connection = dbus_bus_get(DBUS_BUS_SESSION, &err); if (dbus_error_is_set(&err)) { dbus_error_free(&err); } else { theme_path = "type='signal',sender='org.freedesktop.portal.Desktop',interface='org.freedesktop.portal.Settings',member='SettingChanged'"; - dbus_bus_add_match(theme_connection, theme_path.utf8().get_data(), &err); + dbus_bus_add_match(monitor_connection, theme_path.utf8().get_data(), &err); if (dbus_error_is_set(&err)) { dbus_error_free(&err); - dbus_connection_unref(theme_connection); - theme_connection = nullptr; + dbus_connection_unref(monitor_connection); + monitor_connection = nullptr; } - dbus_connection_read_write(theme_connection, 0); + dbus_connection_read_write(monitor_connection, 0); } if (!unsupported) { @@ -673,21 +644,17 @@ FreeDesktopPortalDesktop::~FreeDesktopPortalDesktop() { monitor_thread.wait_to_finish(); } - for (FreeDesktopPortalDesktop::FileDialogData &fd : file_dialogs) { - if (fd.connection) { - DBusError err; + if (monitor_connection) { + DBusError err; + for (FreeDesktopPortalDesktop::FileDialogData &fd : file_dialogs) { dbus_error_init(&err); - dbus_bus_remove_match(fd.connection, fd.path.utf8().get_data(), &err); + dbus_bus_remove_match(monitor_connection, fd.filter.utf8().get_data(), &err); dbus_error_free(&err); - dbus_connection_unref(fd.connection); } - } - if (theme_connection) { - DBusError err; dbus_error_init(&err); - dbus_bus_remove_match(theme_connection, theme_path.utf8().get_data(), &err); + dbus_bus_remove_match(monitor_connection, theme_path.utf8().get_data(), &err); dbus_error_free(&err); - dbus_connection_unref(theme_connection); + dbus_connection_unref(monitor_connection); } } diff --git a/platform/linuxbsd/freedesktop_portal_desktop.h b/platform/linuxbsd/freedesktop_portal_desktop.h index 75afe02a26..ba9c352e62 100644 --- a/platform/linuxbsd/freedesktop_portal_desktop.h +++ b/platform/linuxbsd/freedesktop_portal_desktop.h @@ -60,9 +60,9 @@ private: struct FileDialogData { Vector<String> filter_names; - DBusConnection *connection = nullptr; DisplayServer::WindowID prev_focus = DisplayServer::INVALID_WINDOW_ID; Callable callback; + String filter; String path; bool opt_in_cb = false; }; @@ -71,8 +71,8 @@ private: Vector<FileDialogData> file_dialogs; Thread monitor_thread; SafeFlag monitor_thread_abort; + DBusConnection *monitor_connection = nullptr; - DBusConnection *theme_connection = nullptr; String theme_path; Callable system_theme_changed; void _system_theme_changed_callback(); diff --git a/platform/macos/display_server_macos.mm b/platform/macos/display_server_macos.mm index d06eab8531..b7a9fb1bbd 100644 --- a/platform/macos/display_server_macos.mm +++ b/platform/macos/display_server_macos.mm @@ -1995,7 +1995,7 @@ void DisplayServerMacOS::window_set_position(const Point2i &p_position, WindowID ERR_FAIL_COND(!windows.has(p_window)); WindowData &wd = windows[p_window]; - if (NSEqualRects([wd.window_object frame], [[wd.window_object screen] visibleFrame])) { + if (wd.fullscreen) { return; } @@ -2084,12 +2084,21 @@ Size2i DisplayServerMacOS::window_get_max_size(WindowID p_window) const { } void DisplayServerMacOS::update_presentation_mode() { + bool has_fs_windows = false; for (const KeyValue<WindowID, WindowData> &wd : windows) { - if (wd.value.fullscreen && wd.value.exclusive_fullscreen) { - return; + if (wd.value.fullscreen) { + if (wd.value.exclusive_fullscreen) { + return; + } else { + has_fs_windows = true; + } } } - [NSApp setPresentationOptions:NSApplicationPresentationDefault]; + if (has_fs_windows) { + [NSApp setPresentationOptions:NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock | NSApplicationPresentationFullScreen]; + } else { + [NSApp setPresentationOptions:NSApplicationPresentationDefault]; + } } void DisplayServerMacOS::window_set_min_size(const Size2i p_size, WindowID p_window) { |