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

This commit is contained in:
Fredia Huya-Kouadio 2024-03-07 19:16:25 -08:00 committed by Fredia Huya-Kouadio
parent e63c40e59c
commit 794ea99240
20 changed files with 670 additions and 268 deletions

View File

@ -41,6 +41,7 @@
* - Are added to the Error enum in core/error/error_list.h * - 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 * - 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 * - 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 { enum Error {

View File

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

View File

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

View File

@ -83,12 +83,17 @@ import java.util.concurrent.atomic.AtomicReference
*/ */
class Godot(private val context: Context) { class Godot(private val context: Context) {
private companion object { internal companion object {
private val TAG = Godot::class.java.simpleName private val TAG = Godot::class.java.simpleName
// Supported build flavors // Supported build flavors
const val EDITOR_FLAVOR = "editor" const val EDITOR_FLAVOR = "editor"
const val TEMPLATE_FLAVOR = "template" 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 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 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 { fun getClipboard(): String {
val clipData = mClipboard.primaryClip ?: return "" val clipData = mClipboard.primaryClip ?: return ""
val text = clipData.getItemAt(0).text ?: return "" val text = clipData.getItemAt(0).text ?: return ""

View File

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

View File

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

View File

@ -34,11 +34,17 @@ import android.content.Context
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import java.io.File import java.io.File
import org.godotengine.godot.GodotLib
/** /**
* Represents the different storage scopes. * Represents the different storage scopes.
*/ */
internal enum class StorageScope { internal enum class StorageScope {
/**
* Covers the 'assets' directory
*/
ASSETS,
/** /**
* Covers internal and external directories accessible to the app without restrictions. * Covers internal and external directories accessible to the app without restrictions.
*/ */
@ -56,6 +62,10 @@ internal enum class StorageScope {
class Identifier(context: Context) { class Identifier(context: Context) {
companion object {
internal const val ASSETS_PREFIX = "assets://"
}
private val internalAppDir: String? = context.filesDir.canonicalPath private val internalAppDir: String? = context.filesDir.canonicalPath
private val internalCacheDir: String? = context.cacheDir.canonicalPath private val internalCacheDir: String? = context.cacheDir.canonicalPath
private val externalAppDir: String? = context.getExternalFilesDir(null)?.canonicalPath private val externalAppDir: String? = context.getExternalFilesDir(null)?.canonicalPath
@ -71,9 +81,16 @@ internal enum class StorageScope {
return UNKNOWN return UNKNOWN
} }
val pathFile = File(path) if (path.startsWith(ASSETS_PREFIX)) {
return ASSETS
}
var pathFile = File(path)
if (!pathFile.isAbsolute) { 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 // If we have 'All Files Access' permission, we can access all directories without

View File

@ -33,18 +33,30 @@ package org.godotengine.godot.io.directory
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import android.util.SparseArray 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.INVALID_DIR_ID
import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_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.File
import java.io.IOException import java.io.IOException
/** /**
* Handles directories access within the Android assets directory. * 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 { companion object {
private val TAG = AssetsDirectoryAccess::class.java.simpleName 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) 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 var lastDirId = STARTING_DIR_ID
private val dirs = SparseArray<AssetDir>() 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 hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0
override fun dirOpen(path: String): Int { override fun dirOpen(path: String): Int {
@ -68,8 +73,8 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.
try { try {
val files = assetManager.list(assetsPath) ?: return INVALID_DIR_ID val files = assetManager.list(assetsPath) ?: return INVALID_DIR_ID
// Empty directories don't get added to the 'assets' directory, so // Empty directories don't get added to the 'assets' directory, so
// if ad.files.length > 0 ==> path is directory // if files.length > 0 ==> path is directory
// if ad.files.length == 0 ==> path is file // if files.length == 0 ==> path is file
if (files.isEmpty()) { if (files.isEmpty()) {
return INVALID_DIR_ID return INVALID_DIR_ID
} }
@ -89,8 +94,8 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.
try { try {
val files = assetManager.list(assetsPath) ?: return false val files = assetManager.list(assetsPath) ?: return false
// Empty directories don't get added to the 'assets' directory, so // Empty directories don't get added to the 'assets' directory, so
// if ad.files.length > 0 ==> path is directory // if files.length > 0 ==> path is directory
// if ad.files.length == 0 ==> path is file // if files.length == 0 ==> path is file
return files.isNotEmpty() return files.isNotEmpty()
} catch (e: IOException) { } catch (e: IOException) {
Log.e(TAG, "Exception on dirExists", e) Log.e(TAG, "Exception on dirExists", e)
@ -98,19 +103,7 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.
} }
} }
override fun fileExists(path: String): Boolean { override fun fileExists(path: String) = AssetData.fileExists(context, path)
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 dirIsDir(dirId: Int): Boolean { override fun dirIsDir(dirId: Int): Boolean {
val ad: AssetDir = dirs[dirId] val ad: AssetDir = dirs[dirId]
@ -171,7 +164,7 @@ internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.
override fun getSpaceLeft() = 0L 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)
} }

View File

@ -32,7 +32,8 @@ package org.godotengine.godot.io.directory
import android.content.Context import android.content.Context
import android.util.Log 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 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 INVALID_DIR_ID = -1
internal const val STARTING_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) { 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 { internal interface DirectoryAccess {
@ -76,8 +141,10 @@ class DirectoryAccessHandler(context: Context) {
fun remove(filename: String): Boolean fun remove(filename: String): Boolean
} }
private val storageScopeIdentifier = StorageScope.Identifier(context)
private val assetsDirAccess = AssetsDirectoryAccess(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 assetsFileExists(assetsPath: String) = assetsDirAccess.fileExists(assetsPath)
fun filesystemFileExists(path: String) = fileSystemDirAccess.fileExists(path) fun filesystemFileExists(path: String) = fileSystemDirAccess.fileExists(path)
@ -85,24 +152,32 @@ class DirectoryAccessHandler(context: Context) {
private fun hasDirId(accessType: AccessType, dirId: Int): Boolean { private fun hasDirId(accessType: AccessType, dirId: Int): Boolean {
return when (accessType) { return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId) ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.hasDirId(dirId) else -> fileSystemDirAccess.hasDirId(dirId)
} }
} }
fun dirOpen(nativeAccessType: Int, path: String?): Int { fun dirOpen(nativeAccessType: Int, path: String?): Int {
val accessType = getAccessTypeFromNative(nativeAccessType) if (path == null) {
if (path == null || accessType == null) {
return INVALID_DIR_ID 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_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 { fun dirNext(dirAccessId: Int): String {
val accessType = getAccessTypeFromNative(nativeAccessType) val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
if (accessType == null || !hasDirId(accessType, dirId)) { if (accessType == null || !hasDirId(accessType, dirId)) {
Log.w(TAG, "dirNext: Invalid dir id: $dirId") Log.w(TAG, "dirNext: Invalid dir id: $dirId")
return "" return ""
@ -110,12 +185,12 @@ class DirectoryAccessHandler(context: Context) {
return when (accessType) { return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirNext(dirId) ACCESS_RESOURCES -> assetsDirAccess.dirNext(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirNext(dirId) else -> fileSystemDirAccess.dirNext(dirId)
} }
} }
fun dirClose(nativeAccessType: Int, dirId: Int) { fun dirClose(dirAccessId: Int) {
val accessType = getAccessTypeFromNative(nativeAccessType) val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
if (accessType == null || !hasDirId(accessType, dirId)) { if (accessType == null || !hasDirId(accessType, dirId)) {
Log.w(TAG, "dirClose: Invalid dir id: $dirId") Log.w(TAG, "dirClose: Invalid dir id: $dirId")
return return
@ -123,12 +198,12 @@ class DirectoryAccessHandler(context: Context) {
when (accessType) { when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirClose(dirId) ACCESS_RESOURCES -> assetsDirAccess.dirClose(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirClose(dirId) else -> fileSystemDirAccess.dirClose(dirId)
} }
} }
fun dirIsDir(nativeAccessType: Int, dirId: Int): Boolean { fun dirIsDir(dirAccessId: Int): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType) val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
if (accessType == null || !hasDirId(accessType, dirId)) { if (accessType == null || !hasDirId(accessType, dirId)) {
Log.w(TAG, "dirIsDir: Invalid dir id: $dirId") Log.w(TAG, "dirIsDir: Invalid dir id: $dirId")
return false return false
@ -136,91 +211,106 @@ class DirectoryAccessHandler(context: Context) {
return when (accessType) { return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirIsDir(dirId) ACCESS_RESOURCES -> assetsDirAccess.dirIsDir(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirIsDir(dirId) else -> fileSystemDirAccess.dirIsDir(dirId)
} }
} }
fun isCurrentHidden(nativeAccessType: Int, dirId: Int): Boolean { fun isCurrentHidden(dirAccessId: Int): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType) val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
if (accessType == null || !hasDirId(accessType, dirId)) { if (accessType == null || !hasDirId(accessType, dirId)) {
return false return false
} }
return when (accessType) { return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.isCurrentHidden(dirId) ACCESS_RESOURCES -> assetsDirAccess.isCurrentHidden(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.isCurrentHidden(dirId) else -> fileSystemDirAccess.isCurrentHidden(dirId)
} }
} }
fun dirExists(nativeAccessType: Int, path: String?): Boolean { fun dirExists(nativeAccessType: Int, path: String?): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType) if (path == null) {
if (path == null || accessType == null) {
return false return false
} }
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
return when (accessType) { return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirExists(path) ACCESS_RESOURCES -> assetsDirAccess.dirExists(path)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirExists(path) else -> fileSystemDirAccess.dirExists(path)
} }
} }
fun fileExists(nativeAccessType: Int, path: String?): Boolean { fun fileExists(nativeAccessType: Int, path: String?): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType) if (path == null) {
if (path == null || accessType == null) {
return false return false
} }
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
return when (accessType) { return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.fileExists(path) ACCESS_RESOURCES -> assetsDirAccess.fileExists(path)
ACCESS_FILESYSTEM -> fileSystemDirAccess.fileExists(path) else -> fileSystemDirAccess.fileExists(path)
} }
} }
fun getDriveCount(nativeAccessType: Int): Int { fun getDriveCount(nativeAccessType: Int): Int {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0 val accessType = AccessType.fromNative(nativeAccessType) ?: return 0
return when(accessType) { return when(accessType) {
ACCESS_RESOURCES -> assetsDirAccess.getDriveCount() ACCESS_RESOURCES -> assetsDirAccess.getDriveCount()
ACCESS_FILESYSTEM -> fileSystemDirAccess.getDriveCount() else -> fileSystemDirAccess.getDriveCount()
} }
} }
fun getDrive(nativeAccessType: Int, drive: Int): String { fun getDrive(nativeAccessType: Int, drive: Int): String {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return "" val accessType = AccessType.fromNative(nativeAccessType) ?: return ""
return when (accessType) { return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.getDrive(drive) ACCESS_RESOURCES -> assetsDirAccess.getDrive(drive)
ACCESS_FILESYSTEM -> fileSystemDirAccess.getDrive(drive) else -> fileSystemDirAccess.getDrive(drive)
} }
} }
fun makeDir(nativeAccessType: Int, dir: String): Boolean { fun makeDir(nativeAccessType: Int, dir: String?): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false if (dir == null) {
return false
}
val storageScope = storageScopeIdentifier.identifyStorageScope(dir)
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
return when (accessType) { return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.makeDir(dir) ACCESS_RESOURCES -> assetsDirAccess.makeDir(dir)
ACCESS_FILESYSTEM -> fileSystemDirAccess.makeDir(dir) else -> fileSystemDirAccess.makeDir(dir)
} }
} }
fun getSpaceLeft(nativeAccessType: Int): Long { fun getSpaceLeft(nativeAccessType: Int): Long {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0L val accessType = AccessType.fromNative(nativeAccessType) ?: return 0L
return when (accessType) { return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.getSpaceLeft() ACCESS_RESOURCES -> assetsDirAccess.getSpaceLeft()
ACCESS_FILESYSTEM -> fileSystemDirAccess.getSpaceLeft() else -> fileSystemDirAccess.getSpaceLeft()
} }
} }
fun rename(nativeAccessType: Int, from: String, to: String): Boolean { 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) { return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.rename(from, to) ACCESS_RESOURCES -> assetsDirAccess.rename(from, to)
ACCESS_FILESYSTEM -> fileSystemDirAccess.rename(from, to) else -> fileSystemDirAccess.rename(from, to)
} }
} }
fun remove(nativeAccessType: Int, filename: String): Boolean { fun remove(nativeAccessType: Int, filename: String?): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false if (filename == null) {
return false
}
val storageScope = storageScopeIdentifier.identifyStorageScope(filename)
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
return when (accessType) { return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.remove(filename) ACCESS_RESOURCES -> assetsDirAccess.remove(filename)
ACCESS_FILESYSTEM -> fileSystemDirAccess.remove(filename) else -> fileSystemDirAccess.remove(filename)
} }
} }

View File

@ -45,7 +45,7 @@ import java.io.File
/** /**
* Handles directories access with the internal and external filesystem. * 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 { DirectoryAccessHandler.DirectoryAccess {
companion object { 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 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 val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
private var lastDirId = STARTING_DIR_ID private var lastDirId = STARTING_DIR_ID
private val dirs = SparseArray<DirData>() 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+ // Directory access is available for shared storage on Android 11+
// On Android 10, access is also available as long as the `requestLegacyExternalStorage` // On Android 10, access is also available as long as the `requestLegacyExternalStorage`
// tag is available. // 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 override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0

View File

@ -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.")
}
}

View File

@ -33,12 +33,17 @@ package org.godotengine.godot.io.file
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import org.godotengine.godot.error.Error
import org.godotengine.godot.io.StorageScope import org.godotengine.godot.io.StorageScope
import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.io.InputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.channels.Channels
import java.nio.channels.ClosedChannelException import java.nio.channels.ClosedChannelException
import java.nio.channels.FileChannel import java.nio.channels.FileChannel
import java.nio.channels.NonWritableChannelException import java.nio.channels.NonWritableChannelException
import kotlin.jvm.Throws
import kotlin.math.max 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 * 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. * 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 { companion object {
private val TAG = DataAccess::class.java.simpleName 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( fun generateDataAccess(
storageScope: StorageScope, storageScope: StorageScope,
context: Context, context: Context,
@ -61,6 +92,8 @@ internal abstract class DataAccess(private val filePath: String) {
return when (storageScope) { return when (storageScope) {
StorageScope.APP -> FileData(filePath, accessFlag) StorageScope.APP -> FileData(filePath, accessFlag)
StorageScope.ASSETS -> AssetData(context, filePath, accessFlag)
StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStoreData(context, filePath, accessFlag) MediaStoreData(context, filePath, accessFlag)
} else { } else {
@ -74,7 +107,13 @@ internal abstract class DataAccess(private val filePath: String) {
fun fileExists(storageScope: StorageScope, context: Context, path: String): Boolean { fun fileExists(storageScope: StorageScope, context: Context, path: String): Boolean {
return when(storageScope) { return when(storageScope) {
StorageScope.APP -> FileData.fileExists(path) 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 StorageScope.UNKNOWN -> false
} }
} }
@ -82,7 +121,13 @@ internal abstract class DataAccess(private val filePath: String) {
fun fileLastModified(storageScope: StorageScope, context: Context, path: String): Long { fun fileLastModified(storageScope: StorageScope, context: Context, path: String): Long {
return when(storageScope) { return when(storageScope) {
StorageScope.APP -> FileData.fileLastModified(path) 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 StorageScope.UNKNOWN -> 0L
} }
} }
@ -90,7 +135,13 @@ internal abstract class DataAccess(private val filePath: String) {
fun removeFile(storageScope: StorageScope, context: Context, path: String): Boolean { fun removeFile(storageScope: StorageScope, context: Context, path: String): Boolean {
return when(storageScope) { return when(storageScope) {
StorageScope.APP -> FileData.delete(path) 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 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 { fun renameFile(storageScope: StorageScope, context: Context, from: String, to: String): Boolean {
return when(storageScope) { return when(storageScope) {
StorageScope.APP -> FileData.rename(from, to) 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 StorageScope.UNKNOWN -> false
} }
} }
} }
protected abstract val fileChannel: FileChannel
internal var endOfFile = false internal var endOfFile = false
abstract fun close()
fun close() { abstract fun flush()
try { abstract fun seek(position: Long)
fileChannel.close() abstract fun resize(length: Long): Error
} catch (e: IOException) { abstract fun position(): Long
Log.w(TAG, "Exception when closing file $filePath.", e) abstract fun size(): Long
} abstract fun read(buffer: ByteBuffer): Int
} abstract fun write(buffer: ByteBuffer)
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)
}
}
fun seekFromEnd(positionFromEnd: Long) { fun seekFromEnd(positionFromEnd: Long) {
val positionFromBeginning = max(0, size() - positionFromEnd) val positionFromBeginning = max(0, size() - positionFromEnd)
seek(positionFromBeginning) seek(positionFromBeginning)
} }
fun resize(length: Long): Int { abstract class FileChannelDataAccess(private val filePath: String) : DataAccess() {
return try { internal abstract val fileChannel: FileChannel
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
}
}
fun position(): Long { override fun close() {
return try { try {
fileChannel.position() 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) { } catch (e: IOException) {
Log.w( Log.w(TAG, "Exception when retrieving size for file $filePath.", e)
TAG,
"Exception when retrieving position for file $filePath.",
e
)
0L 0L
} }
}
fun size() = try { override fun read(buffer: ByteBuffer): Int {
fileChannel.size() return try {
} catch (e: IOException) { val readBytes = fileChannel.read(buffer)
Log.w(TAG, "Exception when retrieving size for file $filePath.", e) endOfFile = readBytes == -1 || (fileChannel.position() >= fileChannel.size())
0L if (readBytes == -1) {
} 0
} else {
fun read(buffer: ByteBuffer): Int { readBytes
return try { }
val readBytes = fileChannel.read(buffer) } catch (e: IOException) {
endOfFile = readBytes == -1 || (fileChannel.position() >= fileChannel.size()) Log.w(TAG, "Exception while reading from file $filePath.", e)
if (readBytes == -1) {
0 0
} else {
readBytes
} }
} catch (e: IOException) {
Log.w(TAG, "Exception while reading from file $filePath.", e)
0
} }
}
fun write(buffer: ByteBuffer) { override fun write(buffer: ByteBuffer) {
try { try {
val writtenBytes = fileChannel.write(buffer) val writtenBytes = fileChannel.write(buffer)
if (writtenBytes > 0) { if (writtenBytes > 0) {
endOfFile = false 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)
} }
} }
} }

View File

@ -76,7 +76,7 @@ internal enum class FileAccessFlags(val nativeValue: Int) {
companion object { companion object {
fun fromNativeModeFlags(modeFlag: Int): FileAccessFlags? { fun fromNativeModeFlags(modeFlag: Int): FileAccessFlags? {
for (flag in values()) { for (flag in entries) {
if (flag.nativeValue == modeFlag) { if (flag.nativeValue == modeFlag) {
return flag return flag
} }

View File

@ -33,8 +33,11 @@ package org.godotengine.godot.io.file
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import android.util.SparseArray import android.util.SparseArray
import org.godotengine.godot.error.Error
import org.godotengine.godot.io.StorageScope import org.godotengine.godot.io.StorageScope
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.InputStream
import java.lang.UnsupportedOperationException
import java.nio.ByteBuffer import java.nio.ByteBuffer
/** /**
@ -45,8 +48,20 @@ class FileAccessHandler(val context: Context) {
companion object { companion object {
private val TAG = FileAccessHandler::class.java.simpleName 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 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 { internal fun fileExists(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean {
val storageScope = storageScopeIdentifier.identifyStorageScope(path) val storageScope = storageScopeIdentifier.identifyStorageScope(path)
@ -98,29 +113,45 @@ class FileAccessHandler(val context: Context) {
private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0 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 { fun fileOpen(path: String?, modeFlags: Int): Int {
val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID val (fileError, fileId) = fileOpen(path, FileAccessFlags.fromNativeModeFlags(modeFlags))
return fileOpen(path, accessFlag) 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) val storageScope = storageScopeIdentifier.identifyStorageScope(path)
if (storageScope == StorageScope.UNKNOWN) { if (storageScope == StorageScope.UNKNOWN) {
return INVALID_FILE_ID return FILE_OPEN_FAILED
} }
return try { return try {
path?.let { 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) files.put(++lastFileId, dataAccess)
lastFileId Pair(Error.OK, lastFileId)
} ?: INVALID_FILE_ID } ?: FILE_OPEN_FAILED
} catch (e: FileNotFoundException) { } 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) { } catch (e: Exception) {
Log.w(TAG, "Error while opening $path", e) 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() 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 fileExists(path: String?) = Companion.fileExists(context, storageScopeIdentifier, path)
fun fileLastModified(filepath: String?): Long { fun fileLastModified(filepath: String?): Long {
@ -191,10 +226,10 @@ class FileAccessHandler(val context: Context) {
fun fileResize(fileId: Int, length: Long): Int { fun fileResize(fileId: Int, length: Long): Int {
if (!hasFileId(fileId)) { 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 { fun fileGetPosition(fileId: Int): Long {

View File

@ -38,7 +38,7 @@ import java.nio.channels.FileChannel
/** /**
* Implementation of [DataAccess] which handles regular (not scoped) file access and interactions. * 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 { companion object {
private val TAG = FileData::class.java.simpleName private val TAG = FileData::class.java.simpleName
@ -80,10 +80,16 @@ internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAcc
override val fileChannel: FileChannel override val fileChannel: FileChannel
init { init {
if (accessFlag == FileAccessFlags.WRITE) { fileChannel = if (accessFlag == FileAccessFlags.WRITE) {
fileChannel = FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel // Create parent directory is necessary
val parentDir = File(filePath).parentFile
if (parentDir != null && !parentDir.exists()) {
parentDir.mkdirs()
}
FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel
} else { } else {
fileChannel = RandomAccessFile(filePath, accessFlag.getMode()).channel RandomAccessFile(filePath, accessFlag.getMode()).channel
} }
if (accessFlag.shouldTruncate()) { if (accessFlag.shouldTruncate()) {

View File

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

View File

@ -52,7 +52,7 @@ import java.nio.channels.FileChannel
*/ */
@RequiresApi(Build.VERSION_CODES.Q) @RequiresApi(Build.VERSION_CODES.Q)
internal class MediaStoreData(context: Context, filePath: String, accessFlag: FileAccessFlags) : internal class MediaStoreData(context: Context, filePath: String, accessFlag: FileAccessFlags) :
DataAccess(filePath) { DataAccess.FileChannelDataAccess(filePath) {
private data class DataItem( private data class DataItem(
val id: Long, val id: Long,

View File

@ -37,6 +37,7 @@ import android.os.SystemClock
import android.os.Trace import android.os.Trace
import android.util.Log import android.util.Log
import org.godotengine.godot.BuildConfig import org.godotengine.godot.BuildConfig
import org.godotengine.godot.error.Error
import org.godotengine.godot.io.file.FileAccessFlags import org.godotengine.godot.io.file.FileAccessFlags
import org.godotengine.godot.io.file.FileAccessHandler import org.godotengine.godot.io.file.FileAccessHandler
import org.json.JSONObject import org.json.JSONObject
@ -128,8 +129,8 @@ fun dumpBenchmark(fileAccessHandler: FileAccessHandler? = null, filepath: String
Log.i(TAG, "BENCHMARK:\n$printOut") Log.i(TAG, "BENCHMARK:\n$printOut")
if (fileAccessHandler != null && !filepath.isNullOrBlank()) { if (fileAccessHandler != null && !filepath.isNullOrBlank()) {
val fileId = fileAccessHandler.fileOpen(filepath, FileAccessFlags.WRITE) val (fileError, fileId) = fileAccessHandler.fileOpen(filepath, FileAccessFlags.WRITE)
if (fileId != FileAccessHandler.INVALID_FILE_ID) { if (fileError == Error.OK) {
val jsonOutput = JSONObject(benchmarkTracker.toMap()).toString(4) val jsonOutput = JSONObject(benchmarkTracker.toMap()).toString(4)
fileAccessHandler.fileWrite(fileId, ByteBuffer.wrap(jsonOutput.toByteArray())) fileAccessHandler.fileWrite(fileId, ByteBuffer.wrap(jsonOutput.toByteArray()))
fileAccessHandler.fileClose(fileId) fileAccessHandler.fileClose(fileId)

View File

@ -574,4 +574,9 @@ JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInp
} }
return false; 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());
}
} }

View File

@ -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_onRendererResumed(JNIEnv *env, jclass clazz);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(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 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 #endif // JAVA_GODOT_LIB_JNI_H