From 794ea99240607a7afbc1512582083db82d81ff83 Mon Sep 17 00:00:00 2001 From: Fredia Huya-Kouadio Date: Thu, 7 Mar 2024 19:16:25 -0800 Subject: [PATCH] Update the storage access handler logic to support accessing / retrieving contents with the `assets:/` prefix --- core/error/error_list.h | 1 + platform/android/dir_access_jandroid.cpp | 16 +- .../file_access_filesystem_jandroid.cpp | 26 +- .../lib/src/org/godotengine/godot/Godot.kt | 12 +- .../src/org/godotengine/godot/GodotLib.java | 5 + .../src/org/godotengine/godot/error/Error.kt | 100 ++++++++ .../org/godotengine/godot/io/StorageScope.kt | 21 +- .../io/directory/AssetsDirectoryAccess.kt | 47 ++-- .../io/directory/DirectoryAccessHandler.kt | 186 ++++++++++---- .../io/directory/FilesystemDirectoryAccess.kt | 6 +- .../godotengine/godot/io/file/AssetData.kt | 151 ++++++++++++ .../godotengine/godot/io/file/DataAccess.kt | 226 ++++++++++++------ .../godot/io/file/FileAccessFlags.kt | 2 +- .../godot/io/file/FileAccessHandler.kt | 59 ++++- .../org/godotengine/godot/io/file/FileData.kt | 14 +- .../godotengine/godot/io/file/FileErrors.kt | 53 ---- .../godot/io/file/MediaStoreData.kt | 2 +- .../godotengine/godot/utils/BenchmarkUtils.kt | 5 +- platform/android/java_godot_lib_jni.cpp | 5 + platform/android/java_godot_lib_jni.h | 1 + 20 files changed, 670 insertions(+), 268 deletions(-) create mode 100644 platform/android/java/lib/src/org/godotengine/godot/error/Error.kt create mode 100644 platform/android/java/lib/src/org/godotengine/godot/io/file/AssetData.kt delete mode 100644 platform/android/java/lib/src/org/godotengine/godot/io/file/FileErrors.kt diff --git a/core/error/error_list.h b/core/error/error_list.h index abc637106a5..cdf06eb06d7 100644 --- a/core/error/error_list.h +++ b/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 { diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp index ab90527bfaf..56d41f2ab72 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/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); } } diff --git a/platform/android/file_access_filesystem_jandroid.cpp b/platform/android/file_access_filesystem_jandroid.cpp index f28d469d07e..9ae48dfb102 100644 --- a/platform/android/file_access_filesystem_jandroid.cpp +++ b/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(-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(res); } else { return ERR_UNAVAILABLE; } 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 111cd484050..1e0027089cf 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt +++ b/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 "" 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 909daf05c97..295a4a63400 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/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(); } 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 00000000000..00ef5ee3413 --- /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/io/StorageScope.kt b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt index 8ee3d5f48f7..2e5649b563e 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,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 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 b9b7ebac6e0..523e852518e 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, var current: Int = 0) @@ -54,13 +66,6 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler. private var lastDirId = STARTING_DIR_ID private val dirs = SparseArray() - 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 dd6d5180c55..9f3461200b7 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 { + 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 c8b4f79f309..2830216e12b 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, 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() @@ -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 00000000000..1ab739d90bd --- /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 11cf7b3566d..73f020f2495 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 - - fun close() { - try { - fileChannel.close() - } catch (e: IOException) { - Log.w(TAG, "Exception when closing file $filePath.", e) - } - } - - fun flush() { - try { - fileChannel.force(false) - } catch (e: IOException) { - Log.w(TAG, "Exception when flushing 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) - } - } + 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 seekFromEnd(positionFromEnd: Long) { val positionFromBeginning = max(0, size() - positionFromEnd) seek(positionFromBeginning) } - fun resize(length: Long): Int { - return try { - fileChannel.truncate(length) - FileErrors.OK.nativeValue - } catch (e: NonWritableChannelException) { - FileErrors.FILE_CANT_OPEN.nativeValue - } catch (e: ClosedChannelException) { - FileErrors.FILE_CANT_OPEN.nativeValue - } catch (e: IllegalArgumentException) { - FileErrors.INVALID_PARAMETER.nativeValue - } catch (e: IOException) { - FileErrors.FAILED.nativeValue - } - } + abstract class FileChannelDataAccess(private val filePath: String) : DataAccess() { + internal abstract val fileChannel: FileChannel - fun position(): Long { - return try { - fileChannel.position() + override fun close() { + try { + fileChannel.close() + } catch (e: IOException) { + Log.w(TAG, "Exception when closing file $filePath.", e) + } + } + + override fun flush() { + try { + fileChannel.force(false) + } catch (e: IOException) { + Log.w(TAG, "Exception when flushing file $filePath.", e) + } + } + + 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) + } + } + + 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 + } + } + + 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 38974af7533..f81127e90ae 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 1d773467e87..5d57052ce6a 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) @@ -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 { + 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 { 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 d0b8a8dffae..873daada3cf 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 @@ -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 2df0195de7f..00000000000 --- 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 146fc04da49..97362e2542b 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, 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 d39f2309b80..738f27e877c 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 @@ -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) diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index a4a425f685a..1114969de81 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/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()); +} } diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index d027da31fa4..2165ce264bb 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/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