Update the storage access handler logic to support accessing / retrieving contents with the assets:/
prefix
This commit is contained in:
parent
e63c40e59c
commit
794ea99240
@ -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 {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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 ""
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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,10 +81,17 @@ 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) {
|
||||||
|
pathFile = File(GodotLib.getProjectResourceDir(), path)
|
||||||
if (!pathFile.isAbsolute) {
|
if (!pathFile.isAbsolute) {
|
||||||
return UNKNOWN
|
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
|
||||||
// restriction.
|
// restriction.
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dirNext(nativeAccessType: Int, dirId: Int): String {
|
val dirAccessId = accessType.generateDirAccessId(dirId)
|
||||||
val accessType = getAccessTypeFromNative(nativeAccessType)
|
return dirAccessId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dirNext(dirAccessId: Int): String {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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.")
|
||||||
|
}
|
||||||
|
}
|
@ -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,16 +149,37 @@ 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()
|
||||||
|
abstract fun flush()
|
||||||
|
abstract fun seek(position: Long)
|
||||||
|
abstract fun resize(length: Long): Error
|
||||||
|
abstract fun position(): Long
|
||||||
|
abstract fun size(): Long
|
||||||
|
abstract fun read(buffer: ByteBuffer): Int
|
||||||
|
abstract fun write(buffer: ByteBuffer)
|
||||||
|
|
||||||
fun close() {
|
fun seekFromEnd(positionFromEnd: Long) {
|
||||||
|
val positionFromBeginning = max(0, size() - positionFromEnd)
|
||||||
|
seek(positionFromBeginning)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class FileChannelDataAccess(private val filePath: String) : DataAccess() {
|
||||||
|
internal abstract val fileChannel: FileChannel
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
try {
|
try {
|
||||||
fileChannel.close()
|
fileChannel.close()
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
@ -115,7 +187,7 @@ internal abstract class DataAccess(private val filePath: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun flush() {
|
override fun flush() {
|
||||||
try {
|
try {
|
||||||
fileChannel.force(false)
|
fileChannel.force(false)
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
@ -123,7 +195,7 @@ internal abstract class DataAccess(private val filePath: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun seek(position: Long) {
|
override fun seek(position: Long) {
|
||||||
try {
|
try {
|
||||||
fileChannel.position(position)
|
fileChannel.position(position)
|
||||||
endOfFile = position >= fileChannel.size()
|
endOfFile = position >= fileChannel.size()
|
||||||
@ -132,27 +204,22 @@ internal abstract class DataAccess(private val filePath: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun seekFromEnd(positionFromEnd: Long) {
|
override fun resize(length: Long): Error {
|
||||||
val positionFromBeginning = max(0, size() - positionFromEnd)
|
|
||||||
seek(positionFromBeginning)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resize(length: Long): Int {
|
|
||||||
return try {
|
return try {
|
||||||
fileChannel.truncate(length)
|
fileChannel.truncate(length)
|
||||||
FileErrors.OK.nativeValue
|
Error.OK
|
||||||
} catch (e: NonWritableChannelException) {
|
} catch (e: NonWritableChannelException) {
|
||||||
FileErrors.FILE_CANT_OPEN.nativeValue
|
Error.ERR_FILE_CANT_OPEN
|
||||||
} catch (e: ClosedChannelException) {
|
} catch (e: ClosedChannelException) {
|
||||||
FileErrors.FILE_CANT_OPEN.nativeValue
|
Error.ERR_FILE_CANT_OPEN
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
FileErrors.INVALID_PARAMETER.nativeValue
|
Error.ERR_INVALID_PARAMETER
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
FileErrors.FAILED.nativeValue
|
Error.FAILED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun position(): Long {
|
override fun position(): Long {
|
||||||
return try {
|
return try {
|
||||||
fileChannel.position()
|
fileChannel.position()
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
@ -165,14 +232,14 @@ internal abstract class DataAccess(private val filePath: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun size() = try {
|
override fun size() = try {
|
||||||
fileChannel.size()
|
fileChannel.size()
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.w(TAG, "Exception when retrieving size for file $filePath.", e)
|
Log.w(TAG, "Exception when retrieving size for file $filePath.", e)
|
||||||
0L
|
0L
|
||||||
}
|
}
|
||||||
|
|
||||||
fun read(buffer: ByteBuffer): Int {
|
override fun read(buffer: ByteBuffer): Int {
|
||||||
return try {
|
return try {
|
||||||
val readBytes = fileChannel.read(buffer)
|
val readBytes = fileChannel.read(buffer)
|
||||||
endOfFile = readBytes == -1 || (fileChannel.position() >= fileChannel.size())
|
endOfFile = readBytes == -1 || (fileChannel.position() >= fileChannel.size())
|
||||||
@ -187,7 +254,7 @@ internal abstract class DataAccess(private val filePath: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@ -197,4 +264,5 @@ internal abstract class DataAccess(private val filePath: String) {
|
|||||||
Log.w(TAG, "Exception while writing to file $filePath.", e)
|
Log.w(TAG, "Exception while writing to file $filePath.", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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?): Pair<Error, Int> {
|
||||||
|
if (accessFlag == null) {
|
||||||
|
return FILE_OPEN_FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun fileOpen(path: String?, accessFlag: FileAccessFlags): Int {
|
|
||||||
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 {
|
||||||
|
@ -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()) {
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
||||||
|
@ -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)
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user