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
* - 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 {

View file

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

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

View file

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

View file

@ -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();
}

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.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

View file

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

View file

@ -32,7 +32,8 @@ package org.godotengine.godot.io.directory
import android.content.Context
import android.util.Log
import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_FILESYSTEM
import org.godotengine.godot.Godot
import org.godotengine.godot.io.StorageScope
import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_RESOURCES
/**
@ -45,18 +46,82 @@ class DirectoryAccessHandler(context: Context) {
internal const val INVALID_DIR_ID = -1
internal const val STARTING_DIR_ID = 1
private fun getAccessTypeFromNative(accessType: Int): AccessType? {
return when (accessType) {
ACCESS_RESOURCES.nativeValue -> ACCESS_RESOURCES
ACCESS_FILESYSTEM.nativeValue -> ACCESS_FILESYSTEM
else -> null
}
}
}
private enum class AccessType(val nativeValue: Int) {
ACCESS_RESOURCES(0), ACCESS_FILESYSTEM(2)
ACCESS_RESOURCES(0),
/**
* Maps to [ACCESS_FILESYSTEM]
*/
ACCESS_USERDATA(1),
ACCESS_FILESYSTEM(2);
fun generateDirAccessId(dirId: Int) = (dirId * DIR_ACCESS_ID_MULTIPLIER) + nativeValue
companion object {
const val DIR_ACCESS_ID_MULTIPLIER = 10
fun fromDirAccessId(dirAccessId: Int): Pair<AccessType?, Int> {
val nativeValue = dirAccessId % DIR_ACCESS_ID_MULTIPLIER
val dirId = dirAccessId / DIR_ACCESS_ID_MULTIPLIER
return Pair(fromNative(nativeValue), dirId)
}
private fun fromNative(nativeAccessType: Int): AccessType? {
for (accessType in entries) {
if (accessType.nativeValue == nativeAccessType) {
return accessType
}
}
return null
}
fun fromNative(nativeAccessType: Int, storageScope: StorageScope? = null): AccessType? {
val accessType = fromNative(nativeAccessType)
if (accessType == null) {
Log.w(TAG, "Unsupported access type $nativeAccessType")
return null
}
// 'Resources' access type takes precedence as it is simple to handle:
// if we receive a 'Resources' access type and this is a template build,
// we provide a 'Resources' directory handler.
// If this is an editor build, 'Resources' refers to the opened project resources
// and so we provide a 'Filesystem' directory handler.
if (accessType == ACCESS_RESOURCES) {
return if (Godot.isEditorBuild()) {
ACCESS_FILESYSTEM
} else {
ACCESS_RESOURCES
}
} else {
// We've received a 'Filesystem' or 'Userdata' access type. On Android, this
// may refer to:
// - assets directory (path has 'assets:/' prefix)
// - app directories
// - device shared directories
// As such we check the storage scope (if available) to figure what type of
// directory handler to provide
if (storageScope != null) {
val accessTypeFromStorageScope = when (storageScope) {
StorageScope.ASSETS -> ACCESS_RESOURCES
StorageScope.APP, StorageScope.SHARED -> ACCESS_FILESYSTEM
StorageScope.UNKNOWN -> null
}
if (accessTypeFromStorageScope != null) {
return accessTypeFromStorageScope
}
}
// If we're not able to infer the type of directory handler from the storage
// scope, we fall-back to the 'Filesystem' directory handler as it's the default
// for the 'Filesystem' access type.
// Note that ACCESS_USERDATA also maps to ACCESS_FILESYSTEM
return ACCESS_FILESYSTEM
}
}
}
}
internal interface DirectoryAccess {
@ -76,8 +141,10 @@ class DirectoryAccessHandler(context: Context) {
fun remove(filename: String): Boolean
}
private val storageScopeIdentifier = StorageScope.Identifier(context)
private val assetsDirAccess = AssetsDirectoryAccess(context)
private val fileSystemDirAccess = FilesystemDirectoryAccess(context)
private val fileSystemDirAccess = FilesystemDirectoryAccess(context, storageScopeIdentifier)
fun assetsFileExists(assetsPath: String) = assetsDirAccess.fileExists(assetsPath)
fun filesystemFileExists(path: String) = fileSystemDirAccess.fileExists(path)
@ -85,24 +152,32 @@ class DirectoryAccessHandler(context: Context) {
private fun hasDirId(accessType: AccessType, dirId: Int): Boolean {
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.hasDirId(dirId)
else -> fileSystemDirAccess.hasDirId(dirId)
}
}
fun dirOpen(nativeAccessType: Int, path: String?): Int {
val accessType = getAccessTypeFromNative(nativeAccessType)
if (path == null || accessType == null) {
if (path == null) {
return INVALID_DIR_ID
}
return when (accessType) {
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return INVALID_DIR_ID
val dirId = when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirOpen(path)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirOpen(path)
else -> fileSystemDirAccess.dirOpen(path)
}
if (dirId == INVALID_DIR_ID) {
return INVALID_DIR_ID
}
val dirAccessId = accessType.generateDirAccessId(dirId)
return dirAccessId
}
fun dirNext(nativeAccessType: Int, dirId: Int): String {
val accessType = getAccessTypeFromNative(nativeAccessType)
fun dirNext(dirAccessId: Int): String {
val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
if (accessType == null || !hasDirId(accessType, dirId)) {
Log.w(TAG, "dirNext: Invalid dir id: $dirId")
return ""
@ -110,12 +185,12 @@ class DirectoryAccessHandler(context: Context) {
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirNext(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirNext(dirId)
else -> fileSystemDirAccess.dirNext(dirId)
}
}
fun dirClose(nativeAccessType: Int, dirId: Int) {
val accessType = getAccessTypeFromNative(nativeAccessType)
fun dirClose(dirAccessId: Int) {
val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
if (accessType == null || !hasDirId(accessType, dirId)) {
Log.w(TAG, "dirClose: Invalid dir id: $dirId")
return
@ -123,12 +198,12 @@ class DirectoryAccessHandler(context: Context) {
when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirClose(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirClose(dirId)
else -> fileSystemDirAccess.dirClose(dirId)
}
}
fun dirIsDir(nativeAccessType: Int, dirId: Int): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType)
fun dirIsDir(dirAccessId: Int): Boolean {
val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
if (accessType == null || !hasDirId(accessType, dirId)) {
Log.w(TAG, "dirIsDir: Invalid dir id: $dirId")
return false
@ -136,91 +211,106 @@ class DirectoryAccessHandler(context: Context) {
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirIsDir(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirIsDir(dirId)
else -> fileSystemDirAccess.dirIsDir(dirId)
}
}
fun isCurrentHidden(nativeAccessType: Int, dirId: Int): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType)
fun isCurrentHidden(dirAccessId: Int): Boolean {
val (accessType, dirId) = AccessType.fromDirAccessId(dirAccessId)
if (accessType == null || !hasDirId(accessType, dirId)) {
return false
}
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.isCurrentHidden(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.isCurrentHidden(dirId)
else -> fileSystemDirAccess.isCurrentHidden(dirId)
}
}
fun dirExists(nativeAccessType: Int, path: String?): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType)
if (path == null || accessType == null) {
if (path == null) {
return false
}
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirExists(path)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirExists(path)
else -> fileSystemDirAccess.dirExists(path)
}
}
fun fileExists(nativeAccessType: Int, path: String?): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType)
if (path == null || accessType == null) {
if (path == null) {
return false
}
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.fileExists(path)
ACCESS_FILESYSTEM -> fileSystemDirAccess.fileExists(path)
else -> fileSystemDirAccess.fileExists(path)
}
}
fun getDriveCount(nativeAccessType: Int): Int {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0
val accessType = AccessType.fromNative(nativeAccessType) ?: return 0
return when(accessType) {
ACCESS_RESOURCES -> assetsDirAccess.getDriveCount()
ACCESS_FILESYSTEM -> fileSystemDirAccess.getDriveCount()
else -> fileSystemDirAccess.getDriveCount()
}
}
fun getDrive(nativeAccessType: Int, drive: Int): String {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return ""
val accessType = AccessType.fromNative(nativeAccessType) ?: return ""
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.getDrive(drive)
ACCESS_FILESYSTEM -> fileSystemDirAccess.getDrive(drive)
else -> fileSystemDirAccess.getDrive(drive)
}
}
fun makeDir(nativeAccessType: Int, dir: String): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
fun makeDir(nativeAccessType: Int, dir: String?): Boolean {
if (dir == null) {
return false
}
val storageScope = storageScopeIdentifier.identifyStorageScope(dir)
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.makeDir(dir)
ACCESS_FILESYSTEM -> fileSystemDirAccess.makeDir(dir)
else -> fileSystemDirAccess.makeDir(dir)
}
}
fun getSpaceLeft(nativeAccessType: Int): Long {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0L
val accessType = AccessType.fromNative(nativeAccessType) ?: return 0L
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.getSpaceLeft()
ACCESS_FILESYSTEM -> fileSystemDirAccess.getSpaceLeft()
else -> fileSystemDirAccess.getSpaceLeft()
}
}
fun rename(nativeAccessType: Int, from: String, to: String): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
val accessType = AccessType.fromNative(nativeAccessType) ?: return false
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.rename(from, to)
ACCESS_FILESYSTEM -> fileSystemDirAccess.rename(from, to)
else -> fileSystemDirAccess.rename(from, to)
}
}
fun remove(nativeAccessType: Int, filename: String): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
fun remove(nativeAccessType: Int, filename: String?): Boolean {
if (filename == null) {
return false
}
val storageScope = storageScopeIdentifier.identifyStorageScope(filename)
val accessType = AccessType.fromNative(nativeAccessType, storageScope) ?: return false
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.remove(filename)
ACCESS_FILESYSTEM -> fileSystemDirAccess.remove(filename)
else -> fileSystemDirAccess.remove(filename)
}
}

View file

@ -45,7 +45,7 @@ import java.io.File
/**
* Handles directories access with the internal and external filesystem.
*/
internal class FilesystemDirectoryAccess(private val context: Context):
internal class FilesystemDirectoryAccess(private val context: Context, private val storageScopeIdentifier: StorageScope.Identifier):
DirectoryAccessHandler.DirectoryAccess {
companion object {
@ -54,7 +54,6 @@ internal class FilesystemDirectoryAccess(private val context: Context):
private data class DirData(val dirFile: File, val files: Array<File>, var current: Int = 0)
private val storageScopeIdentifier = StorageScope.Identifier(context)
private val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
private var lastDirId = STARTING_DIR_ID
private val dirs = SparseArray<DirData>()
@ -63,7 +62,8 @@ internal class FilesystemDirectoryAccess(private val context: Context):
// Directory access is available for shared storage on Android 11+
// On Android 10, access is also available as long as the `requestLegacyExternalStorage`
// tag is available.
return storageScopeIdentifier.identifyStorageScope(path) != StorageScope.UNKNOWN
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
return storageScope != StorageScope.UNKNOWN && storageScope != StorageScope.ASSETS
}
override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0

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

View file

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

View file

@ -33,8 +33,11 @@ package org.godotengine.godot.io.file
import android.content.Context
import android.util.Log
import android.util.SparseArray
import org.godotengine.godot.error.Error
import org.godotengine.godot.io.StorageScope
import java.io.FileNotFoundException
import java.io.InputStream
import java.lang.UnsupportedOperationException
import java.nio.ByteBuffer
/**
@ -45,8 +48,20 @@ class FileAccessHandler(val context: Context) {
companion object {
private val TAG = FileAccessHandler::class.java.simpleName
internal const val INVALID_FILE_ID = 0
private const val INVALID_FILE_ID = 0
private const val STARTING_FILE_ID = 1
private val FILE_OPEN_FAILED = Pair(Error.FAILED, INVALID_FILE_ID)
internal fun getInputStream(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): InputStream? {
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
return try {
path?.let {
DataAccess.getInputStream(storageScope, context, path)
}
} catch (e: Exception) {
null
}
}
internal fun fileExists(context: Context, storageScopeIdentifier: StorageScope.Identifier, path: String?): Boolean {
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
@ -98,29 +113,45 @@ class FileAccessHandler(val context: Context) {
private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0
/**
* Returns a positive (> 0) file id when the operation succeeds.
* Otherwise, returns a negative value of [Error].
*/
fun fileOpen(path: String?, modeFlags: Int): Int {
val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID
return fileOpen(path, accessFlag)
val (fileError, fileId) = fileOpen(path, FileAccessFlags.fromNativeModeFlags(modeFlags))
return if (fileError == Error.OK) {
fileId
} else {
// Return the negative of the [Error#toNativeValue()] value to differentiate from the
// positive file id.
-fileError.toNativeValue()
}
}
internal fun fileOpen(path: String?, accessFlag: FileAccessFlags): Int {
internal fun fileOpen(path: String?, accessFlag: FileAccessFlags?): Pair<Error, Int> {
if (accessFlag == null) {
return FILE_OPEN_FAILED
}
val storageScope = storageScopeIdentifier.identifyStorageScope(path)
if (storageScope == StorageScope.UNKNOWN) {
return INVALID_FILE_ID
return FILE_OPEN_FAILED
}
return try {
path?.let {
val dataAccess = DataAccess.generateDataAccess(storageScope, context, it, accessFlag) ?: return INVALID_FILE_ID
val dataAccess = DataAccess.generateDataAccess(storageScope, context, it, accessFlag) ?: return FILE_OPEN_FAILED
files.put(++lastFileId, dataAccess)
lastFileId
} ?: INVALID_FILE_ID
Pair(Error.OK, lastFileId)
} ?: FILE_OPEN_FAILED
} catch (e: FileNotFoundException) {
FileErrors.FILE_NOT_FOUND.nativeValue
Pair(Error.ERR_FILE_NOT_FOUND, INVALID_FILE_ID)
} catch (e: UnsupportedOperationException) {
Pair(Error.ERR_UNAVAILABLE, INVALID_FILE_ID)
} catch (e: Exception) {
Log.w(TAG, "Error while opening $path", e)
INVALID_FILE_ID
FILE_OPEN_FAILED
}
}
@ -172,6 +203,10 @@ class FileAccessHandler(val context: Context) {
files[fileId].flush()
}
fun getInputStream(path: String?) = Companion.getInputStream(context, storageScopeIdentifier, path)
fun renameFile(from: String, to: String) = Companion.renameFile(context, storageScopeIdentifier, from, to)
fun fileExists(path: String?) = Companion.fileExists(context, storageScopeIdentifier, path)
fun fileLastModified(filepath: String?): Long {
@ -191,10 +226,10 @@ class FileAccessHandler(val context: Context) {
fun fileResize(fileId: Int, length: Long): Int {
if (!hasFileId(fileId)) {
return FileErrors.FAILED.nativeValue
return Error.FAILED.toNativeValue()
}
return files[fileId].resize(length)
return files[fileId].resize(length).toNativeValue()
}
fun fileGetPosition(fileId: Int): Long {

View file

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

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)
internal class MediaStoreData(context: Context, filePath: String, accessFlag: FileAccessFlags) :
DataAccess(filePath) {
DataAccess.FileChannelDataAccess(filePath) {
private data class DataItem(
val id: Long,

View file

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

View file

@ -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());
}
}

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_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