summaryrefslogtreecommitdiffstats
path: root/platform/android/java/lib/src
diff options
context:
space:
mode:
Diffstat (limited to 'platform/android/java/lib/src')
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/Godot.kt318
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotActivity.kt14
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java33
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotGLRenderView.java16
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotHost.java28
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotIO.java2
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotLib.java11
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotRenderView.java7
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/GodotVulkanRenderView.java15
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/error/Error.kt100
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/gl/GLSurfaceView.java69
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/gl/GodotRenderer.java10
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotGestureHandler.kt41
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotInputHandler.java241
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/GodotTextInputWrapper.java18
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/input/InputEventRunnable.java353
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt29
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt47
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt186
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt6
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/AssetData.kt151
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt220
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt2
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt65
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt16
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt53
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt4
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/plugin/GodotPluginRegistry.java4
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt16
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/utils/ProcessPhoenix.java27
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/vulkan/VkRenderer.kt15
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/vulkan/VkSurfaceView.kt8
-rw-r--r--platform/android/java/lib/src/org/godotengine/godot/vulkan/VkThread.kt6
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
}