2
0
Эх сурвалжийг харах

Update the storage access handler logic to support accessing / retrieving contents with the `assets:/` prefix

Fredia Huya-Kouadio 1 жил өмнө
parent
commit
794ea99240
20 өөрчлөгдсөн 667 нэмэгдсэн , 265 устгасан
  1. 1 0
      core/error/error_list.h
  2. 8 8
      platform/android/dir_access_jandroid.cpp
  3. 4 22
      platform/android/file_access_filesystem_jandroid.cpp
  4. 6 6
      platform/android/java/lib/src/org/godotengine/godot/Godot.kt
  5. 5 0
      platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
  6. 100 0
      platform/android/java/lib/src/org/godotengine/godot/error/Error.kt
  7. 19 2
      platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt
  8. 20 27
      platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt
  9. 138 48
      platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt
  10. 3 3
      platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt
  11. 151 0
      platform/android/java/lib/src/org/godotengine/godot/io/file/AssetData.kt
  12. 144 76
      platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt
  13. 1 1
      platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt
  14. 47 12
      platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt
  15. 10 4
      platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt
  16. 0 53
      platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt
  17. 1 1
      platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt
  18. 3 2
      platform/android/java/lib/src/org/godotengine/godot/utils/BenchmarkUtils.kt
  19. 5 0
      platform/android/java_godot_lib_jni.cpp
  20. 1 0
      platform/android/java_godot_lib_jni.h

+ 1 - 0
core/error/error_list.h

@@ -41,6 +41,7 @@
  * - Are added to the Error enum in core/error/error_list.h
  * - Have a description added to error_names in core/error/error_list.cpp
  * - Are bound with BIND_CORE_ENUM_CONSTANT() in core/core_constants.cpp
+ * - Have a matching Android version in platform/android/java/lib/src/org/godotengine/godot/error/Error.kt
  */
 
 enum Error {

+ 8 - 8
platform/android/dir_access_jandroid.cpp

@@ -68,7 +68,7 @@ String DirAccessJAndroid::get_next() {
 	if (_dir_next) {
 		JNIEnv *env = get_jni_env();
 		ERR_FAIL_NULL_V(env, "");
-		jstring str = (jstring)env->CallObjectMethod(dir_access_handler, _dir_next, get_access_type(), id);
+		jstring str = (jstring)env->CallObjectMethod(dir_access_handler, _dir_next, id);
 		if (!str) {
 			return "";
 		}
@@ -85,7 +85,7 @@ bool DirAccessJAndroid::current_is_dir() const {
 	if (_dir_is_dir) {
 		JNIEnv *env = get_jni_env();
 		ERR_FAIL_NULL_V(env, false);
-		return env->CallBooleanMethod(dir_access_handler, _dir_is_dir, get_access_type(), id);
+		return env->CallBooleanMethod(dir_access_handler, _dir_is_dir, id);
 	} else {
 		return false;
 	}
@@ -95,7 +95,7 @@ bool DirAccessJAndroid::current_is_hidden() const {
 	if (_current_is_hidden) {
 		JNIEnv *env = get_jni_env();
 		ERR_FAIL_NULL_V(env, false);
-		return env->CallBooleanMethod(dir_access_handler, _current_is_hidden, get_access_type(), id);
+		return env->CallBooleanMethod(dir_access_handler, _current_is_hidden, id);
 	}
 	return false;
 }
@@ -307,9 +307,9 @@ void DirAccessJAndroid::setup(jobject p_dir_access_handler) {
 	cls = (jclass)env->NewGlobalRef(c);
 
 	_dir_open = env->GetMethodID(cls, "dirOpen", "(ILjava/lang/String;)I");
-	_dir_next = env->GetMethodID(cls, "dirNext", "(II)Ljava/lang/String;");
-	_dir_close = env->GetMethodID(cls, "dirClose", "(II)V");
-	_dir_is_dir = env->GetMethodID(cls, "dirIsDir", "(II)Z");
+	_dir_next = env->GetMethodID(cls, "dirNext", "(I)Ljava/lang/String;");
+	_dir_close = env->GetMethodID(cls, "dirClose", "(I)V");
+	_dir_is_dir = env->GetMethodID(cls, "dirIsDir", "(I)Z");
 	_dir_exists = env->GetMethodID(cls, "dirExists", "(ILjava/lang/String;)Z");
 	_file_exists = env->GetMethodID(cls, "fileExists", "(ILjava/lang/String;)Z");
 	_get_drive_count = env->GetMethodID(cls, "getDriveCount", "(I)I");
@@ -318,7 +318,7 @@ void DirAccessJAndroid::setup(jobject p_dir_access_handler) {
 	_get_space_left = env->GetMethodID(cls, "getSpaceLeft", "(I)J");
 	_rename = env->GetMethodID(cls, "rename", "(ILjava/lang/String;Ljava/lang/String;)Z");
 	_remove = env->GetMethodID(cls, "remove", "(ILjava/lang/String;)Z");
-	_current_is_hidden = env->GetMethodID(cls, "isCurrentHidden", "(II)Z");
+	_current_is_hidden = env->GetMethodID(cls, "isCurrentHidden", "(I)Z");
 }
 
 void DirAccessJAndroid::terminate() {
@@ -355,6 +355,6 @@ void DirAccessJAndroid::dir_close(int p_id) {
 	if (_dir_close) {
 		JNIEnv *env = get_jni_env();
 		ERR_FAIL_NULL(env);
-		env->CallVoidMethod(dir_access_handler, _dir_close, get_access_type(), p_id);
+		env->CallVoidMethod(dir_access_handler, _dir_close, p_id);
 	}
 }

+ 4 - 22
platform/android/file_access_filesystem_jandroid.cpp

@@ -77,15 +77,9 @@ Error FileAccessFilesystemJAndroid::open_internal(const String &p_path, int p_mo
 		int res = env->CallIntMethod(file_access_handler, _file_open, js, p_mode_flags);
 		env->DeleteLocalRef(js);
 
-		if (res <= 0) {
-			switch (res) {
-				case 0:
-				default:
-					return ERR_FILE_CANT_OPEN;
-
-				case -2:
-					return ERR_FILE_NOT_FOUND;
-			}
+		if (res < 0) {
+			// Errors are passed back as their negative value to differentiate from the positive file id.
+			return static_cast<Error>(-res);
 		}
 
 		id = res;
@@ -331,19 +325,7 @@ Error FileAccessFilesystemJAndroid::resize(int64_t p_length) {
 		ERR_FAIL_NULL_V(env, FAILED);
 		ERR_FAIL_COND_V_MSG(!is_open(), FAILED, "File must be opened before use.");
 		int res = env->CallIntMethod(file_access_handler, _file_resize, id, p_length);
-		switch (res) {
-			case 0:
-				return OK;
-			case -4:
-				return ERR_INVALID_PARAMETER;
-			case -3:
-				return ERR_FILE_CANT_OPEN;
-			case -2:
-				return ERR_FILE_NOT_FOUND;
-			case -1:
-			default:
-				return FAILED;
-		}
+		return static_cast<Error>(res);
 	} else {
 		return ERR_UNAVAILABLE;
 	}

+ 6 - 6
platform/android/java/lib/src/org/godotengine/godot/Godot.kt

@@ -83,12 +83,17 @@ import java.util.concurrent.atomic.AtomicReference
  */
 class Godot(private val context: Context) {
 
-	private companion object {
+	internal companion object {
 		private val TAG = Godot::class.java.simpleName
 
 		// 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 windowManager: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
@@ -834,11 +839,6 @@ class Godot(private val context: Context) {
 		return mClipboard.hasPrimaryClip()
 	}
 
-	/**
-	 * @return true if this is an editor build, false if this is a template build
-	 */
-	fun isEditorBuild() = BuildConfig.FLAVOR == EDITOR_FLAVOR
-
 	fun getClipboard(): String {
 		val clipData = mClipboard.primaryClip ?: return ""
 		val text = clipData.getItemAt(0).text ?: return ""

+ 5 - 0
platform/android/java/lib/src/org/godotengine/godot/GodotLib.java

@@ -246,4 +246,9 @@ public class GodotLib {
 	 * dispatched from the UI thread.
 	 */
 	public static native boolean shouldDispatchInputToRenderThread();
+
+	/**
+	 * @return the project resource directory
+	 */
+	public static native String getProjectResourceDir();
 }

+ 100 - 0
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
+	}
+}

+ 19 - 2
platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt

@@ -34,11 +34,17 @@ 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.
 	 */
@@ -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
@@ -71,9 +81,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

+ 20 - 27
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)
 }

+ 138 - 48
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)
 		}
 	}
 

+ 3 - 3
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

+ 151 - 0
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.")
+	}
+}

+ 144 - 76
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)
 		}
 	}
 }

+ 1 - 1
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
                 }

+ 47 - 12
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)
@@ -98,29 +113,45 @@ class FileAccessHandler(val context: Context) {
 
 	private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0
 
+	/**
+	 * 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 +203,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 +226,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 {

+ 10 - 4
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
@@ -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()) {

+ 0 - 53
platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt

@@ -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
-		}
-	}
-}

+ 1 - 1
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,

+ 3 - 2
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
@@ -128,8 +129,8 @@ fun dumpBenchmark(fileAccessHandler: FileAccessHandler? = null, filepath: String
 	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)

+ 5 - 0
platform/android/java_godot_lib_jni.cpp

@@ -574,4 +574,9 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInp
 	}
 	return false;
 }
+
+JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getProjectResourceDir(JNIEnv *env, jclass clazz) {
+	const String resource_dir = OS::get_singleton()->get_resource_dir();
+	return env->NewStringUTF(resource_dir.utf8().get_data());
+}
 }

+ 1 - 0
platform/android/java_godot_lib_jni.h

@@ -70,6 +70,7 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JN
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNIEnv *env, jclass clazz);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz);
 JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInputToRenderThread(JNIEnv *env, jclass clazz);
+JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getProjectResourceDir(JNIEnv *env, jclass clazz);
 }
 
 #endif // JAVA_GODOT_LIB_JNI_H