diff options
Diffstat (limited to 'platform/android/java/lib/src')
33 files changed, 1573 insertions, 558 deletions
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 290be727ab..49e8ffb008 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt @@ -39,8 +39,6 @@ import android.content.res.Configuration import android.content.res.Resources import android.graphics.Color import android.hardware.Sensor -import android.hardware.SensorEvent -import android.hardware.SensorEventListener import android.hardware.SensorManager import android.os.* import android.util.Log @@ -52,7 +50,9 @@ 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.error.Error import org.godotengine.godot.input.GodotEditText +import org.godotengine.godot.input.GodotInputHandler import org.godotengine.godot.io.directory.DirectoryAccessHandler import org.godotengine.godot.io.file.FileAccessHandler import org.godotengine.godot.plugin.GodotPluginRegistry @@ -73,6 +73,8 @@ import java.io.InputStream import java.lang.Exception import java.security.MessageDigest import java.util.* +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference /** * Core component used to interface with the native layer of the engine. @@ -80,36 +82,48 @@ import java.util.* * Can be hosted by [Activity], [Fragment] or [Service] android components, so long as its * lifecycle methods are properly invoked. */ -class Godot(private val context: Context) : SensorEventListener { +class Godot(private val context: Context) { - private companion object { + internal companion object { private val TAG = Godot::class.java.simpleName - } - private val windowManager: WindowManager by lazy { - requireActivity().getSystemService(Context.WINDOW_SERVICE) as WindowManager + // Supported build flavors + const val EDITOR_FLAVOR = "editor" + const val TEMPLATE_FLAVOR = "template" + + /** + * @return true if this is an editor build, false if this is a template build + */ + fun isEditorBuild() = BuildConfig.FLAVOR == EDITOR_FLAVOR } + + private val mSensorManager: SensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private val mClipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + private val vibratorService: Vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + private val pluginRegistry: GodotPluginRegistry by lazy { GodotPluginRegistry.getPluginRegistry() } - private val mSensorManager: SensorManager by lazy { - requireActivity().getSystemService(Context.SENSOR_SERVICE) as SensorManager - } + + private val accelerometer_enabled = AtomicBoolean(false) private val mAccelerometer: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) } + + private val gravity_enabled = AtomicBoolean(false) private val mGravity: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY) } + + private val magnetometer_enabled = AtomicBoolean(false) private val mMagnetometer: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) } + + private val gyroscope_enabled = AtomicBoolean(false) private val mGyroscope: Sensor? by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) } - private val mClipboard: ClipboardManager by lazy { - requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - } private val uiChangeListener = View.OnSystemUiVisibilityChangeListener { visibility: Int -> if (visibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0) { @@ -126,6 +140,12 @@ class Godot(private val context: Context) : SensorEventListener { val fileAccessHandler = FileAccessHandler(context) val netUtils = GodotNetUtils(context) private val commandLineFileParser = CommandLineFileParser() + private val godotInputHandler = GodotInputHandler(context, this) + + /** + * Task to run when the engine terminates. + */ + private val runOnTerminate = AtomicReference<Runnable>() /** * Tracks whether [onCreate] was completed successfully. @@ -148,6 +168,17 @@ class Godot(private val context: Context) : SensorEventListener { private var renderViewInitialized = false private var primaryHost: GodotHost? = null + /** + * Tracks whether we're in the RESUMED lifecycle state. + * See [onResume] and [onPause] + */ + private var resumed = false + + /** + * Tracks whether [onGodotSetupCompleted] fired. + */ + private val godotMainLoopStarted = AtomicBoolean(false) + var io: GodotIO? = null private var commandLine : MutableList<String> = ArrayList<String>() @@ -192,6 +223,8 @@ class Godot(private val context: Context) : SensorEventListener { return } + Log.v(TAG, "OnCreate: $primaryHost") + darkMode = context.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES beginBenchmarkMeasure("Startup", "Godot::onCreate") @@ -200,6 +233,8 @@ class Godot(private val context: Context) : SensorEventListener { val activity = requireActivity() val window = activity.window window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) + + Log.v(TAG, "Initializing Godot plugin registry") GodotPluginRegistry.initializePluginRegistry(this, primaryHost.getHostPlugins(this)) if (io == null) { io = GodotIO(activity) @@ -323,13 +358,17 @@ class Godot(private val context: Context) : SensorEventListener { return false } - if (expansionPackPath.isNotEmpty()) { - commandLine.add("--main-pack") - commandLine.add(expansionPackPath) - } - val activity = requireActivity() - if (!nativeLayerInitializeCompleted) { - nativeLayerInitializeCompleted = GodotLib.initialize( + Log.v(TAG, "OnInitNativeLayer: $host") + + beginBenchmarkMeasure("Startup", "Godot::onInitNativeLayer") + try { + if (expansionPackPath.isNotEmpty()) { + commandLine.add("--main-pack") + commandLine.add(expansionPackPath) + } + val activity = requireActivity() + if (!nativeLayerInitializeCompleted) { + nativeLayerInitializeCompleted = GodotLib.initialize( activity, this, activity.assets, @@ -338,15 +377,20 @@ class Godot(private val context: Context) : SensorEventListener { directoryAccessHandler, fileAccessHandler, useApkExpansion, - ) - } + ) + Log.v(TAG, "Godot native layer initialization completed: $nativeLayerInitializeCompleted") + } - if (nativeLayerInitializeCompleted && !nativeLayerSetupCompleted) { - nativeLayerSetupCompleted = GodotLib.setup(commandLine.toTypedArray(), tts) - if (!nativeLayerSetupCompleted) { - Log.e(TAG, "Unable to setup the Godot engine! Aborting...") - alert(R.string.error_engine_setup_message, R.string.text_error_title, this::forceQuit) + if (nativeLayerInitializeCompleted && !nativeLayerSetupCompleted) { + nativeLayerSetupCompleted = GodotLib.setup(commandLine.toTypedArray(), tts) + if (!nativeLayerSetupCompleted) { + throw IllegalStateException("Unable to setup the Godot engine! Aborting...") + } else { + Log.v(TAG, "Godot native layer setup completed") + } } + } finally { + endBenchmarkMeasure("Startup", "Godot::onInitNativeLayer") } return isNativeInitialized() } @@ -370,6 +414,9 @@ class Godot(private val context: Context) : SensorEventListener { throw IllegalStateException("onInitNativeLayer() must be invoked successfully prior to initializing the render view") } + Log.v(TAG, "OnInitRenderView: $host") + + beginBenchmarkMeasure("Startup", "Godot::onInitRenderView") try { val activity: Activity = host.activity containerLayout = providedContainerLayout @@ -392,13 +439,12 @@ class Godot(private val context: Context) : SensorEventListener { containerLayout?.addView(editText) renderView = if (usesVulkan()) { if (!meetsVulkanRequirements(activity.packageManager)) { - alert(R.string.error_missing_vulkan_requirements_message, R.string.text_error_title, this::forceQuit) - return null + throw IllegalStateException(activity.getString(R.string.error_missing_vulkan_requirements_message)) } - GodotVulkanRenderView(host, this) + GodotVulkanRenderView(host, this, godotInputHandler) } else { // Fallback to openGl - GodotGLRenderView(host, this, xrMode, useDebugOpengl) + GodotGLRenderView(host, this, godotInputHandler, xrMode, useDebugOpengl) } if (host == primaryHost) { @@ -482,11 +528,14 @@ class Godot(private val context: Context) : SensorEventListener { containerLayout?.removeAllViews() containerLayout = null } + + endBenchmarkMeasure("Startup", "Godot::onInitRenderView") } return containerLayout } fun onStart(host: GodotHost) { + Log.v(TAG, "OnStart: $host") if (host != primaryHost) { return } @@ -495,23 +544,14 @@ class Godot(private val context: Context) : SensorEventListener { } fun onResume(host: GodotHost) { + Log.v(TAG, "OnResume: $host") + resumed = true if (host != primaryHost) { return } renderView?.onActivityResumed() - if (mAccelerometer != null) { - mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME) - } - if (mGravity != null) { - mSensorManager.registerListener(this, mGravity, SensorManager.SENSOR_DELAY_GAME) - } - if (mMagnetometer != null) { - mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME) - } - if (mGyroscope != null) { - mSensorManager.registerListener(this, mGyroscope, SensorManager.SENSOR_DELAY_GAME) - } + registerSensorsIfNeeded() if (useImmersive) { val window = requireActivity().window window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or @@ -526,19 +566,41 @@ class Godot(private val context: Context) : SensorEventListener { } } + private fun registerSensorsIfNeeded() { + if (!resumed || !godotMainLoopStarted.get()) { + return + } + + if (accelerometer_enabled.get() && mAccelerometer != null) { + mSensorManager.registerListener(godotInputHandler, mAccelerometer, SensorManager.SENSOR_DELAY_GAME) + } + if (gravity_enabled.get() && mGravity != null) { + mSensorManager.registerListener(godotInputHandler, mGravity, SensorManager.SENSOR_DELAY_GAME) + } + if (magnetometer_enabled.get() && mMagnetometer != null) { + mSensorManager.registerListener(godotInputHandler, mMagnetometer, SensorManager.SENSOR_DELAY_GAME) + } + if (gyroscope_enabled.get() && mGyroscope != null) { + mSensorManager.registerListener(godotInputHandler, mGyroscope, SensorManager.SENSOR_DELAY_GAME) + } + } + fun onPause(host: GodotHost) { + Log.v(TAG, "OnPause: $host") + resumed = false if (host != primaryHost) { return } renderView?.onActivityPaused() - mSensorManager.unregisterListener(this) + mSensorManager.unregisterListener(godotInputHandler) for (plugin in pluginRegistry.allPlugins) { plugin.onMainPause() } } fun onStop(host: GodotHost) { + Log.v(TAG, "OnStop: $host") if (host != primaryHost) { return } @@ -547,6 +609,7 @@ class Godot(private val context: Context) : SensorEventListener { } fun onDestroy(primaryHost: GodotHost) { + Log.v(TAG, "OnDestroy: $primaryHost") if (this.primaryHost != primaryHost) { return } @@ -555,10 +618,7 @@ class Godot(private val context: Context) : SensorEventListener { plugin.onMainDestroy() } - runOnRenderThread { - GodotLib.ondestroy() - forceQuit() - } + renderView?.onActivityDestroyed() } /** @@ -604,18 +664,22 @@ class Godot(private val context: Context) : SensorEventListener { * Invoked on the render thread when the Godot setup is complete. */ private fun onGodotSetupCompleted() { - Log.d(TAG, "OnGodotSetupCompleted") + Log.v(TAG, "OnGodotSetupCompleted") // 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 rotaryInputAxisValue = GodotLib.getGlobal("input_devices/pointing/android/rotary_input_scroll_axis") runOnUiThread { renderView?.inputHandler?.apply { enableLongPress(longPressEnabled) enablePanningAndScalingGestures(panScaleEnabled) - setRotaryInputAxis(rotaryInputAxis) + try { + setRotaryInputAxis(Integer.parseInt(rotaryInputAxisValue)) + } catch (e: NumberFormatException) { + Log.w(TAG, e) + } } } @@ -629,7 +693,17 @@ class Godot(private val context: Context) : SensorEventListener { * Invoked on the render thread when the Godot main loop has started. */ private fun onGodotMainLoopStarted() { - Log.d(TAG, "OnGodotMainLoopStarted") + Log.v(TAG, "OnGodotMainLoopStarted") + godotMainLoopStarted.set(true) + + accelerometer_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_accelerometer"))) + gravity_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_gravity"))) + gyroscope_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_gyroscope"))) + magnetometer_enabled.set(java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/sensors/enable_magnetometer"))) + + runOnUiThread { + registerSensorsIfNeeded() + } for (plugin in pluginRegistry.allPlugins) { plugin.onGodotMainLoopStarted() @@ -637,6 +711,15 @@ class Godot(private val context: Context) : SensorEventListener { primaryHost?.onGodotMainLoopStarted() } + /** + * Invoked on the render thread when the engine is about to terminate. + */ + @Keep + private fun onGodotTerminating() { + Log.v(TAG, "OnGodotTerminating") + runOnTerminate.get()?.run() + } + private fun restart() { primaryHost?.onGodotRestartRequested(this) } @@ -646,12 +729,7 @@ class Godot(private val context: Context) : SensorEventListener { decorView.setOnSystemUiVisibilityChangeListener(uiChangeListener) } - @Keep - private fun alert(message: String, title: String) { - alert(message, title, null) - } - - private fun alert( + fun alert( @StringRes messageResId: Int, @StringRes titleResId: Int, okCallback: Runnable? @@ -660,7 +738,9 @@ class Godot(private val context: Context) : SensorEventListener { alert(res.getString(messageResId), res.getString(titleResId), okCallback) } - private fun alert(message: String, title: String, okCallback: Runnable?) { + @JvmOverloads + @Keep + fun alert(message: String, title: String, okCallback: Runnable? = null) { val activity: Activity = getActivity() ?: return runOnUiThread { val builder = AlertDialog.Builder(activity) @@ -770,8 +850,28 @@ class Godot(private val context: Context) : SensorEventListener { mClipboard.setPrimaryClip(clip) } - private fun forceQuit() { - forceQuit(0) + /** + * Destroys the Godot Engine and kill the process it's running in. + */ + @JvmOverloads + fun destroyAndKillProcess(destroyRunnable: Runnable? = null) { + val host = primaryHost + val activity = host?.activity + if (host == null || activity == null) { + // Run the destroyRunnable right away as we are about to force quit. + destroyRunnable?.run() + + // Fallback to force quit + forceQuit(0) + return + } + + // Store the destroyRunnable so it can be run when the engine is terminating + runOnTerminate.set(destroyRunnable) + + runOnUiThread { + onDestroy(host) + } } @Keep @@ -786,11 +886,7 @@ class Godot(private val context: Context) : SensorEventListener { } ?: return false } - fun onBackPressed(host: GodotHost) { - if (host != primaryHost) { - return - } - + fun onBackPressed() { var shouldQuit = true for (plugin in pluginRegistry.allPlugins) { if (plugin.onMainBackPressed()) { @@ -802,77 +898,6 @@ class Godot(private val context: Context) : SensorEventListener { } } - private fun getRotatedValues(values: FloatArray?): FloatArray? { - if (values == null || values.size != 3) { - return null - } - val rotatedValues = FloatArray(3) - when (windowManager.defaultDisplay.rotation) { - Surface.ROTATION_0 -> { - rotatedValues[0] = values[0] - rotatedValues[1] = values[1] - rotatedValues[2] = values[2] - } - Surface.ROTATION_90 -> { - rotatedValues[0] = -values[1] - rotatedValues[1] = values[0] - rotatedValues[2] = values[2] - } - Surface.ROTATION_180 -> { - rotatedValues[0] = -values[0] - rotatedValues[1] = -values[1] - rotatedValues[2] = values[2] - } - Surface.ROTATION_270 -> { - rotatedValues[0] = values[1] - rotatedValues[1] = -values[0] - rotatedValues[2] = values[2] - } - } - return rotatedValues - } - - override fun onSensorChanged(event: SensorEvent) { - if (renderView == null) { - return - } - - val rotatedValues = getRotatedValues(event.values) - - when (event.sensor.type) { - Sensor.TYPE_ACCELEROMETER -> { - rotatedValues?.let { - renderView?.queueOnRenderThread { - GodotLib.accelerometer(-it[0], -it[1], -it[2]) - } - } - } - Sensor.TYPE_GRAVITY -> { - rotatedValues?.let { - renderView?.queueOnRenderThread { - GodotLib.gravity(-it[0], -it[1], -it[2]) - } - } - } - Sensor.TYPE_MAGNETIC_FIELD -> { - rotatedValues?.let { - renderView?.queueOnRenderThread { - GodotLib.magnetometer(-it[0], -it[1], -it[2]) - } - } - } - Sensor.TYPE_GYROSCOPE -> { - rotatedValues?.let { - renderView?.queueOnRenderThread { - GodotLib.gyroscope(it[0], it[1], it[2]) - } - } - } - } - } - - override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} - /** * Used by the native code (java_godot_wrapper.h) to vibrate the device. * @param durationMs @@ -881,7 +906,6 @@ class Godot(private val context: Context) : SensorEventListener { @Keep 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) { if (amplitude <= -1) { vibratorService.vibrate( @@ -1008,7 +1032,7 @@ class Godot(private val context: Context) : SensorEventListener { @Keep private fun initInputDevices() { - renderView?.initInputDevices() + godotInputHandler.initInputDevices() } @Keep @@ -1030,4 +1054,20 @@ class Godot(private val context: Context) : SensorEventListener { private fun nativeDumpBenchmark(benchmarkFile: String) { dumpBenchmark(fileAccessHandler, benchmarkFile) } + + @Keep + private fun nativeSignApk(inputPath: String, + outputPath: String, + keystorePath: String, + keystoreUser: String, + keystorePassword: String): Int { + val signResult = primaryHost?.signApk(inputPath, outputPath, keystorePath, keystoreUser, keystorePassword) ?: Error.ERR_UNAVAILABLE + return signResult.toNativeValue() + } + + @Keep + private fun nativeVerifyApk(apkPath: String): Int { + val verifyResult = primaryHost?.verifyApk(apkPath) ?: Error.ERR_UNAVAILABLE + return verifyResult.toNativeValue() + } } 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 4c5e857b7a..474c6e9b2f 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt @@ -53,8 +53,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { private val TAG = GodotActivity::class.java.simpleName @JvmStatic - protected val EXTRA_FORCE_QUIT = "force_quit_requested" - @JvmStatic protected val EXTRA_NEW_LAUNCH = "new_launch_requested" } @@ -85,12 +83,8 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { protected open fun getGodotAppLayout() = R.layout.godot_app_layout override fun onDestroy() { - Log.v(TAG, "Destroying Godot app...") + Log.v(TAG, "Destroying GodotActivity $this...") super.onDestroy() - - godotFragment?.let { - terminateGodotInstance(it.godot) - } } override fun onGodotForceQuit(instance: Godot) { @@ -132,12 +126,6 @@ abstract class GodotActivity : FragmentActivity(), GodotHost { } private fun handleStartIntent(intent: Intent, newLaunch: Boolean) { - val forceQuitRequested = intent.getBooleanExtra(EXTRA_FORCE_QUIT, false) - if (forceQuitRequested) { - Log.d(TAG, "Force quit requested, terminating..") - ProcessPhoenix.forceQuit(this) - return - } if (!newLaunch) { val newLaunchRequested = intent.getBooleanExtra(EXTRA_NEW_LAUNCH, false) if (newLaunchRequested) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java index a323045e1b..e0f5744368 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java @@ -30,6 +30,7 @@ package org.godotengine.godot; +import org.godotengine.godot.error.Error; import org.godotengine.godot.plugin.GodotPlugin; import org.godotengine.godot.utils.BenchmarkUtils; @@ -42,6 +43,7 @@ import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.os.Messenger; +import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -186,7 +188,12 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH final Activity activity = getActivity(); mCurrentIntent = activity.getIntent(); - godot = new Godot(requireContext()); + if (parentHost != null) { + godot = parentHost.getGodot(); + } + if (godot == null) { + godot = new Godot(requireContext()); + } performEngineInitialization(); BenchmarkUtils.endBenchmarkMeasure("Startup", "GodotFragment::onCreate"); } @@ -203,6 +210,12 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH if (godotContainerLayout == null) { throw new IllegalStateException("Unable to initialize engine render view"); } + } catch (IllegalStateException e) { + Log.e(TAG, "Engine initialization failed", e); + final String errorMessage = TextUtils.isEmpty(e.getMessage()) + ? getString(R.string.error_engine_setup_message) + : e.getMessage(); + godot.alert(errorMessage, getString(R.string.text_error_title), godot::destroyAndKillProcess); } catch (IllegalArgumentException ignored) { final Activity activity = getActivity(); Intent notifierIntent = new Intent(activity, activity.getClass()); @@ -318,7 +331,7 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH } public void onBackPressed() { - godot.onBackPressed(this); + godot.onBackPressed(); } /** @@ -472,4 +485,20 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH } return Collections.emptySet(); } + + @Override + public Error signApk(@NonNull String inputPath, @NonNull String outputPath, @NonNull String keystorePath, @NonNull String keystoreUser, @NonNull String keystorePassword) { + if (parentHost != null) { + return parentHost.signApk(inputPath, outputPath, keystorePath, keystoreUser, keystorePassword); + } + return Error.ERR_UNAVAILABLE; + } + + @Override + public Error verifyApk(@NonNull String apkPath) { + if (parentHost != null) { + return parentHost.verifyApk(apkPath); + } + return Error.ERR_UNAVAILABLE; + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java index 81043ce782..15a811ce83 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java @@ -42,7 +42,6 @@ import org.godotengine.godot.xr.regular.RegularContextFactory; import org.godotengine.godot.xr.regular.RegularFallbackConfigChooser; import android.annotation.SuppressLint; -import android.content.Context; import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -77,19 +76,19 @@ import java.io.InputStream; * that matches it exactly (with regards to red/green/blue/alpha channels * bit depths). Failure to do so would result in an EGL_BAD_MATCH error. */ -public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView { +class GodotGLRenderView extends GLSurfaceView implements GodotRenderView { private final GodotHost host; private final Godot godot; private final GodotInputHandler inputHandler; private final GodotRenderer godotRenderer; private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>(); - public GodotGLRenderView(GodotHost host, Godot godot, XRMode xrMode, boolean useDebugOpengl) { + public GodotGLRenderView(GodotHost host, Godot godot, GodotInputHandler inputHandler, XRMode xrMode, boolean useDebugOpengl) { super(host.getActivity()); this.host = host; this.godot = godot; - this.inputHandler = new GodotInputHandler(this); + this.inputHandler = inputHandler; this.godotRenderer = new GodotRenderer(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); @@ -103,11 +102,6 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView } @Override - public void initInputDevices() { - this.inputHandler.initInputDevices(); - } - - @Override public void queueOnRenderThread(Runnable event) { queueEvent(event); } @@ -141,8 +135,8 @@ public class GodotGLRenderView extends GLSurfaceView implements GodotRenderView } @Override - public void onBackPressed() { - godot.onBackPressed(host); + public void onActivityDestroyed() { + requestRenderThreadExitAndWait(); } @Override diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java index 1862b9fa9b..f1c84e90a7 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java @@ -30,10 +30,13 @@ package org.godotengine.godot; +import org.godotengine.godot.error.Error; import org.godotengine.godot.plugin.GodotPlugin; import android.app.Activity; +import androidx.annotation.NonNull; + import java.util.Collections; import java.util.List; import java.util.Set; @@ -108,4 +111,29 @@ public interface GodotHost { default Set<GodotPlugin> getHostPlugins(Godot engine) { return Collections.emptySet(); } + + /** + * Signs the given Android apk + * + * @param inputPath Path to the apk that should be signed + * @param outputPath Path for the signed output apk; can be the same as inputPath + * @param keystorePath Path to the keystore to use for signing the apk + * @param keystoreUser Keystore user credential + * @param keystorePassword Keystore password credential + * + * @return {@link Error#OK} if signing is successful + */ + default Error signApk(@NonNull String inputPath, @NonNull String outputPath, @NonNull String keystorePath, @NonNull String keystoreUser, @NonNull String keystorePassword) { + return Error.ERR_UNAVAILABLE; + } + + /** + * Verifies the given Android apk is signed + * + * @param apkPath Path to the apk that should be verified + * @return {@link Error#OK} if verification was successful + */ + default Error verifyApk(@NonNull String apkPath) { + return Error.ERR_UNAVAILABLE; + } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java index 4b51bd778d..219631284a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -121,7 +121,7 @@ public class GodotIO { activity.startActivity(intent); return 0; - } catch (ActivityNotFoundException e) { + } catch (Exception e) { Log.e(TAG, "Unable to open uri " + uriString, e); return 1; } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java index d0c3d4a687..295a4a6340 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -240,4 +240,15 @@ public class GodotLib { * @see GodotRenderer#onActivityPaused() */ public static native void onRendererPaused(); + + /** + * @return true if input must be dispatched from the render thread. If false, input is + * dispatched from the UI thread. + */ + public static native boolean shouldDispatchInputToRenderThread(); + + /** + * @return the project resource directory + */ + public static native String getProjectResourceDir(); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java index 5b2f9f57c7..30821eaa8e 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java @@ -37,13 +37,14 @@ import android.view.SurfaceView; public interface GodotRenderView { SurfaceView getView(); - void initInputDevices(); - /** * Starts the thread that will drive Godot's rendering. */ void startRenderer(); + /** + * Queues a runnable to be run on the rendering thread. + */ void queueOnRenderThread(Runnable event); void onActivityPaused(); @@ -54,7 +55,7 @@ public interface GodotRenderView { void onActivityStarted(); - void onBackPressed(); + void onActivityDestroyed(); GodotInputHandler getInputHandler(); diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java index a1ee9bd6b4..d5b05913d8 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java @@ -50,19 +50,19 @@ import androidx.annotation.Keep; import java.io.InputStream; -public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView { +class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderView { private final GodotHost host; private final Godot godot; private final GodotInputHandler mInputHandler; private final VkRenderer mRenderer; private final SparseArray<PointerIcon> customPointerIcons = new SparseArray<>(); - public GodotVulkanRenderView(GodotHost host, Godot godot) { + public GodotVulkanRenderView(GodotHost host, Godot godot, GodotInputHandler inputHandler) { super(host.getActivity()); this.host = host; this.godot = godot; - mInputHandler = new GodotInputHandler(this); + mInputHandler = inputHandler; mRenderer = new VkRenderer(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_DEFAULT)); @@ -81,11 +81,6 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV } @Override - public void initInputDevices() { - mInputHandler.initInputDevices(); - } - - @Override public void queueOnRenderThread(Runnable event) { queueOnVkThread(event); } @@ -119,8 +114,8 @@ public class GodotVulkanRenderView extends VkSurfaceView implements GodotRenderV } @Override - public void onBackPressed() { - godot.onBackPressed(host); + public void onActivityDestroyed() { + requestRenderThreadExitAndWait(); } @Override diff --git a/platform/android/java/lib/src/org/godotengine/godot/error/Error.kt b/platform/android/java/lib/src/org/godotengine/godot/error/Error.kt new file mode 100644 index 0000000000..00ef5ee341 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/error/Error.kt @@ -0,0 +1,100 @@ +/**************************************************************************/ +/* Error.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.error + +/** + * Godot error list. + * + * This enum MUST match its native counterpart in 'core/error/error_list.h' + */ +enum class Error(private val description: String) { + OK("OK"), // (0) + FAILED("Failed"), ///< Generic fail error + ERR_UNAVAILABLE("Unavailable"), ///< What is requested is unsupported/unavailable + ERR_UNCONFIGURED("Unconfigured"), ///< The object being used hasn't been properly set up yet + ERR_UNAUTHORIZED("Unauthorized"), ///< Missing credentials for requested resource + ERR_PARAMETER_RANGE_ERROR("Parameter out of range"), ///< Parameter given out of range (5) + ERR_OUT_OF_MEMORY("Out of memory"), ///< Out of memory + ERR_FILE_NOT_FOUND("File not found"), + ERR_FILE_BAD_DRIVE("File: Bad drive"), + ERR_FILE_BAD_PATH("File: Bad path"), + ERR_FILE_NO_PERMISSION("File: Permission denied"), // (10) + ERR_FILE_ALREADY_IN_USE("File already in use"), + ERR_FILE_CANT_OPEN("Can't open file"), + ERR_FILE_CANT_WRITE("Can't write file"), + ERR_FILE_CANT_READ("Can't read file"), + ERR_FILE_UNRECOGNIZED("File unrecognized"), // (15) + ERR_FILE_CORRUPT("File corrupt"), + ERR_FILE_MISSING_DEPENDENCIES("Missing dependencies for file"), + ERR_FILE_EOF("End of file"), + ERR_CANT_OPEN("Can't open"), ///< Can't open a resource/socket/file + ERR_CANT_CREATE("Can't create"), // (20) + ERR_QUERY_FAILED("Query failed"), + ERR_ALREADY_IN_USE("Already in use"), + ERR_LOCKED("Locked"), ///< resource is locked + ERR_TIMEOUT("Timeout"), + ERR_CANT_CONNECT("Can't connect"), // (25) + ERR_CANT_RESOLVE("Can't resolve"), + ERR_CONNECTION_ERROR("Connection error"), + ERR_CANT_ACQUIRE_RESOURCE("Can't acquire resource"), + ERR_CANT_FORK("Can't fork"), + ERR_INVALID_DATA("Invalid data"), ///< Data passed is invalid (30) + ERR_INVALID_PARAMETER("Invalid parameter"), ///< Parameter passed is invalid + ERR_ALREADY_EXISTS("Already exists"), ///< When adding, item already exists + ERR_DOES_NOT_EXIST("Does not exist"), ///< When retrieving/erasing, if item does not exist + ERR_DATABASE_CANT_READ("Can't read database"), ///< database is full + ERR_DATABASE_CANT_WRITE("Can't write database"), ///< database is full (35) + ERR_COMPILATION_FAILED("Compilation failed"), + ERR_METHOD_NOT_FOUND("Method not found"), + ERR_LINK_FAILED("Link failed"), + ERR_SCRIPT_FAILED("Script failed"), + ERR_CYCLIC_LINK("Cyclic link detected"), // (40) + ERR_INVALID_DECLARATION("Invalid declaration"), + ERR_DUPLICATE_SYMBOL("Duplicate symbol"), + ERR_PARSE_ERROR("Parse error"), + ERR_BUSY("Busy"), + ERR_SKIP("Skip"), // (45) + ERR_HELP("Help"), ///< user requested help!! + ERR_BUG("Bug"), ///< a bug in the software certainly happened, due to a double check failing or unexpected behavior. + ERR_PRINTER_ON_FIRE("Printer on fire"); /// the parallel port printer is engulfed in flames + + companion object { + internal fun fromNativeValue(nativeValue: Int): Error? { + return Error.entries.getOrNull(nativeValue) + } + } + + internal fun toNativeValue(): Int = this.ordinal + + override fun toString(): String { + return description + } +} 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 c316812404..6a4e9da699 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 @@ -595,6 +595,15 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback protected final void resumeGLThread() { mGLThread.onResume(); } + + /** + * Requests the render thread to exit and block until it does. + */ + protected final void requestRenderThreadExitAndWait() { + if (mGLThread != null) { + mGLThread.requestExitAndWait(); + } + } // -- GODOT end -- /** @@ -783,6 +792,11 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback * @return true if the buffers should be swapped, false otherwise. */ boolean onDrawFrame(GL10 gl); + + /** + * Invoked when the render thread is in the process of shutting down. + */ + void onRenderThreadExiting(); // -- GODOT end -- } @@ -1621,6 +1635,12 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback * clean-up everything... */ synchronized (sGLThreadManager) { + Log.d("GLThread", "Exiting render thread"); + GLSurfaceView view = mGLSurfaceViewWeakRef.get(); + if (view != null) { + view.mRenderer.onRenderThreadExiting(); + } + stopEglSurfaceLocked(); stopEglContextLocked(); } @@ -1704,15 +1724,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback mHasSurface = true; mFinishedCreatingEglSurface = false; sGLThreadManager.notifyAll(); - while (mWaitingForSurface - && !mFinishedCreatingEglSurface - && !mExited) { - try { - sGLThreadManager.wait(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } } } @@ -1723,13 +1734,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback } mHasSurface = false; sGLThreadManager.notifyAll(); - while((!mWaitingForSurface) && (!mExited)) { - try { - sGLThreadManager.wait(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } } } @@ -1740,16 +1744,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback } mRequestPaused = true; sGLThreadManager.notifyAll(); - while ((! mExited) && (! mPaused)) { - if (LOG_PAUSE_RESUME) { - Log.i("Main thread", "onPause waiting for mPaused."); - } - try { - sGLThreadManager.wait(); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } } } @@ -1762,16 +1756,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback mRequestRender = true; mRenderComplete = false; sGLThreadManager.notifyAll(); - while ((! mExited) && mPaused && (!mRenderComplete)) { - if (LOG_PAUSE_RESUME) { - Log.i("Main thread", "onResume waiting for !mPaused."); - } - try { - sGLThreadManager.wait(); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } } } @@ -1793,19 +1777,6 @@ public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback } sGLThreadManager.notifyAll(); - - // Wait for thread to react to resize and render a frame - while (! mExited && !mPaused && !mRenderComplete - && ableToDraw()) { - if (LOG_SURFACE) { - Log.i("Main thread", "onWindowResize waiting for render complete from tid=" + getId()); - } - try { - sGLThreadManager.wait(); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java b/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java index 9d44d8826c..7e5e262b2d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java +++ b/platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java @@ -34,6 +34,8 @@ import org.godotengine.godot.GodotLib; import org.godotengine.godot.plugin.GodotPlugin; import org.godotengine.godot.plugin.GodotPluginRegistry; +import android.util.Log; + import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; @@ -41,6 +43,8 @@ import javax.microedition.khronos.opengles.GL10; * Godot's GL renderer implementation. */ public class GodotRenderer implements GLSurfaceView.Renderer { + private final String TAG = GodotRenderer.class.getSimpleName(); + private final GodotPluginRegistry pluginRegistry; private boolean activityJustResumed = false; @@ -62,6 +66,12 @@ public class GodotRenderer implements GLSurfaceView.Renderer { return swapBuffers; } + @Override + public void onRenderThreadExiting() { + Log.d(TAG, "Destroying Godot Engine"); + GodotLib.ondestroy(); + } + public void onSurfaceChanged(GL10 gl, int width, int height) { GodotLib.resize(null, width, height); for (GodotPlugin plugin : pluginRegistry.getAllPlugins()) { 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 49b34a5229..2929a0a0b0 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 @@ -44,7 +44,7 @@ import org.godotengine.godot.GodotLib * @See https://developer.android.com/reference/android/view/GestureDetector.SimpleOnGestureListener * @See https://developer.android.com/reference/android/view/ScaleGestureDetector.OnScaleGestureListener */ -internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureListener { +internal class GodotGestureHandler(private val inputHandler: GodotInputHandler) : SimpleOnGestureListener(), OnScaleGestureListener { companion object { private val TAG = GodotGestureHandler::class.java.simpleName @@ -65,18 +65,21 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi private var lastDragY: Float = 0.0f override fun onDown(event: MotionEvent): Boolean { - GodotInputHandler.handleMotionEvent(event, MotionEvent.ACTION_DOWN, nextDownIsDoubleTap) + inputHandler.handleMotionEvent(event, MotionEvent.ACTION_DOWN, nextDownIsDoubleTap) nextDownIsDoubleTap = false return true } override fun onSingleTapUp(event: MotionEvent): Boolean { - GodotInputHandler.handleMotionEvent(event) + inputHandler.handleMotionEvent(event) return true } override fun onLongPress(event: MotionEvent) { - contextClickRouter(event) + val toolType = GodotInputHandler.getEventToolType(event) + if (toolType != MotionEvent.TOOL_TYPE_MOUSE) { + contextClickRouter(event) + } } private fun contextClickRouter(event: MotionEvent) { @@ -85,10 +88,10 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi } // Cancel the previous down event - GodotInputHandler.handleMotionEvent(event, MotionEvent.ACTION_CANCEL) + inputHandler.handleMotionEvent(event, MotionEvent.ACTION_CANCEL) // Turn a context click into a single tap right mouse button click. - GodotInputHandler.handleMouseEvent( + inputHandler.handleMouseEvent( event, MotionEvent.ACTION_DOWN, MotionEvent.BUTTON_SECONDARY, @@ -104,7 +107,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, true) + inputHandler.handleMouseEvent(MotionEvent.ACTION_UP, true) } pointerCaptureInProgress = hasCapture } @@ -131,9 +134,9 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi 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(event, MotionEvent.ACTION_UP) + inputHandler.handleMouseEvent(event, MotionEvent.ACTION_UP) } else { - GodotInputHandler.handleTouchEvent(event) + inputHandler.handleTouchEvent(event) } pointerCaptureInProgress = false dragInProgress = false @@ -148,7 +151,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi private fun onActionMove(event: MotionEvent): Boolean { if (contextClickInProgress) { - GodotInputHandler.handleMouseEvent(event, event.actionMasked, MotionEvent.BUTTON_SECONDARY, false) + inputHandler.handleMouseEvent(event, event.actionMasked, MotionEvent.BUTTON_SECONDARY, false) return true } else if (!scaleInProgress) { // The 'onScroll' event is triggered with a long delay. @@ -158,7 +161,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi if (lastDragX != event.getX(0) || lastDragY != event.getY(0)) { lastDragX = event.getX(0) lastDragY = event.getY(0) - GodotInputHandler.handleMotionEvent(event) + inputHandler.handleMotionEvent(event) return true } } @@ -168,9 +171,9 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi override fun onDoubleTapEvent(event: MotionEvent): Boolean { if (event.actionMasked == MotionEvent.ACTION_UP) { nextDownIsDoubleTap = false - GodotInputHandler.handleMotionEvent(event) + inputHandler.handleMotionEvent(event) } else if (event.actionMasked == MotionEvent.ACTION_MOVE && !panningAndScalingEnabled) { - GodotInputHandler.handleMotionEvent(event) + inputHandler.handleMotionEvent(event) } return true @@ -191,7 +194,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi if (dragInProgress || lastDragX != 0.0f || lastDragY != 0.0f) { if (originEvent != null) { // Cancel the drag - GodotInputHandler.handleMotionEvent(originEvent, MotionEvent.ACTION_CANCEL) + inputHandler.handleMotionEvent(originEvent, MotionEvent.ACTION_CANCEL) } dragInProgress = false lastDragX = 0.0f @@ -202,12 +205,12 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi val x = terminusEvent.x val y = terminusEvent.y if (terminusEvent.pointerCount >= 2 && panningAndScalingEnabled && !pointerCaptureInProgress && !dragInProgress) { - GodotLib.pan(x, y, distanceX / 5f, distanceY / 5f) + inputHandler.handlePanEvent(x, y, distanceX / 5f, distanceY / 5f) } else if (!scaleInProgress) { dragInProgress = true lastDragX = terminusEvent.getX(0) lastDragY = terminusEvent.getY(0) - GodotInputHandler.handleMotionEvent(terminusEvent) + inputHandler.handleMotionEvent(terminusEvent) } return true } @@ -218,11 +221,7 @@ internal class GodotGestureHandler : SimpleOnGestureListener(), OnScaleGestureLi } if (detector.scaleFactor >= 0.8f && detector.scaleFactor != 1f && detector.scaleFactor <= 1.2f) { - GodotLib.magnify( - detector.focusX, - detector.focusY, - detector.scaleFactor - ) + inputHandler.handleMagnifyEvent(detector.focusX, detector.focusY, detector.scaleFactor) } 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 83e76e49c9..fb41cd00c0 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 @@ -32,10 +32,14 @@ package org.godotengine.godot.input; import static org.godotengine.godot.utils.GLUtils.DEBUG; +import org.godotengine.godot.Godot; import org.godotengine.godot.GodotLib; import org.godotengine.godot.GodotRenderView; import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; import android.hardware.input.InputManager; import android.os.Build; import android.util.Log; @@ -46,6 +50,10 @@ import android.view.InputDevice; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ScaleGestureDetector; +import android.view.Surface; +import android.view.WindowManager; + +import androidx.annotation.NonNull; import java.util.Collections; import java.util.HashSet; @@ -54,7 +62,7 @@ import java.util.Set; /** * Handles input related events for the {@link GodotRenderView} view. */ -public class GodotInputHandler implements InputManager.InputDeviceListener { +public class GodotInputHandler implements InputManager.InputDeviceListener, SensorEventListener { private static final String TAG = GodotInputHandler.class.getSimpleName(); private static final int ROTARY_INPUT_VERTICAL_AXIS = 1; @@ -64,8 +72,9 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { private final SparseArray<Joystick> mJoysticksDevices = new SparseArray<>(4); private final HashSet<Integer> mHardwareKeyboardIds = new HashSet<>(); - private final GodotRenderView mRenderView; + private final Godot godot; private final InputManager mInputManager; + private final WindowManager windowManager; private final GestureDetector gestureDetector; private final ScaleGestureDetector scaleGestureDetector; private final GodotGestureHandler godotGestureHandler; @@ -75,15 +84,16 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { */ private int lastSeenToolType = MotionEvent.TOOL_TYPE_UNKNOWN; - private static int rotaryInputAxis = ROTARY_INPUT_VERTICAL_AXIS; + private int rotaryInputAxis = ROTARY_INPUT_VERTICAL_AXIS; - public GodotInputHandler(GodotRenderView godotView) { - final Context context = godotView.getView().getContext(); - mRenderView = godotView; + public GodotInputHandler(Context context, Godot godot) { + this.godot = godot; mInputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE); mInputManager.registerInputDeviceListener(this, null); - this.godotGestureHandler = new GodotGestureHandler(); + windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); + + this.godotGestureHandler = new GodotGestureHandler(this); this.gestureDetector = new GestureDetector(context, godotGestureHandler); this.gestureDetector.setIsLongpressEnabled(false); this.scaleGestureDetector = new ScaleGestureDetector(context, godotGestureHandler); @@ -109,6 +119,14 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } /** + * @return true if input must be dispatched from the render thread. If false, input is + * dispatched from the UI thread. + */ + private boolean shouldDispatchInputToRenderThread() { + return GodotLib.shouldDispatchInputToRenderThread(); + } + + /** * On Wear OS devices, sets which axis of the mouse wheel rotary input is mapped to. This is 1 (vertical axis) by default. */ public void setRotaryInputAxis(int axis) { @@ -151,14 +169,14 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { if (mJoystickIds.indexOfKey(deviceId) >= 0) { final int button = getGodotButton(keyCode); final int godotJoyId = mJoystickIds.get(deviceId); - GodotLib.joybutton(godotJoyId, button, false); + handleJoystickButtonEvent(godotJoyId, button, false); } } else { // getKeyCode(): The physical key that was pressed. final int physical_keycode = event.getKeyCode(); final int unicode = event.getUnicodeChar(); final int key_label = event.getDisplayLabel(); - GodotLib.key(physical_keycode, unicode, key_label, false, event.getRepeatCount() > 0); + handleKeyEvent(physical_keycode, unicode, key_label, false, event.getRepeatCount() > 0); }; return true; @@ -166,7 +184,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { public boolean onKeyDown(final int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { - mRenderView.onBackPressed(); + godot.onBackPressed(); // press 'back' button should not terminate program //normal handle 'back' event in game logic return true; @@ -187,13 +205,13 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { if (mJoystickIds.indexOfKey(deviceId) >= 0) { final int button = getGodotButton(keyCode); final int godotJoyId = mJoystickIds.get(deviceId); - GodotLib.joybutton(godotJoyId, button, true); + handleJoystickButtonEvent(godotJoyId, button, true); } } else { final int physical_keycode = event.getKeyCode(); final int unicode = event.getUnicodeChar(); final int key_label = event.getDisplayLabel(); - GodotLib.key(physical_keycode, unicode, key_label, true, event.getRepeatCount() > 0); + handleKeyEvent(physical_keycode, unicode, key_label, true, event.getRepeatCount() > 0); } return true; @@ -248,7 +266,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { if (joystick.axesValues.indexOfKey(axis) < 0 || (float)joystick.axesValues.get(axis) != value) { // save value to prevent repeats joystick.axesValues.put(axis, value); - GodotLib.joyaxis(godotJoyId, i, value); + handleJoystickAxisEvent(godotJoyId, i, value); } } @@ -258,7 +276,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { if (joystick.hatX != hatX || joystick.hatY != hatY) { joystick.hatX = hatX; joystick.hatY = hatY; - GodotLib.joyhat(godotJoyId, hatX, hatY); + handleJoystickHatEvent(godotJoyId, hatX, hatY); } } return true; @@ -284,10 +302,12 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { int[] deviceIds = mInputManager.getInputDeviceIds(); for (int deviceId : deviceIds) { InputDevice device = mInputManager.getInputDevice(deviceId); - if (DEBUG) { - Log.v(TAG, String.format("init() deviceId:%d, Name:%s\n", deviceId, device.getName())); + if (device != null) { + if (DEBUG) { + Log.v(TAG, String.format("init() deviceId:%d, Name:%s\n", deviceId, device.getName())); + } + onInputDeviceAdded(deviceId); } - onInputDeviceAdded(deviceId); } } @@ -364,7 +384,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } mJoysticksDevices.put(deviceId, joystick); - GodotLib.joyconnectionchanged(id, true, joystick.name); + handleJoystickConnectionChangedEvent(id, true, joystick.name); } @Override @@ -378,7 +398,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { final int godotJoyId = mJoystickIds.get(deviceId); mJoystickIds.delete(deviceId); mJoysticksDevices.delete(deviceId); - GodotLib.joyconnectionchanged(godotJoyId, false, ""); + handleJoystickConnectionChangedEvent(godotJoyId, false, ""); } @Override @@ -452,7 +472,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return button; } - private static int getEventToolType(MotionEvent event) { + static int getEventToolType(MotionEvent event) { return event.getPointerCount() > 0 ? event.getToolType(0) : MotionEvent.TOOL_TYPE_UNKNOWN; } @@ -482,22 +502,22 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { } } - static boolean handleMotionEvent(final MotionEvent event) { + boolean handleMotionEvent(final MotionEvent event) { return handleMotionEvent(event, event.getActionMasked()); } - static boolean handleMotionEvent(final MotionEvent event, int eventActionOverride) { + boolean handleMotionEvent(final MotionEvent event, int eventActionOverride) { return handleMotionEvent(event, eventActionOverride, false); } - static boolean handleMotionEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { + boolean handleMotionEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { if (isMouseEvent(event)) { return handleMouseEvent(event, eventActionOverride, doubleTap); } return handleTouchEvent(event, eventActionOverride, doubleTap); } - private static float getEventTiltX(MotionEvent event) { + 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(); @@ -510,7 +530,7 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return (float)-Math.sin(orientation) * tiltMult; } - private static float getEventTiltY(MotionEvent event) { + 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(); @@ -523,19 +543,19 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return (float)Math.cos(orientation) * tiltMult; } - static boolean handleMouseEvent(final MotionEvent event) { + boolean handleMouseEvent(final MotionEvent event) { return handleMouseEvent(event, event.getActionMasked()); } - static boolean handleMouseEvent(final MotionEvent event, int eventActionOverride) { + boolean handleMouseEvent(final MotionEvent event, int eventActionOverride) { return handleMouseEvent(event, eventActionOverride, false); } - static boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { + 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) { + boolean handleMouseEvent(final MotionEvent event, int eventActionOverride, int buttonMaskOverride, boolean doubleTap) { final float x = event.getX(); final float y = event.getY(); @@ -564,11 +584,16 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { return handleMouseEvent(eventActionOverride, buttonMaskOverride, x, y, horizontalFactor, verticalFactor, doubleTap, sourceMouseRelative, pressure, getEventTiltX(event), getEventTiltY(event)); } - static boolean handleMouseEvent(int eventAction, boolean sourceMouseRelative) { + 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) { + boolean handleMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) { + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return false; + } + // Fix the buttonsMask switch (eventAction) { case MotionEvent.ACTION_CANCEL: @@ -596,38 +621,31 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { case MotionEvent.ACTION_HOVER_MOVE: case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_SCROLL: { - GodotLib.dispatchMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY); + runnable.setMouseEvent(eventAction, buttonsMask, x, y, deltaX, deltaY, doubleClick, sourceMouseRelative, pressure, tiltX, tiltY); + dispatchInputEventRunnable(runnable); return true; } } return false; } - static boolean handleTouchEvent(final MotionEvent event) { + boolean handleTouchEvent(final MotionEvent event) { return handleTouchEvent(event, event.getActionMasked()); } - static boolean handleTouchEvent(final MotionEvent event, int eventActionOverride) { + 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) { + boolean handleTouchEvent(final MotionEvent event, int eventActionOverride, boolean doubleTap) { + if (event.getPointerCount() == 0) { return true; } - final float[] positions = new float[pointerCount * 6]; // pointerId1, x1, y1, pressure1, tiltX1, tiltY1, pointerId2, etc... - - for (int i = 0; i < pointerCount; 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); + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return false; } - final int actionPointerId = event.getPointerId(event.getActionIndex()); switch (eventActionOverride) { case MotionEvent.ACTION_DOWN: @@ -636,10 +654,137 @@ public class GodotInputHandler implements InputManager.InputDeviceListener { case MotionEvent.ACTION_MOVE: case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_POINTER_DOWN: { - GodotLib.dispatchTouchEvent(eventActionOverride, actionPointerId, pointerCount, positions, doubleTap); + runnable.setTouchEvent(event, eventActionOverride, doubleTap); + dispatchInputEventRunnable(runnable); return true; } } return false; } + + void handleMagnifyEvent(float x, float y, float factor) { + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; + } + + runnable.setMagnifyEvent(x, y, factor); + dispatchInputEventRunnable(runnable); + } + + void handlePanEvent(float x, float y, float deltaX, float deltaY) { + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; + } + + runnable.setPanEvent(x, y, deltaX, deltaY); + dispatchInputEventRunnable(runnable); + } + + private void handleJoystickButtonEvent(int device, int button, boolean pressed) { + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; + } + + runnable.setJoystickButtonEvent(device, button, pressed); + dispatchInputEventRunnable(runnable); + } + + private void handleJoystickAxisEvent(int device, int axis, float value) { + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; + } + + runnable.setJoystickAxisEvent(device, axis, value); + dispatchInputEventRunnable(runnable); + } + + private void handleJoystickHatEvent(int device, int hatX, int hatY) { + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; + } + + runnable.setJoystickHatEvent(device, hatX, hatY); + dispatchInputEventRunnable(runnable); + } + + private void handleJoystickConnectionChangedEvent(int device, boolean connected, String name) { + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; + } + + runnable.setJoystickConnectionChangedEvent(device, connected, name); + dispatchInputEventRunnable(runnable); + } + + void handleKeyEvent(int physicalKeycode, int unicode, int keyLabel, boolean pressed, boolean echo) { + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; + } + + runnable.setKeyEvent(physicalKeycode, unicode, keyLabel, pressed, echo); + dispatchInputEventRunnable(runnable); + } + + private void dispatchInputEventRunnable(@NonNull InputEventRunnable runnable) { + if (shouldDispatchInputToRenderThread()) { + godot.runOnRenderThread(runnable); + } else { + runnable.run(); + } + } + + @Override + public void onSensorChanged(SensorEvent event) { + final float[] values = event.values; + if (values == null || values.length != 3) { + return; + } + + InputEventRunnable runnable = InputEventRunnable.obtain(); + if (runnable == null) { + return; + } + + float rotatedValue0 = 0f; + float rotatedValue1 = 0f; + float rotatedValue2 = 0f; + switch (windowManager.getDefaultDisplay().getRotation()) { + case Surface.ROTATION_0: + rotatedValue0 = values[0]; + rotatedValue1 = values[1]; + rotatedValue2 = values[2]; + break; + + case Surface.ROTATION_90: + rotatedValue0 = -values[1]; + rotatedValue1 = values[0]; + rotatedValue2 = values[2]; + break; + + case Surface.ROTATION_180: + rotatedValue0 = -values[0]; + rotatedValue1 = -values[1]; + rotatedValue2 = values[2]; + break; + + case Surface.ROTATION_270: + rotatedValue0 = values[1]; + rotatedValue1 = -values[0]; + rotatedValue2 = values[2]; + break; + } + + runnable.setSensorEvent(event.sensor.getType(), rotatedValue0, rotatedValue1, rotatedValue2); + godot.runOnRenderThread(runnable); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) {} } diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java index 06b565c30f..e545669970 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java +++ b/platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java @@ -93,8 +93,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene @Override public void beforeTextChanged(final CharSequence pCharSequence, final int start, final int count, final int after) { for (int i = 0; i < count; ++i) { - GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, true, false); - GodotLib.key(KeyEvent.KEYCODE_DEL, 0, 0, false, false); + mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_DEL, 0, 0, true, false); + mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_DEL, 0, 0, false, false); if (mHasSelection) { mHasSelection = false; @@ -115,8 +115,8 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene // Return keys are handled through action events continue; } - GodotLib.key(0, character, 0, true, false); - GodotLib.key(0, character, 0, false, false); + mRenderView.getInputHandler().handleKeyEvent(0, character, 0, true, false); + mRenderView.getInputHandler().handleKeyEvent(0, character, 0, false, false); } } @@ -127,18 +127,16 @@ public class GodotTextInputWrapper implements TextWatcher, OnEditorActionListene if (characters != null) { for (int i = 0; i < characters.length(); i++) { final int character = characters.codePointAt(i); - GodotLib.key(0, character, 0, true, false); - GodotLib.key(0, character, 0, false, false); + mRenderView.getInputHandler().handleKeyEvent(0, character, 0, true, false); + mRenderView.getInputHandler().handleKeyEvent(0, character, 0, false, false); } } } if (pActionID == EditorInfo.IME_ACTION_DONE) { // Enter key has been pressed - mRenderView.queueOnRenderThread(() -> { - GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, true, false); - GodotLib.key(KeyEvent.KEYCODE_ENTER, 0, 0, false, false); - }); + mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_ENTER, 0, 0, true, false); + mRenderView.getInputHandler().handleKeyEvent(KeyEvent.KEYCODE_ENTER, 0, 0, false, false); mRenderView.getView().requestFocus(); return true; } diff --git a/platform/android/java/lib/src/org/godotengine/godot/input/InputEventRunnable.java b/platform/android/java/lib/src/org/godotengine/godot/input/InputEventRunnable.java new file mode 100644 index 0000000000..a282791b2e --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/input/InputEventRunnable.java @@ -0,0 +1,353 @@ +/**************************************************************************/ +/* InputEventRunnable.java */ +/**************************************************************************/ +/* 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.input; + +import org.godotengine.godot.GodotLib; + +import android.hardware.Sensor; +import android.util.Log; +import android.view.MotionEvent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Pools; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Used to dispatch input events. + * + * This is a specialized version of @{@link Runnable} which allows to allocate a finite pool of + * objects for input events dispatching, thus avoid the creation (and garbage collection) of + * spurious @{@link Runnable} objects. + */ +final class InputEventRunnable implements Runnable { + private static final String TAG = InputEventRunnable.class.getSimpleName(); + + private static final int MAX_TOUCH_POINTER_COUNT = 10; // assuming 10 fingers as max supported concurrent touch pointers + + private static final Pools.Pool<InputEventRunnable> POOL = new Pools.Pool<>() { + private static final int MAX_POOL_SIZE = 120 * 10; // up to 120Hz input events rate for up to 5 secs (ANR limit) * 2 + + private final ArrayBlockingQueue<InputEventRunnable> queue = new ArrayBlockingQueue<>(MAX_POOL_SIZE); + private final AtomicInteger createdCount = new AtomicInteger(); + + @Nullable + @Override + public InputEventRunnable acquire() { + InputEventRunnable instance = queue.poll(); + if (instance == null) { + int creationCount = createdCount.incrementAndGet(); + if (creationCount <= MAX_POOL_SIZE) { + instance = new InputEventRunnable(creationCount - 1); + } + } + + return instance; + } + + @Override + public boolean release(@NonNull InputEventRunnable instance) { + return queue.offer(instance); + } + }; + + @Nullable + static InputEventRunnable obtain() { + InputEventRunnable runnable = POOL.acquire(); + if (runnable == null) { + Log.w(TAG, "Input event pool is at capacity"); + } + return runnable; + } + + /** + * Used to track when this instance was created and added to the pool. Primarily used for + * debug purposes. + */ + private final int creationRank; + + private InputEventRunnable(int creationRank) { + this.creationRank = creationRank; + } + + /** + * Set of supported input events. + */ + private enum EventType { + MOUSE, + TOUCH, + MAGNIFY, + PAN, + JOYSTICK_BUTTON, + JOYSTICK_AXIS, + JOYSTICK_HAT, + JOYSTICK_CONNECTION_CHANGED, + KEY, + SENSOR + } + + private EventType currentEventType = null; + + // common event fields + private float eventX; + private float eventY; + private float eventDeltaX; + private float eventDeltaY; + private boolean eventPressed; + + // common touch / mouse fields + private int eventAction; + private boolean doubleTap; + + // Mouse event fields and setter + private int buttonsMask; + private boolean sourceMouseRelative; + private float pressure; + private float tiltX; + private float tiltY; + void setMouseEvent(int eventAction, int buttonsMask, float x, float y, float deltaX, float deltaY, boolean doubleClick, boolean sourceMouseRelative, float pressure, float tiltX, float tiltY) { + this.currentEventType = EventType.MOUSE; + this.eventAction = eventAction; + this.buttonsMask = buttonsMask; + this.eventX = x; + this.eventY = y; + this.eventDeltaX = deltaX; + this.eventDeltaY = deltaY; + this.doubleTap = doubleClick; + this.sourceMouseRelative = sourceMouseRelative; + this.pressure = pressure; + this.tiltX = tiltX; + this.tiltY = tiltY; + } + + // Touch event fields and setter + private int actionPointerId; + private int pointerCount; + private final float[] positions = new float[MAX_TOUCH_POINTER_COUNT * 6]; // pointerId1, x1, y1, pressure1, tiltX1, tiltY1, pointerId2, etc... + void setTouchEvent(MotionEvent event, int eventAction, boolean doubleTap) { + this.currentEventType = EventType.TOUCH; + this.eventAction = eventAction; + this.doubleTap = doubleTap; + this.actionPointerId = event.getPointerId(event.getActionIndex()); + this.pointerCount = Math.min(event.getPointerCount(), MAX_TOUCH_POINTER_COUNT); + for (int i = 0; i < pointerCount; 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] = GodotInputHandler.getEventTiltX(event); + positions[i * 6 + 5] = GodotInputHandler.getEventTiltY(event); + } + } + + // Magnify event fields and setter + private float magnifyFactor; + void setMagnifyEvent(float x, float y, float factor) { + this.currentEventType = EventType.MAGNIFY; + this.eventX = x; + this.eventY = y; + this.magnifyFactor = factor; + } + + // Pan event setter + void setPanEvent(float x, float y, float deltaX, float deltaY) { + this.currentEventType = EventType.PAN; + this.eventX = x; + this.eventY = y; + this.eventDeltaX = deltaX; + this.eventDeltaY = deltaY; + } + + // common joystick field + private int joystickDevice; + + // Joystick button event fields and setter + private int button; + void setJoystickButtonEvent(int device, int button, boolean pressed) { + this.currentEventType = EventType.JOYSTICK_BUTTON; + this.joystickDevice = device; + this.button = button; + this.eventPressed = pressed; + } + + // Joystick axis event fields and setter + private int axis; + private float value; + void setJoystickAxisEvent(int device, int axis, float value) { + this.currentEventType = EventType.JOYSTICK_AXIS; + this.joystickDevice = device; + this.axis = axis; + this.value = value; + } + + // Joystick hat event fields and setter + private int hatX; + private int hatY; + void setJoystickHatEvent(int device, int hatX, int hatY) { + this.currentEventType = EventType.JOYSTICK_HAT; + this.joystickDevice = device; + this.hatX = hatX; + this.hatY = hatY; + } + + // Joystick connection changed event fields and setter + private boolean connected; + private String joystickName; + void setJoystickConnectionChangedEvent(int device, boolean connected, String name) { + this.currentEventType = EventType.JOYSTICK_CONNECTION_CHANGED; + this.joystickDevice = device; + this.connected = connected; + this.joystickName = name; + } + + // Key event fields and setter + private int physicalKeycode; + private int unicode; + private int keyLabel; + private boolean echo; + void setKeyEvent(int physicalKeycode, int unicode, int keyLabel, boolean pressed, boolean echo) { + this.currentEventType = EventType.KEY; + this.physicalKeycode = physicalKeycode; + this.unicode = unicode; + this.keyLabel = keyLabel; + this.eventPressed = pressed; + this.echo = echo; + } + + // Sensor event fields and setter + private int sensorType; + private float rotatedValue0; + private float rotatedValue1; + private float rotatedValue2; + void setSensorEvent(int sensorType, float rotatedValue0, float rotatedValue1, float rotatedValue2) { + this.currentEventType = EventType.SENSOR; + this.sensorType = sensorType; + this.rotatedValue0 = rotatedValue0; + this.rotatedValue1 = rotatedValue1; + this.rotatedValue2 = rotatedValue2; + } + + @Override + public void run() { + try { + if (currentEventType == null) { + Log.w(TAG, "Invalid event type"); + return; + } + + switch (currentEventType) { + case MOUSE: + GodotLib.dispatchMouseEvent( + eventAction, + buttonsMask, + eventX, + eventY, + eventDeltaX, + eventDeltaY, + doubleTap, + sourceMouseRelative, + pressure, + tiltX, + tiltY); + break; + + case TOUCH: + GodotLib.dispatchTouchEvent( + eventAction, + actionPointerId, + pointerCount, + positions, + doubleTap); + break; + + case MAGNIFY: + GodotLib.magnify(eventX, eventY, magnifyFactor); + break; + + case PAN: + GodotLib.pan(eventX, eventY, eventDeltaX, eventDeltaY); + break; + + case JOYSTICK_BUTTON: + GodotLib.joybutton(joystickDevice, button, eventPressed); + break; + + case JOYSTICK_AXIS: + GodotLib.joyaxis(joystickDevice, axis, value); + break; + + case JOYSTICK_HAT: + GodotLib.joyhat(joystickDevice, hatX, hatY); + break; + + case JOYSTICK_CONNECTION_CHANGED: + GodotLib.joyconnectionchanged(joystickDevice, connected, joystickName); + break; + + case KEY: + GodotLib.key(physicalKeycode, unicode, keyLabel, eventPressed, echo); + break; + + case SENSOR: + switch (sensorType) { + case Sensor.TYPE_ACCELEROMETER: + GodotLib.accelerometer(-rotatedValue0, -rotatedValue1, -rotatedValue2); + break; + + case Sensor.TYPE_GRAVITY: + GodotLib.gravity(-rotatedValue0, -rotatedValue1, -rotatedValue2); + break; + + case Sensor.TYPE_MAGNETIC_FIELD: + GodotLib.magnetometer(-rotatedValue0, -rotatedValue1, -rotatedValue2); + break; + + case Sensor.TYPE_GYROSCOPE: + GodotLib.gyroscope(rotatedValue0, rotatedValue1, rotatedValue2); + break; + } + break; + } + } finally { + recycle(); + } + } + + /** + * Release the current instance back to the pool + */ + private void recycle() { + currentEventType = null; + POOL.release(this); + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt index 8ee3d5f48f..574ecd58eb 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt @@ -34,12 +34,18 @@ import android.content.Context import android.os.Build import android.os.Environment import java.io.File +import org.godotengine.godot.GodotLib /** * Represents the different storage scopes. */ internal enum class StorageScope { /** + * Covers the 'assets' directory + */ + ASSETS, + + /** * Covers internal and external directories accessible to the app without restrictions. */ APP, @@ -56,6 +62,10 @@ internal enum class StorageScope { class Identifier(context: Context) { + companion object { + internal const val ASSETS_PREFIX = "assets://" + } + private val internalAppDir: String? = context.filesDir.canonicalPath private val internalCacheDir: String? = context.cacheDir.canonicalPath private val externalAppDir: String? = context.getExternalFilesDir(null)?.canonicalPath @@ -64,6 +74,14 @@ internal enum class StorageScope { private val documentsSharedDir: String? = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).canonicalPath /** + * Determine if the given path is accessible. + */ + fun canAccess(path: String?): Boolean { + val storageScope = identifyStorageScope(path) + return storageScope == APP || storageScope == SHARED + } + + /** * Determines which [StorageScope] the given path falls under. */ fun identifyStorageScope(path: String?): StorageScope { @@ -71,9 +89,16 @@ internal enum class StorageScope { return UNKNOWN } - val pathFile = File(path) + if (path.startsWith(ASSETS_PREFIX)) { + return ASSETS + } + + var pathFile = File(path) if (!pathFile.isAbsolute) { - return UNKNOWN + pathFile = File(GodotLib.getProjectResourceDir(), path) + if (!pathFile.isAbsolute) { + return UNKNOWN + } } // If we have 'All Files Access' permission, we can access all directories without diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt index b9b7ebac6e..523e852518 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt @@ -33,18 +33,30 @@ package org.godotengine.godot.io.directory import android.content.Context import android.util.Log import android.util.SparseArray +import org.godotengine.godot.io.StorageScope import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.INVALID_DIR_ID import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_DIR_ID +import org.godotengine.godot.io.file.AssetData import java.io.File import java.io.IOException /** * Handles directories access within the Android assets directory. */ -internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.DirectoryAccess { +internal class AssetsDirectoryAccess(private val context: Context) : DirectoryAccessHandler.DirectoryAccess { companion object { private val TAG = AssetsDirectoryAccess::class.java.simpleName + + internal fun getAssetsPath(originalPath: String): String { + if (originalPath.startsWith(File.separator)) { + return originalPath.substring(File.separator.length) + } + if (originalPath.startsWith(StorageScope.Identifier.ASSETS_PREFIX)) { + return originalPath.substring(StorageScope.Identifier.ASSETS_PREFIX.length) + } + return originalPath + } } private data class AssetDir(val path: String, val files: Array<String>, var current: Int = 0) @@ -54,13 +66,6 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler. private var lastDirId = STARTING_DIR_ID private val dirs = SparseArray<AssetDir>() - private fun getAssetsPath(originalPath: String): String { - if (originalPath.startsWith(File.separatorChar)) { - return originalPath.substring(1) - } - return originalPath - } - override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0 override fun dirOpen(path: String): Int { @@ -68,8 +73,8 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler. try { val files = assetManager.list(assetsPath) ?: return INVALID_DIR_ID // Empty directories don't get added to the 'assets' directory, so - // if ad.files.length > 0 ==> path is directory - // if ad.files.length == 0 ==> path is file + // if files.length > 0 ==> path is directory + // if files.length == 0 ==> path is file if (files.isEmpty()) { return INVALID_DIR_ID } @@ -89,8 +94,8 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler. try { val files = assetManager.list(assetsPath) ?: return false // Empty directories don't get added to the 'assets' directory, so - // if ad.files.length > 0 ==> path is directory - // if ad.files.length == 0 ==> path is file + // if files.length > 0 ==> path is directory + // if files.length == 0 ==> path is file return files.isNotEmpty() } catch (e: IOException) { Log.e(TAG, "Exception on dirExists", e) @@ -98,19 +103,7 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler. } } - override fun fileExists(path: String): Boolean { - val assetsPath = getAssetsPath(path) - try { - val files = assetManager.list(assetsPath) ?: return false - // Empty directories don't get added to the 'assets' directory, so - // if ad.files.length > 0 ==> path is directory - // if ad.files.length == 0 ==> path is file - return files.isEmpty() - } catch (e: IOException) { - Log.e(TAG, "Exception on fileExists", e) - return false - } - } + override fun fileExists(path: String) = AssetData.fileExists(context, path) override fun dirIsDir(dirId: Int): Boolean { val ad: AssetDir = dirs[dirId] @@ -171,7 +164,7 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler. override fun getSpaceLeft() = 0L - override fun rename(from: String, to: String) = false + override fun rename(from: String, to: String) = AssetData.rename(from, to) - override fun remove(filename: String) = false + override fun remove(filename: String) = AssetData.delete(filename) } diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt index dd6d5180c5..9f3461200b 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt @@ -32,7 +32,8 @@ package org.godotengine.godot.io.directory import android.content.Context import android.util.Log -import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_FILESYSTEM +import org.godotengine.godot.Godot +import org.godotengine.godot.io.StorageScope import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_RESOURCES /** @@ -45,18 +46,82 @@ class DirectoryAccessHandler(context: Context) { internal const val INVALID_DIR_ID = -1 internal const val STARTING_DIR_ID = 1 - - private fun getAccessTypeFromNative(accessType: Int): AccessType? { - return when (accessType) { - ACCESS_RESOURCES.nativeValue -> ACCESS_RESOURCES - ACCESS_FILESYSTEM.nativeValue -> ACCESS_FILESYSTEM - else -> null - } - } } private enum class AccessType(val nativeValue: Int) { - ACCESS_RESOURCES(0), ACCESS_FILESYSTEM(2) + ACCESS_RESOURCES(0), + + /** + * Maps to [ACCESS_FILESYSTEM] + */ + ACCESS_USERDATA(1), + ACCESS_FILESYSTEM(2); + + fun generateDirAccessId(dirId: Int) = (dirId * DIR_ACCESS_ID_MULTIPLIER) + nativeValue + + companion object { + const val DIR_ACCESS_ID_MULTIPLIER = 10 + + fun fromDirAccessId(dirAccessId: Int): Pair<AccessType?, Int> { + val nativeValue = dirAccessId % DIR_ACCESS_ID_MULTIPLIER + val dirId = dirAccessId / DIR_ACCESS_ID_MULTIPLIER + return Pair(fromNative(nativeValue), dirId) + } + + private fun fromNative(nativeAccessType: Int): AccessType? { + for (accessType in entries) { + if (accessType.nativeValue == nativeAccessType) { + return accessType + } + } + return null + } + + fun fromNative(nativeAccessType: Int, storageScope: StorageScope? = null): AccessType? { + val accessType = fromNative(nativeAccessType) + if (accessType == null) { + Log.w(TAG, "Unsupported access type $nativeAccessType") + return null + } + + // 'Resources' access type takes precedence as it is simple to handle: + // if we receive a 'Resources' access type and this is a template build, + // we provide a 'Resources' directory handler. + // If this is an editor build, 'Resources' refers to the opened project resources + // and so we provide a 'Filesystem' directory handler. + if (accessType == ACCESS_RESOURCES) { + return if (Godot.isEditorBuild()) { + ACCESS_FILESYSTEM + } else { + ACCESS_RESOURCES + } + } else { + // We've received a 'Filesystem' or 'Userdata' access type. On Android, this + // may refer to: + // - assets directory (path has 'assets:/' prefix) + // - app directories + // - device shared directories + // As such we check the storage scope (if available) to figure what type of + // directory handler to provide + if (storageScope != null) { + val accessTypeFromStorageScope = when (storageScope) { + StorageScope.ASSETS -> ACCESS_RESOURCES + StorageScope.APP, StorageScope.SHARED -> ACCESS_FILESYSTEM + StorageScope.UNKNOWN -> null + } + + if (accessTypeFromStorageScope != null) { + return accessTypeFromStorageScope + } + } + // If we're not able to infer the type of directory handler from the storage + // scope, we fall-back to the 'Filesystem' directory handler as it's the default + // for the 'Filesystem' access type. + // Note that ACCESS_USERDATA also maps to ACCESS_FILESYSTEM + return ACCESS_FILESYSTEM + } + } + } } internal interface DirectoryAccess { @@ -76,8 +141,10 @@ class DirectoryAccessHandler(context: Context) { fun remove(filename: String): Boolean } + private val storageScopeIdentifier = StorageScope.Identifier(context) + private val assetsDirAccess = AssetsDirectoryAccess(context) - private val fileSystemDirAccess = FilesystemDirectoryAccess(context) + private val fileSystemDirAccess = FilesystemDirectoryAccess(context, storageScopeIdentifier) fun assetsFileExists(assetsPath: String) = assetsDirAccess.fileExists(assetsPath) fun filesystemFileExists(path: String) = fileSystemDirAccess.fileExists(path) @@ -85,24 +152,32 @@ class DirectoryAccessHandler(context: Context) { private fun hasDirId(accessType: AccessType, dirId: Int): Boolean { return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId) - ACCESS_FILESYSTEM -> fileSystemDirAccess.hasDirId(dirId) + else -> fileSystemDirAccess.hasDirId(dirId) } } fun dirOpen(nativeAccessType: Int, path: String?): Int { - val accessType = getAccessTypeFromNative(nativeAccessType) - if (path == null || accessType == null) { + if (path == null) { return INVALID_DIR_ID } - return when (accessType) { + val storageScope = storageScopeIdentifier.identifyStorageScope(path) + val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return INVALID_DIR_ID + + val dirId = when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.dirOpen(path) - ACCESS_FILESYSTEM -> fileSystemDirAccess.dirOpen(path) + else -> fileSystemDirAccess.dirOpen(path) + } + if (dirId == INVALID_DIR_ID) { + return INVALID_DIR_ID } + + val dirAccessId = accessType.generateDirAccessId(dirId) + return dirAccessId } - fun dirNext(nativeAccessType: Int, dirId: Int): String { - val accessType = getAccessTypeFromNative(nativeAccessType) + fun dirNext(dirAccessId: Int): String { + val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId) if (accessType == null || !hasDirId(accessType, dirId)) { Log.w(TAG, "dirNext: Invalid dir id: $dirId") return "" @@ -110,12 +185,12 @@ class DirectoryAccessHandler(context: Context) { return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.dirNext(dirId) - ACCESS_FILESYSTEM -> fileSystemDirAccess.dirNext(dirId) + else -> fileSystemDirAccess.dirNext(dirId) } } - fun dirClose(nativeAccessType: Int, dirId: Int) { - val accessType = getAccessTypeFromNative(nativeAccessType) + fun dirClose(dirAccessId: Int) { + val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId) if (accessType == null || !hasDirId(accessType, dirId)) { Log.w(TAG, "dirClose: Invalid dir id: $dirId") return @@ -123,12 +198,12 @@ class DirectoryAccessHandler(context: Context) { when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.dirClose(dirId) - ACCESS_FILESYSTEM -> fileSystemDirAccess.dirClose(dirId) + else -> fileSystemDirAccess.dirClose(dirId) } } - fun dirIsDir(nativeAccessType: Int, dirId: Int): Boolean { - val accessType = getAccessTypeFromNative(nativeAccessType) + fun dirIsDir(dirAccessId: Int): Boolean { + val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId) if (accessType == null || !hasDirId(accessType, dirId)) { Log.w(TAG, "dirIsDir: Invalid dir id: $dirId") return false @@ -136,91 +211,106 @@ class DirectoryAccessHandler(context: Context) { return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.dirIsDir(dirId) - ACCESS_FILESYSTEM -> fileSystemDirAccess.dirIsDir(dirId) + else -> fileSystemDirAccess.dirIsDir(dirId) } } - fun isCurrentHidden(nativeAccessType: Int, dirId: Int): Boolean { - val accessType = getAccessTypeFromNative(nativeAccessType) + fun isCurrentHidden(dirAccessId: Int): Boolean { + val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId) if (accessType == null || !hasDirId(accessType, dirId)) { return false } return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.isCurrentHidden(dirId) - ACCESS_FILESYSTEM -> fileSystemDirAccess.isCurrentHidden(dirId) + else -> fileSystemDirAccess.isCurrentHidden(dirId) } } fun dirExists(nativeAccessType: Int, path: String?): Boolean { - val accessType = getAccessTypeFromNative(nativeAccessType) - if (path == null || accessType == null) { + if (path == null) { return false } + val storageScope = storageScopeIdentifier.identifyStorageScope(path) + val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false + return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.dirExists(path) - ACCESS_FILESYSTEM -> fileSystemDirAccess.dirExists(path) + else -> fileSystemDirAccess.dirExists(path) } } fun fileExists(nativeAccessType: Int, path: String?): Boolean { - val accessType = getAccessTypeFromNative(nativeAccessType) - if (path == null || accessType == null) { + if (path == null) { return false } + val storageScope = storageScopeIdentifier.identifyStorageScope(path) + val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false + return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.fileExists(path) - ACCESS_FILESYSTEM -> fileSystemDirAccess.fileExists(path) + else -> fileSystemDirAccess.fileExists(path) } } fun getDriveCount(nativeAccessType: Int): Int { - val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0 + val accessType = AccessType.fromNative(nativeAccessType) ?: return 0 return when(accessType) { ACCESS_RESOURCES -> assetsDirAccess.getDriveCount() - ACCESS_FILESYSTEM -> fileSystemDirAccess.getDriveCount() + else -> fileSystemDirAccess.getDriveCount() } } fun getDrive(nativeAccessType: Int, drive: Int): String { - val accessType = getAccessTypeFromNative(nativeAccessType) ?: return "" + val accessType = AccessType.fromNative(nativeAccessType) ?: return "" return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.getDrive(drive) - ACCESS_FILESYSTEM -> fileSystemDirAccess.getDrive(drive) + else -> fileSystemDirAccess.getDrive(drive) } } - fun makeDir(nativeAccessType: Int, dir: String): Boolean { - val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false + fun makeDir(nativeAccessType: Int, dir: String?): Boolean { + if (dir == null) { + return false + } + + val storageScope = storageScopeIdentifier.identifyStorageScope(dir) + val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false + return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.makeDir(dir) - ACCESS_FILESYSTEM -> fileSystemDirAccess.makeDir(dir) + else -> fileSystemDirAccess.makeDir(dir) } } fun getSpaceLeft(nativeAccessType: Int): Long { - val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0L + val accessType = AccessType.fromNative(nativeAccessType) ?: return 0L return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.getSpaceLeft() - ACCESS_FILESYSTEM -> fileSystemDirAccess.getSpaceLeft() + else -> fileSystemDirAccess.getSpaceLeft() } } fun rename(nativeAccessType: Int, from: String, to: String): Boolean { - val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false + val accessType = AccessType.fromNative(nativeAccessType) ?: return false return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.rename(from, to) - ACCESS_FILESYSTEM -> fileSystemDirAccess.rename(from, to) + else -> fileSystemDirAccess.rename(from, to) } } - fun remove(nativeAccessType: Int, filename: String): Boolean { - val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false + fun remove(nativeAccessType: Int, filename: String?): Boolean { + if (filename == null) { + return false + } + + val storageScope = storageScopeIdentifier.identifyStorageScope(filename) + val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false return when (accessType) { ACCESS_RESOURCES -> assetsDirAccess.remove(filename) - ACCESS_FILESYSTEM -> fileSystemDirAccess.remove(filename) + else -> fileSystemDirAccess.remove(filename) } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt index c8b4f79f30..2830216e12 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt @@ -45,7 +45,7 @@ import java.io.File /** * Handles directories access with the internal and external filesystem. */ -internal class FilesystemDirectoryAccess(private val context: Context): +internal class FilesystemDirectoryAccess(private val context: Context, private val storageScopeIdentifier: StorageScope.Identifier): DirectoryAccessHandler.DirectoryAccess { companion object { @@ -54,7 +54,6 @@ internal class FilesystemDirectoryAccess(private val context: Context): private data class DirData(val dirFile: File, val files: Array<File>, var current: Int = 0) - private val storageScopeIdentifier = StorageScope.Identifier(context) private val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager private var lastDirId = STARTING_DIR_ID private val dirs = SparseArray<DirData>() @@ -63,7 +62,8 @@ internal class FilesystemDirectoryAccess(private val context: Context): // Directory access is available for shared storage on Android 11+ // On Android 10, access is also available as long as the `requestLegacyExternalStorage` // tag is available. - return storageScopeIdentifier.identifyStorageScope(path) != StorageScope.UNKNOWN + val storageScope = storageScopeIdentifier.identifyStorageScope(path) + return storageScope != StorageScope.UNKNOWN && storageScope != StorageScope.ASSETS } override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0 diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/AssetData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/AssetData.kt new file mode 100644 index 0000000000..1ab739d90b --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/AssetData.kt @@ -0,0 +1,151 @@ +/**************************************************************************/ +/* AssetData.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.io.file + +import android.content.Context +import android.content.res.AssetManager +import android.util.Log +import org.godotengine.godot.error.Error +import org.godotengine.godot.io.directory.AssetsDirectoryAccess +import java.io.IOException +import java.io.InputStream +import java.lang.UnsupportedOperationException +import java.nio.ByteBuffer +import java.nio.channels.Channels +import java.nio.channels.ReadableByteChannel + +/** + * Implementation of the [DataAccess] which handles access and interaction with files in the + * 'assets' directory + */ +internal class AssetData(context: Context, private val filePath: String, accessFlag: FileAccessFlags) : DataAccess() { + + companion object { + private val TAG = AssetData::class.java.simpleName + + fun fileExists(context: Context, path: String): Boolean { + val assetsPath = AssetsDirectoryAccess.getAssetsPath(path) + try { + val files = context.assets.list(assetsPath) ?: return false + // Empty directories don't get added to the 'assets' directory, so + // if files.length > 0 ==> path is directory + // if files.length == 0 ==> path is file + return files.isEmpty() + } catch (e: IOException) { + Log.e(TAG, "Exception on fileExists", e) + return false + } + } + + fun fileLastModified(path: String) = 0L + + fun delete(path: String) = false + + fun rename(from: String, to: String) = false + } + + private val inputStream: InputStream + internal val readChannel: ReadableByteChannel + + private var position = 0L + private val length: Long + + init { + if (accessFlag == FileAccessFlags.WRITE) { + throw UnsupportedOperationException("Writing to the 'assets' directory is not supported") + } + + val assetsPath = AssetsDirectoryAccess.getAssetsPath(filePath) + inputStream = context.assets.open(assetsPath, AssetManager.ACCESS_BUFFER) + readChannel = Channels.newChannel(inputStream) + + length = inputStream.available().toLong() + } + + override fun close() { + try { + inputStream.close() + } catch (e: IOException) { + Log.w(TAG, "Exception when closing file $filePath.", e) + } + } + + override fun flush() { + Log.w(TAG, "flush() is not supported.") + } + + override fun seek(position: Long) { + try { + inputStream.skip(position) + + this.position = position + if (this.position > length) { + this.position = length + endOfFile = true + } else { + endOfFile = false + } + + } catch(e: IOException) { + Log.w(TAG, "Exception when seeking file $filePath.", e) + } + } + + override fun resize(length: Long): Error { + Log.w(TAG, "resize() is not supported.") + return Error.ERR_UNAVAILABLE + } + + override fun position() = position + + override fun size() = length + + override fun read(buffer: ByteBuffer): Int { + return try { + val readBytes = readChannel.read(buffer) + if (readBytes == -1) { + endOfFile = true + 0 + } else { + position += readBytes + endOfFile = position() >= size() + readBytes + } + } catch (e: IOException) { + Log.w(TAG, "Exception while reading from $filePath.", e) + 0 + } + } + + override fun write(buffer: ByteBuffer) { + Log.w(TAG, "write() is not supported.") + } +} 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 11cf7b3566..73f020f249 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 @@ -33,12 +33,17 @@ package org.godotengine.godot.io.file import android.content.Context import android.os.Build import android.util.Log +import org.godotengine.godot.error.Error import org.godotengine.godot.io.StorageScope +import java.io.FileNotFoundException import java.io.IOException +import java.io.InputStream import java.nio.ByteBuffer +import java.nio.channels.Channels import java.nio.channels.ClosedChannelException import java.nio.channels.FileChannel import java.nio.channels.NonWritableChannelException +import kotlin.jvm.Throws import kotlin.math.max /** @@ -47,11 +52,37 @@ import kotlin.math.max * Its derived instances provide concrete implementations to handle regular file access, as well * as file access through the media store API on versions of Android were scoped storage is enabled. */ -internal abstract class DataAccess(private val filePath: String) { +internal abstract class DataAccess { companion object { private val TAG = DataAccess::class.java.simpleName + @Throws(java.lang.Exception::class, FileNotFoundException::class) + fun getInputStream(storageScope: StorageScope, context: Context, filePath: String): InputStream? { + return when(storageScope) { + StorageScope.ASSETS -> { + val assetData = AssetData(context, filePath, FileAccessFlags.READ) + Channels.newInputStream(assetData.readChannel) + } + + StorageScope.APP -> { + val fileData = FileData(filePath, FileAccessFlags.READ) + Channels.newInputStream(fileData.fileChannel) + } + StorageScope.SHARED -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val mediaStoreData = MediaStoreData(context, filePath, FileAccessFlags.READ) + Channels.newInputStream(mediaStoreData.fileChannel) + } else { + null + } + } + + StorageScope.UNKNOWN -> null + } + } + + @Throws(java.lang.Exception::class, FileNotFoundException::class) fun generateDataAccess( storageScope: StorageScope, context: Context, @@ -61,6 +92,8 @@ internal abstract class DataAccess(private val filePath: String) { return when (storageScope) { StorageScope.APP -> FileData(filePath, accessFlag) + StorageScope.ASSETS -> AssetData(context, filePath, accessFlag) + StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { MediaStoreData(context, filePath, accessFlag) } else { @@ -74,7 +107,13 @@ internal abstract class DataAccess(private val filePath: String) { fun fileExists(storageScope: StorageScope, context: Context, path: String): Boolean { return when(storageScope) { StorageScope.APP -> FileData.fileExists(path) - StorageScope.SHARED -> MediaStoreData.fileExists(context, path) + StorageScope.ASSETS -> AssetData.fileExists(context, path) + StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStoreData.fileExists(context, path) + } else { + false + } + StorageScope.UNKNOWN -> false } } @@ -82,7 +121,13 @@ internal abstract class DataAccess(private val filePath: String) { fun fileLastModified(storageScope: StorageScope, context: Context, path: String): Long { return when(storageScope) { StorageScope.APP -> FileData.fileLastModified(path) - StorageScope.SHARED -> MediaStoreData.fileLastModified(context, path) + StorageScope.ASSETS -> AssetData.fileLastModified(path) + StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStoreData.fileLastModified(context, path) + } else { + 0L + } + StorageScope.UNKNOWN -> 0L } } @@ -90,7 +135,13 @@ internal abstract class DataAccess(private val filePath: String) { fun removeFile(storageScope: StorageScope, context: Context, path: String): Boolean { return when(storageScope) { StorageScope.APP -> FileData.delete(path) - StorageScope.SHARED -> MediaStoreData.delete(context, path) + StorageScope.ASSETS -> AssetData.delete(path) + StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStoreData.delete(context, path) + } else { + false + } + StorageScope.UNKNOWN -> false } } @@ -98,103 +149,120 @@ internal abstract class DataAccess(private val filePath: String) { fun renameFile(storageScope: StorageScope, context: Context, from: String, to: String): Boolean { return when(storageScope) { StorageScope.APP -> FileData.rename(from, to) - StorageScope.SHARED -> MediaStoreData.rename(context, from, to) + StorageScope.ASSETS -> AssetData.rename(from, to) + StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStoreData.rename(context, from, to) + } else { + false + } + StorageScope.UNKNOWN -> false } } } - protected abstract val fileChannel: FileChannel internal var endOfFile = false + abstract fun close() + abstract fun flush() + abstract fun seek(position: Long) + abstract fun resize(length: Long): Error + abstract fun position(): Long + abstract fun size(): Long + abstract fun read(buffer: ByteBuffer): Int + abstract fun write(buffer: ByteBuffer) - fun close() { - try { - fileChannel.close() - } catch (e: IOException) { - Log.w(TAG, "Exception when closing file $filePath.", e) - } + fun seekFromEnd(positionFromEnd: Long) { + val positionFromBeginning = max(0, size() - positionFromEnd) + seek(positionFromBeginning) } - fun flush() { - try { - fileChannel.force(false) - } catch (e: IOException) { - Log.w(TAG, "Exception when flushing file $filePath.", e) + abstract class FileChannelDataAccess(private val filePath: String) : DataAccess() { + internal abstract val fileChannel: FileChannel + + override fun close() { + try { + fileChannel.close() + } catch (e: IOException) { + Log.w(TAG, "Exception when closing file $filePath.", e) + } } - } - fun seek(position: Long) { - try { - fileChannel.position(position) - endOfFile = position >= fileChannel.size() - } catch (e: Exception) { - Log.w(TAG, "Exception when seeking file $filePath.", e) + override fun flush() { + try { + fileChannel.force(false) + } catch (e: IOException) { + Log.w(TAG, "Exception when flushing file $filePath.", e) + } } - } - fun seekFromEnd(positionFromEnd: Long) { - val positionFromBeginning = max(0, size() - positionFromEnd) - seek(positionFromBeginning) - } + override fun seek(position: Long) { + try { + fileChannel.position(position) + endOfFile = position >= fileChannel.size() + } catch (e: Exception) { + Log.w(TAG, "Exception when seeking file $filePath.", e) + } + } - 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 + override fun resize(length: Long): Error { + return try { + fileChannel.truncate(length) + Error.OK + } catch (e: NonWritableChannelException) { + Error.ERR_FILE_CANT_OPEN + } catch (e: ClosedChannelException) { + Error.ERR_FILE_CANT_OPEN + } catch (e: IllegalArgumentException) { + Error.ERR_INVALID_PARAMETER + } catch (e: IOException) { + Error.FAILED + } } - } - fun position(): Long { - return try { - fileChannel.position() + override fun position(): Long { + return try { + fileChannel.position() + } catch (e: IOException) { + Log.w( + TAG, + "Exception when retrieving position for file $filePath.", + e + ) + 0L + } + } + + override fun size() = try { + fileChannel.size() } catch (e: IOException) { - Log.w( - TAG, - "Exception when retrieving position for file $filePath.", - e - ) + Log.w(TAG, "Exception when retrieving size for file $filePath.", e) 0L } - } - - fun size() = try { - fileChannel.size() - } catch (e: IOException) { - Log.w(TAG, "Exception when retrieving size for file $filePath.", e) - 0L - } - fun read(buffer: ByteBuffer): Int { - return try { - val readBytes = fileChannel.read(buffer) - endOfFile = readBytes == -1 || (fileChannel.position() >= fileChannel.size()) - if (readBytes == -1) { + override fun read(buffer: ByteBuffer): Int { + return try { + val readBytes = fileChannel.read(buffer) + endOfFile = readBytes == -1 || (fileChannel.position() >= fileChannel.size()) + if (readBytes == -1) { + 0 + } else { + readBytes + } + } catch (e: IOException) { + Log.w(TAG, "Exception while reading from file $filePath.", e) 0 - } else { - readBytes } - } catch (e: IOException) { - Log.w(TAG, "Exception while reading from file $filePath.", e) - 0 } - } - fun write(buffer: ByteBuffer) { - try { - val writtenBytes = fileChannel.write(buffer) - if (writtenBytes > 0) { - endOfFile = false + override fun write(buffer: ByteBuffer) { + try { + val writtenBytes = fileChannel.write(buffer) + if (writtenBytes > 0) { + endOfFile = false + } + } catch (e: IOException) { + Log.w(TAG, "Exception while writing to file $filePath.", e) } - } catch (e: IOException) { - Log.w(TAG, "Exception while writing to file $filePath.", e) } } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt index 38974af753..f81127e90a 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt @@ -76,7 +76,7 @@ internal enum class FileAccessFlags(val nativeValue: Int) { companion object { fun fromNativeModeFlags(modeFlag: Int): FileAccessFlags? { - for (flag in values()) { + for (flag in entries) { if (flag.nativeValue == modeFlag) { return flag } 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 1d773467e8..dee7aebdc3 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 @@ -33,8 +33,11 @@ package org.godotengine.godot.io.file import android.content.Context import android.util.Log import android.util.SparseArray +import org.godotengine.godot.error.Error import org.godotengine.godot.io.StorageScope import java.io.FileNotFoundException +import java.io.InputStream +import java.lang.UnsupportedOperationException import java.nio.ByteBuffer /** @@ -45,8 +48,20 @@ class FileAccessHandler(val context: Context) { companion object { private val TAG = FileAccessHandler::class.java.simpleName - internal const val INVALID_FILE_ID = 0 + private const val INVALID_FILE_ID = 0 private const val STARTING_FILE_ID = 1 + private val FILE_OPEN_FAILED = Pair(Error.FAILED, INVALID_FILE_ID) + + internal fun getInputStream(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): InputStream? { + val storageScope = storageScopeIdentifier.identifyStorageScope(path) + return try { + path?.let { + DataAccess.getInputStream(storageScope, context, path) + } + } catch (e: Exception) { + null + } + } internal fun fileExists(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean { val storageScope = storageScopeIdentifier.identifyStorageScope(path) @@ -92,35 +107,55 @@ class FileAccessHandler(val context: Context) { } } - private val storageScopeIdentifier = StorageScope.Identifier(context) + internal val storageScopeIdentifier = StorageScope.Identifier(context) private val files = SparseArray<DataAccess>() private var lastFileId = STARTING_FILE_ID private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0 + fun canAccess(filePath: String?): Boolean { + return storageScopeIdentifier.canAccess(filePath) + } + + /** + * Returns a positive (> 0) file id when the operation succeeds. + * Otherwise, returns a negative value of [Error]. + */ fun fileOpen(path: String?, modeFlags: Int): Int { - val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID - return fileOpen(path, accessFlag) + val (fileError, fileId) = fileOpen(path, FileAccessFlags.fromNativeModeFlags(modeFlags)) + return if (fileError == Error.OK) { + fileId + } else { + // Return the negative of the [Error#toNativeValue()] value to differentiate from the + // positive file id. + -fileError.toNativeValue() + } } - internal fun fileOpen(path: String?, accessFlag: FileAccessFlags): Int { + internal fun fileOpen(path: String?, accessFlag: FileAccessFlags?): Pair<Error, Int> { + if (accessFlag == null) { + return FILE_OPEN_FAILED + } + val storageScope = storageScopeIdentifier.identifyStorageScope(path) if (storageScope == StorageScope.UNKNOWN) { - return INVALID_FILE_ID + return FILE_OPEN_FAILED } return try { path?.let { - val dataAccess = DataAccess.generateDataAccess(storageScope, context, it, accessFlag) ?: return INVALID_FILE_ID + val dataAccess = DataAccess.generateDataAccess(storageScope, context, it, accessFlag) ?: return FILE_OPEN_FAILED files.put(++lastFileId, dataAccess) - lastFileId - } ?: INVALID_FILE_ID + Pair(Error.OK, lastFileId) + } ?: FILE_OPEN_FAILED } catch (e: FileNotFoundException) { - FileErrors.FILE_NOT_FOUND.nativeValue + Pair(Error.ERR_FILE_NOT_FOUND, INVALID_FILE_ID) + } catch (e: UnsupportedOperationException) { + Pair(Error.ERR_UNAVAILABLE, INVALID_FILE_ID) } catch (e: Exception) { Log.w(TAG, "Error while opening $path", e) - INVALID_FILE_ID + FILE_OPEN_FAILED } } @@ -172,6 +207,10 @@ class FileAccessHandler(val context: Context) { files[fileId].flush() } + fun getInputStream(path: String?) = Companion.getInputStream(context, storageScopeIdentifier, path) + + fun renameFile(from: String, to: String) = Companion.renameFile(context, storageScopeIdentifier, from, to) + fun fileExists(path: String?) = Companion.fileExists(context, storageScopeIdentifier, path) fun fileLastModified(filepath: String?): Long { @@ -191,10 +230,10 @@ class FileAccessHandler(val context: Context) { fun fileResize(fileId: Int, length: Long): Int { if (!hasFileId(fileId)) { - return FileErrors.FAILED.nativeValue + return Error.FAILED.toNativeValue() } - return files[fileId].resize(length) + return files[fileId].resize(length).toNativeValue() } fun fileGetPosition(fileId: Int): Long { diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt index f2c0577c21..873daada3c 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt @@ -38,7 +38,7 @@ import java.nio.channels.FileChannel /** * Implementation of [DataAccess] which handles regular (not scoped) file access and interactions. */ -internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAccess(filePath) { +internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAccess.FileChannelDataAccess(filePath) { companion object { private val TAG = FileData::class.java.simpleName @@ -53,7 +53,7 @@ internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAcc fun fileLastModified(filepath: String): Long { return try { - File(filepath).lastModified() + File(filepath).lastModified() / 1000L } catch (e: SecurityException) { 0L } @@ -80,10 +80,16 @@ internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAcc override val fileChannel: FileChannel init { - if (accessFlag == FileAccessFlags.WRITE) { - fileChannel = FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel + fileChannel = if (accessFlag == FileAccessFlags.WRITE) { + // Create parent directory is necessary + val parentDir = File(filePath).parentFile + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs() + } + + FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel } else { - fileChannel = RandomAccessFile(filePath, accessFlag.getMode()).channel + RandomAccessFile(filePath, accessFlag.getMode()).channel } if (accessFlag.shouldTruncate()) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt deleted file mode 100644 index 2df0195de7..0000000000 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt +++ /dev/null @@ -1,53 +0,0 @@ -/**************************************************************************/ -/* FileErrors.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.io.file - -/** - * Set of errors that may occur when performing data access. - */ -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/io/file/MediaStoreData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt index 5410eed727..97362e2542 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt @@ -52,7 +52,7 @@ import java.nio.channels.FileChannel */ @RequiresApi(Build.VERSION_CODES.Q) internal class MediaStoreData(context: Context, filePath: String, accessFlag: FileAccessFlags) : - DataAccess(filePath) { + DataAccess.FileChannelDataAccess(filePath) { private data class DataItem( val id: Long, @@ -203,7 +203,7 @@ internal class MediaStoreData(context: Context, filePath: String, accessFlag: Fi } val dataItem = result[0] - return dataItem.dateModified.toLong() + return dataItem.dateModified.toLong() / 1000L } fun rename(context: Context, from: String, to: String): Boolean { diff --git a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java index 711bca02e7..8976dd65db 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java +++ b/platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java @@ -43,6 +43,7 @@ import androidx.annotation.Nullable; import java.lang.reflect.Constructor; import java.util.Collection; +import java.util.Collections; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -82,6 +83,9 @@ public final class GodotPluginRegistry { * Retrieve the full set of loaded plugins. */ public Collection<GodotPlugin> getAllPlugins() { + if (registry.isEmpty()) { + return Collections.emptyList(); + } return registry.values(); } diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt index 69748c0a8d..738f27e877 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt @@ -37,6 +37,7 @@ import android.os.SystemClock import android.os.Trace import android.util.Log import org.godotengine.godot.BuildConfig +import org.godotengine.godot.error.Error import org.godotengine.godot.io.file.FileAccessFlags import org.godotengine.godot.io.file.FileAccessHandler import org.json.JSONObject @@ -81,7 +82,8 @@ fun beginBenchmarkMeasure(scope: String, label: String) { * * * Note: Only enabled on 'editorDev' build variant. */ -fun endBenchmarkMeasure(scope: String, label: String) { +@JvmOverloads +fun endBenchmarkMeasure(scope: String, label: String, dumpBenchmark: Boolean = false) { if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { return } @@ -93,6 +95,10 @@ fun endBenchmarkMeasure(scope: String, label: String) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { Trace.endAsyncSection("[$scope] $label", 0) } + + if (dumpBenchmark) { + dumpBenchmark() + } } /** @@ -102,11 +108,11 @@ fun endBenchmarkMeasure(scope: String, label: String) { * * Note: Only enabled on 'editorDev' build variant. */ @JvmOverloads -fun dumpBenchmark(fileAccessHandler: FileAccessHandler?, filepath: String? = benchmarkFile) { +fun dumpBenchmark(fileAccessHandler: FileAccessHandler? = null, filepath: String? = benchmarkFile) { if (BuildConfig.FLAVOR != "editor" || BuildConfig.BUILD_TYPE != "dev") { return } - if (!useBenchmark) { + if (!useBenchmark || benchmarkTracker.isEmpty()) { return } @@ -123,8 +129,8 @@ fun dumpBenchmark(fileAccessHandler: FileAccessHandler?, filepath: String? = ben Log.i(TAG, "BENCHMARK:\n$printOut") if (fileAccessHandler != null && !filepath.isNullOrBlank()) { - val fileId = fileAccessHandler.fileOpen(filepath, FileAccessFlags.WRITE) - if (fileId != FileAccessHandler.INVALID_FILE_ID) { + val (fileError, fileId) = fileAccessHandler.fileOpen(filepath, FileAccessFlags.WRITE) + if (fileError == Error.OK) { val jsonOutput = JSONObject(benchmarkTracker.toMap()).toString(4) fileAccessHandler.fileWrite(fileId, ByteBuffer.wrap(jsonOutput.toByteArray())) fileAccessHandler.fileClose(fileId) diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java index b1bce45fbb..d9afdf90b1 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java @@ -24,6 +24,7 @@ package org.godotengine.godot.utils; import android.app.Activity; import android.app.ActivityManager; +import android.app.ActivityOptions; import android.content.Context; import android.content.Intent; import android.os.Bundle; @@ -44,6 +45,9 @@ import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; */ public final class ProcessPhoenix extends Activity { private static final String KEY_RESTART_INTENTS = "phoenix_restart_intents"; + // -- GODOT start -- + private static final String KEY_RESTART_ACTIVITY_OPTIONS = "phoenix_restart_activity_options"; + // -- GODOT end -- private static final String KEY_MAIN_PROCESS_PID = "phoenix_main_process_pid"; /** @@ -56,12 +60,23 @@ public final class ProcessPhoenix extends Activity { triggerRebirth(context, getRestartIntent(context)); } + // -- GODOT start -- /** * Call to restart the application process using the specified intents. * <p> * Behavior of the current process after invoking this method is undefined. */ public static void triggerRebirth(Context context, Intent... nextIntents) { + triggerRebirth(context, null, nextIntents); + } + + /** + * Call to restart the application process using the specified intents launched with the given + * {@link ActivityOptions}. + * <p> + * Behavior of the current process after invoking this method is undefined. + */ + public static void triggerRebirth(Context context, Bundle activityOptions, Intent... nextIntents) { if (nextIntents.length < 1) { throw new IllegalArgumentException("intents cannot be empty"); } @@ -72,10 +87,12 @@ public final class ProcessPhoenix extends Activity { intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context. intent.putParcelableArrayListExtra(KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents))); intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid()); + if (activityOptions != null) { + intent.putExtra(KEY_RESTART_ACTIVITY_OPTIONS, activityOptions); + } context.startActivity(intent); } - // -- GODOT start -- /** * Finish the activity and kill its process */ @@ -112,9 +129,11 @@ public final class ProcessPhoenix extends Activity { super.onCreate(savedInstanceState); // -- GODOT start -- - ArrayList<Intent> intents = getIntent().getParcelableArrayListExtra(KEY_RESTART_INTENTS); - startActivities(intents.toArray(new Intent[intents.size()])); - forceQuit(this, getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1)); + Intent launchIntent = getIntent(); + ArrayList<Intent> intents = launchIntent.getParcelableArrayListExtra(KEY_RESTART_INTENTS); + Bundle activityOptions = launchIntent.getBundleExtra(KEY_RESTART_ACTIVITY_OPTIONS); + startActivities(intents.toArray(new Intent[intents.size()]), activityOptions); + forceQuit(this, launchIntent.getIntExtra(KEY_MAIN_PROCESS_PID, -1)); // -- GODOT end -- } diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt index 6f09f51d4c..a93a7dbe09 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt @@ -31,11 +31,9 @@ @file:JvmName("VkRenderer") package org.godotengine.godot.vulkan +import android.util.Log import android.view.Surface - -import org.godotengine.godot.Godot import org.godotengine.godot.GodotLib -import org.godotengine.godot.plugin.GodotPlugin import org.godotengine.godot.plugin.GodotPluginRegistry /** @@ -52,6 +50,11 @@ import org.godotengine.godot.plugin.GodotPluginRegistry * @see [VkSurfaceView.startRenderer] */ internal class VkRenderer { + + companion object { + private val TAG = VkRenderer::class.java.simpleName + } + private val pluginRegistry: GodotPluginRegistry = GodotPluginRegistry.getPluginRegistry() /** @@ -101,8 +104,10 @@ internal class VkRenderer { } /** - * Called when the rendering thread is destroyed and used as signal to tear down the Vulkan logic. + * Invoked when the render thread is in the process of shutting down. */ - fun onVkDestroy() { + fun onRenderThreadExiting() { + Log.d(TAG, "Destroying Godot Engine") + GodotLib.ondestroy() } } diff --git a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt index 791b425444..9e30de6a15 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt +++ b/platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt @@ -113,12 +113,10 @@ open internal class VkSurfaceView(context: Context) : SurfaceView(context), Surf } /** - * Tear down the rendering thread. - * - * Must not be called before a [VkRenderer] has been set. + * Requests the render thread to exit and block until it does. */ - fun onDestroy() { - vkThread.blockingExit() + fun requestRenderThreadExitAndWait() { + vkThread.requestExitAndWait() } override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { 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 8c0065b31e..c7cb97d911 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 @@ -75,6 +75,9 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk private fun threadExiting() { lock.withLock { + Log.d(TAG, "Exiting render thread") + vkRenderer.onRenderThreadExiting() + exited = true lockCondition.signalAll() } @@ -93,7 +96,7 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk /** * Request the thread to exit and block until it's done. */ - fun blockingExit() { + fun requestExitAndWait() { lock.withLock { shouldExit = true lockCondition.signalAll() @@ -171,7 +174,6 @@ internal class VkThread(private val vkSurfaceView: VkSurfaceView, private val vk while (true) { // Code path for exiting the thread loop. if (shouldExit) { - vkRenderer.onVkDestroy() return } |