diff --git a/core/os/dir_access.cpp b/core/os/dir_access.cpp index 49233f15681..4aa30547b0e 100644 --- a/core/os/dir_access.cpp +++ b/core/os/dir_access.cpp @@ -184,6 +184,10 @@ Error DirAccess::make_dir_recursive(String p_dir) { return OK; } +DirAccess::AccessType DirAccess::get_access_type() const { + return _access_type; +} + String DirAccess::fix_path(String p_path) const { switch (_access_type) { case ACCESS_RESOURCES: { diff --git a/core/os/dir_access.h b/core/os/dir_access.h index 207c7ed0e23..01704abf7a0 100644 --- a/core/os/dir_access.h +++ b/core/os/dir_access.h @@ -56,6 +56,7 @@ protected: String _get_root_path() const; String _get_root_string() const; + AccessType get_access_type() const; String fix_path(String p_path) const; bool next_is_dir; diff --git a/drivers/unix/dir_access_unix.cpp b/drivers/unix/dir_access_unix.cpp index 762c7135cdf..fdb0c7bcbcc 100644 --- a/drivers/unix/dir_access_unix.cpp +++ b/drivers/unix/dir_access_unix.cpp @@ -49,10 +49,6 @@ #include #endif -DirAccess *DirAccessUnix::create_fs() { - return memnew(DirAccessUnix); -} - Error DirAccessUnix::list_dir_begin() { list_dir_end(); //close any previous dir opening! diff --git a/drivers/unix/dir_access_unix.h b/drivers/unix/dir_access_unix.h index af20960de27..5def374d898 100644 --- a/drivers/unix/dir_access_unix.h +++ b/drivers/unix/dir_access_unix.h @@ -43,13 +43,11 @@ class DirAccessUnix : public DirAccess { DIR *dir_stream; - static DirAccess *create_fs(); - - String current_dir; bool _cisdir; bool _cishidden; protected: + String current_dir; virtual String fix_unicode_name(const char *p_name) const { return String::utf8(p_name); } virtual bool is_hidden(const String &p_name); diff --git a/drivers/unix/file_access_unix.cpp b/drivers/unix/file_access_unix.cpp index 1d290baddf4..79366c2534f 100644 --- a/drivers/unix/file_access_unix.cpp +++ b/drivers/unix/file_access_unix.cpp @@ -335,10 +335,6 @@ Error FileAccessUnix::_set_unix_permissions(const String &p_file, uint32_t p_per return FAILED; } -FileAccess *FileAccessUnix::create_libc() { - return memnew(FileAccessUnix); -} - CloseNotificationFunc FileAccessUnix::close_notification_func = nullptr; FileAccessUnix::FileAccessUnix() : diff --git a/drivers/unix/file_access_unix.h b/drivers/unix/file_access_unix.h index 36afde71478..6ad26430a56 100644 --- a/drivers/unix/file_access_unix.h +++ b/drivers/unix/file_access_unix.h @@ -49,8 +49,6 @@ class FileAccessUnix : public FileAccess { String path; String path_src; - static FileAccess *create_libc(); - public: static CloseNotificationFunc close_notification_func; diff --git a/platform/android/SCsub b/platform/android/SCsub index a8361750039..6405a385271 100644 --- a/platform/android/SCsub +++ b/platform/android/SCsub @@ -6,6 +6,7 @@ android_files = [ "os_android.cpp", "android_input_handler.cpp", "file_access_android.cpp", + "file_access_filesystem_jandroid.cpp", "audio_driver_opensl.cpp", "dir_access_jandroid.cpp", "thread_jandroid.cpp", diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp index 960aa9db457..2f5fc5f1aec 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/platform/android/dir_access_jandroid.cpp @@ -30,30 +30,32 @@ #include "dir_access_jandroid.h" #include "core/print_string.h" -#include "file_access_android.h" #include "string_android.h" #include "thread_jandroid.h" -jobject DirAccessJAndroid::io = nullptr; +jobject DirAccessJAndroid::dir_access_handler = nullptr; jclass DirAccessJAndroid::cls = nullptr; jmethodID DirAccessJAndroid::_dir_open = nullptr; jmethodID DirAccessJAndroid::_dir_next = nullptr; jmethodID DirAccessJAndroid::_dir_close = nullptr; jmethodID DirAccessJAndroid::_dir_is_dir = nullptr; - -DirAccess *DirAccessJAndroid::create_fs() { - return memnew(DirAccessJAndroid); -} +jmethodID DirAccessJAndroid::_dir_exists = nullptr; +jmethodID DirAccessJAndroid::_file_exists = nullptr; +jmethodID DirAccessJAndroid::_get_drive_count = nullptr; +jmethodID DirAccessJAndroid::_get_drive = nullptr; +jmethodID DirAccessJAndroid::_make_dir = nullptr; +jmethodID DirAccessJAndroid::_get_space_left = nullptr; +jmethodID DirAccessJAndroid::_rename = nullptr; +jmethodID DirAccessJAndroid::_remove = nullptr; +jmethodID DirAccessJAndroid::_current_is_hidden = nullptr; Error DirAccessJAndroid::list_dir_begin() { list_dir_end(); - JNIEnv *env = get_jni_env(); - - jstring js = env->NewStringUTF(current_dir.utf8().get_data()); - int res = env->CallIntMethod(io, _dir_open, js); + int res = dir_open(current_dir); if (res <= 0) { return ERR_CANT_OPEN; } + id = res; return OK; @@ -61,169 +63,236 @@ Error DirAccessJAndroid::list_dir_begin() { String DirAccessJAndroid::get_next() { ERR_FAIL_COND_V(id == 0, ""); + if (_dir_next) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, ""); + jstring str = (jstring)env->CallObjectMethod(dir_access_handler, _dir_next, get_access_type(), id); + if (!str) { + return ""; + } - JNIEnv *env = get_jni_env(); - jstring str = (jstring)env->CallObjectMethod(io, _dir_next, id); - if (!str) { + String ret = jstring_to_string((jstring)str, env); + env->DeleteLocalRef((jobject)str); + return ret; + } else { return ""; } - String ret = jstring_to_string((jstring)str, env); - env->DeleteLocalRef((jobject)str); - return ret; } bool DirAccessJAndroid::current_is_dir() const { - JNIEnv *env = get_jni_env(); - - return env->CallBooleanMethod(io, _dir_is_dir, id); + if (_dir_is_dir) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, false); + return env->CallBooleanMethod(dir_access_handler, _dir_is_dir, get_access_type(), id); + } else { + return false; + } } bool DirAccessJAndroid::current_is_hidden() const { - return current != "." && current != ".." && current.begins_with("."); + if (_current_is_hidden) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, false); + return env->CallBooleanMethod(dir_access_handler, _current_is_hidden, get_access_type(), id); + } + return false; } void DirAccessJAndroid::list_dir_end() { if (id == 0) { return; } - JNIEnv *env = get_jni_env(); - env->CallVoidMethod(io, _dir_close, id); + + dir_close(id); id = 0; } int DirAccessJAndroid::get_drive_count() { - return 0; + if (_get_drive_count) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, 0); + return env->CallIntMethod(dir_access_handler, _get_drive_count, get_access_type()); + } else { + return 0; + } } String DirAccessJAndroid::get_drive(int p_drive) { - return ""; + if (_get_drive) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, ""); + jstring j_drive = (jstring)env->CallObjectMethod(dir_access_handler, _get_drive, get_access_type(), p_drive); + if (!j_drive) { + return ""; + } + + String drive = jstring_to_string(j_drive, env); + env->DeleteLocalRef(j_drive); + return drive; + } else { + return ""; + } } Error DirAccessJAndroid::change_dir(String p_dir) { - JNIEnv *env = get_jni_env(); - - if (p_dir == "" || p_dir == "." || (p_dir == ".." && current_dir == "")) { + String new_dir = get_absolute_path(p_dir); + if (new_dir == current_dir) { return OK; } - String new_dir; - - if (p_dir != "res://" && p_dir.length() > 1 && p_dir.ends_with("/")) { - p_dir = p_dir.substr(0, p_dir.length() - 1); - } - - if (p_dir.begins_with("/")) { - new_dir = p_dir.substr(1, p_dir.length()); - } else if (p_dir.begins_with("res://")) { - new_dir = p_dir.substr(6, p_dir.length()); - } else if (current_dir == "") { - new_dir = p_dir; - } else { - new_dir = current_dir.plus_file(p_dir); - } - - //test if newdir exists - new_dir = new_dir.simplify_path(); - - jstring js = env->NewStringUTF(new_dir.utf8().get_data()); - int res = env->CallIntMethod(io, _dir_open, js); - env->DeleteLocalRef(js); - if (res <= 0) { + if (!dir_exists(new_dir)) { return ERR_INVALID_PARAMETER; } - env->CallVoidMethod(io, _dir_close, res); - current_dir = new_dir; - return OK; } -String DirAccessJAndroid::get_current_dir() { - return "res://" + current_dir; +String DirAccessJAndroid::get_absolute_path(String p_path) { + if (current_dir != "" && p_path == current_dir) { + return current_dir; + } + + if (p_path.is_rel_path()) { + p_path = get_current_dir().plus_file(p_path); + } + + p_path = fix_path(p_path); + p_path = p_path.simplify_path(); + return p_path; } bool DirAccessJAndroid::file_exists(String p_file) { - String sd; - if (current_dir == "") { - sd = p_file; + if (_file_exists) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, false); + + String path = get_absolute_path(p_file); + jstring j_path = env->NewStringUTF(path.utf8().get_data()); + bool result = env->CallBooleanMethod(dir_access_handler, _file_exists, get_access_type(), j_path); + env->DeleteLocalRef(j_path); + return result; } else { - sd = current_dir.plus_file(p_file); + return false; } - - FileAccessAndroid *f = memnew(FileAccessAndroid); - bool exists = f->file_exists(sd); - memdelete(f); - - return exists; } bool DirAccessJAndroid::dir_exists(String p_dir) { - JNIEnv *env = get_jni_env(); + if (_dir_exists) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, false); - String sd; - - if (current_dir == "") { - sd = p_dir; + String path = get_absolute_path(p_dir); + jstring j_path = env->NewStringUTF(path.utf8().get_data()); + bool result = env->CallBooleanMethod(dir_access_handler, _dir_exists, get_access_type(), j_path); + env->DeleteLocalRef(j_path); + return result; } else { - if (p_dir.is_rel_path()) { - sd = current_dir.plus_file(p_dir); - } else { - sd = fix_path(p_dir); - } - } - - String path = sd.simplify_path(); - - if (path.begins_with("/")) { - path = path.substr(1, path.length()); - } else if (path.begins_with("res://")) { - path = path.substr(6, path.length()); - } - - jstring js = env->NewStringUTF(path.utf8().get_data()); - int res = env->CallIntMethod(io, _dir_open, js); - env->DeleteLocalRef(js); - if (res <= 0) { return false; } +} - env->CallVoidMethod(io, _dir_close, res); +Error DirAccessJAndroid::make_dir_recursive(String p_dir) { + // Check if the directory exists already + if (dir_exists(p_dir)) { + return ERR_ALREADY_EXISTS; + } - return true; + if (_make_dir) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED); + + String path = get_absolute_path(p_dir); + jstring j_dir = env->NewStringUTF(path.utf8().get_data()); + bool result = env->CallBooleanMethod(dir_access_handler, _make_dir, get_access_type(), j_dir); + env->DeleteLocalRef(j_dir); + if (result) { + return OK; + } else { + return FAILED; + } + } else { + return ERR_UNCONFIGURED; + } } Error DirAccessJAndroid::make_dir(String p_dir) { - ERR_FAIL_V(ERR_UNAVAILABLE); + return make_dir_recursive(p_dir); } Error DirAccessJAndroid::rename(String p_from, String p_to) { - ERR_FAIL_V(ERR_UNAVAILABLE); + if (_rename) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED); + + String from_path = get_absolute_path(p_from); + jstring j_from = env->NewStringUTF(from_path.utf8().get_data()); + + String to_path = get_absolute_path(p_to); + jstring j_to = env->NewStringUTF(to_path.utf8().get_data()); + + bool result = env->CallBooleanMethod(dir_access_handler, _rename, get_access_type(), j_from, j_to); + env->DeleteLocalRef(j_from); + env->DeleteLocalRef(j_to); + if (result) { + return OK; + } else { + return FAILED; + } + } else { + return ERR_UNCONFIGURED; + } } Error DirAccessJAndroid::remove(String p_name) { - ERR_FAIL_V(ERR_UNAVAILABLE); -} + if (_remove) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED); -String DirAccessJAndroid::get_filesystem_type() const { - return "APK"; + String path = get_absolute_path(p_name); + jstring j_name = env->NewStringUTF(path.utf8().get_data()); + bool result = env->CallBooleanMethod(dir_access_handler, _remove, get_access_type(), j_name); + env->DeleteLocalRef(j_name); + if (result) { + return OK; + } else { + return FAILED; + } + } else { + return ERR_UNCONFIGURED; + } } uint64_t DirAccessJAndroid::get_space_left() { - return 0; + if (_get_space_left) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, 0); + return env->CallLongMethod(dir_access_handler, _get_space_left, get_access_type()); + } else { + return 0; + } } -void DirAccessJAndroid::setup(jobject p_io) { +void DirAccessJAndroid::setup(jobject p_dir_access_handler) { JNIEnv *env = get_jni_env(); - io = p_io; + dir_access_handler = env->NewGlobalRef(p_dir_access_handler); - jclass c = env->GetObjectClass(io); + jclass c = env->GetObjectClass(dir_access_handler); cls = (jclass)env->NewGlobalRef(c); - _dir_open = env->GetMethodID(cls, "dir_open", "(Ljava/lang/String;)I"); - _dir_next = env->GetMethodID(cls, "dir_next", "(I)Ljava/lang/String;"); - _dir_close = env->GetMethodID(cls, "dir_close", "(I)V"); - _dir_is_dir = env->GetMethodID(cls, "dir_is_dir", "(I)Z"); + _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_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"); + _get_drive = env->GetMethodID(cls, "getDrive", "(II)Ljava/lang/String;"); + _make_dir = env->GetMethodID(cls, "makeDir", "(ILjava/lang/String;)Z"); + _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"); } DirAccessJAndroid::DirAccessJAndroid() { @@ -232,3 +301,26 @@ DirAccessJAndroid::DirAccessJAndroid() { DirAccessJAndroid::~DirAccessJAndroid() { list_dir_end(); } + +int DirAccessJAndroid::dir_open(String p_path) { + if (_dir_open) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, 0); + + String path = get_absolute_path(p_path); + jstring js = env->NewStringUTF(path.utf8().get_data()); + int dirId = env->CallIntMethod(dir_access_handler, _dir_open, get_access_type(), js); + env->DeleteLocalRef(js); + return dirId; + } else { + return 0; + } +} + +void DirAccessJAndroid::dir_close(int p_id) { + if (_dir_close) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + env->CallVoidMethod(dir_access_handler, _dir_close, get_access_type(), p_id); + } +} diff --git a/platform/android/dir_access_jandroid.h b/platform/android/dir_access_jandroid.h index 2402abe247d..539fa4d7945 100644 --- a/platform/android/dir_access_jandroid.h +++ b/platform/android/dir_access_jandroid.h @@ -32,58 +32,70 @@ #define DIR_ACCESS_JANDROID_H #include "core/os/dir_access.h" +#include "drivers/unix/dir_access_unix.h" #include "java_godot_lib_jni.h" #include -class DirAccessJAndroid : public DirAccess { - static jobject io; +/// Android implementation of the DirAccess interface used to provide access to +/// ACCESS_FILESYSTEM and ACCESS_RESOURCES directory resources. +/// The implementation use jni in order to comply with Android filesystem +/// access restriction. +class DirAccessJAndroid : public DirAccessUnix { + static jobject dir_access_handler; static jclass cls; static jmethodID _dir_open; static jmethodID _dir_next; static jmethodID _dir_close; static jmethodID _dir_is_dir; - - int id = 0; - - String current_dir; - String current; - - static DirAccess *create_fs(); + static jmethodID _dir_exists; + static jmethodID _file_exists; + static jmethodID _get_drive_count; + static jmethodID _get_drive; + static jmethodID _make_dir; + static jmethodID _get_space_left; + static jmethodID _rename; + static jmethodID _remove; + static jmethodID _current_is_hidden; public: - virtual Error list_dir_begin(); ///< This starts dir listing - virtual String get_next(); - virtual bool current_is_dir() const; - virtual bool current_is_hidden() const; - virtual void list_dir_end(); ///< + virtual Error list_dir_begin() override; ///< This starts dir listing + virtual String get_next() override; + virtual bool current_is_dir() const override; + virtual bool current_is_hidden() const override; + virtual void list_dir_end() override; ///< - virtual int get_drive_count(); - virtual String get_drive(int p_drive); + virtual int get_drive_count() override; + virtual String get_drive(int p_drive) override; - virtual Error change_dir(String p_dir); ///< can be relative or absolute, return false on success - virtual String get_current_dir(); ///< return current dir location + virtual Error change_dir(String p_dir) override; ///< can be relative or absolute, return false on success - virtual bool file_exists(String p_file); - virtual bool dir_exists(String p_dir); + virtual bool file_exists(String p_file) override; + virtual bool dir_exists(String p_dir) override; - virtual Error make_dir(String p_dir); + virtual Error make_dir(String p_dir) override; + virtual Error make_dir_recursive(String p_dir) override; - virtual Error rename(String p_from, String p_to); - virtual Error remove(String p_name); + virtual Error rename(String p_from, String p_to) override; + virtual Error remove(String p_name) override; - virtual bool is_link(String p_file) { return false; } - virtual String read_link(String p_file) { return p_file; } - virtual Error create_link(String p_source, String p_target) { return FAILED; } + virtual bool is_link(String p_file) override { return false; } + virtual String read_link(String p_file) override { return p_file; } + virtual Error create_link(String p_source, String p_target) override { return FAILED; } - virtual String get_filesystem_type() const; + virtual uint64_t get_space_left() override; - uint64_t get_space_left(); - - static void setup(jobject p_io); + static void setup(jobject p_dir_access_handler); DirAccessJAndroid(); ~DirAccessJAndroid(); + +private: + int id = 0; + + int dir_open(String p_path); + void dir_close(int p_id); + String get_absolute_path(String p_path); }; #endif // DIR_ACCESS_JANDROID_H diff --git a/platform/android/export/export_plugin.cpp b/platform/android/export/export_plugin.cpp index 257384d9bf6..609cf887d56 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -103,6 +103,7 @@ static const char *android_perms[] = { "MANAGE_ACCOUNTS", "MANAGE_APP_TOKENS", "MANAGE_DOCUMENTS", + "MANAGE_EXTERNAL_STORAGE", "MASTER_CLEAR", "MEDIA_CONTENT_CONTROL", "MODIFY_AUDIO_SETTINGS", @@ -225,7 +226,7 @@ static const char *APK_ASSETS_DIRECTORY = "res://android/build/assets"; static const char *AAB_ASSETS_DIRECTORY = "res://android/build/assetPacks/installTime/src/main/assets"; static const int DEFAULT_MIN_SDK_VERSION = 19; // Should match the value in 'platform/android/java/app/config.gradle#minSdk' -static const int DEFAULT_TARGET_SDK_VERSION = 31; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk' +static const int DEFAULT_TARGET_SDK_VERSION = 32; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk' const String SDK_VERSION_RANGE = vformat("%s,%s,1", DEFAULT_MIN_SDK_VERSION, DEFAULT_TARGET_SDK_VERSION); void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { @@ -732,10 +733,14 @@ Error EditorExportPlatformAndroid::copy_gradle_so(void *p_userdata, const Shared return OK; } -bool EditorExportPlatformAndroid::_has_storage_permission(const Vector &p_permissions) { +bool EditorExportPlatformAndroid::_has_read_write_storage_permission(const Vector &p_permissions) { return p_permissions.find("android.permission.READ_EXTERNAL_STORAGE") != -1 || p_permissions.find("android.permission.WRITE_EXTERNAL_STORAGE") != -1; } +bool EditorExportPlatformAndroid::_has_manage_external_storage_permission(const Vector &p_permissions) { + return p_permissions.find("android.permission.MANAGE_EXTERNAL_STORAGE") != -1; +} + void EditorExportPlatformAndroid::_get_permissions(const Ref &p_preset, bool p_give_internet, Vector &r_permissions) { const char **aperms = android_perms; while (*aperms) { @@ -783,7 +788,7 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref\n", permission); } else { manifest_text += vformat(" \n", permission); @@ -791,7 +796,7 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref &p Vector perms; // Write permissions into the perms variable. _get_permissions(p_preset, p_give_internet, perms); - bool has_storage_permission = _has_storage_permission(perms); + bool has_read_write_storage_permission = _has_read_write_storage_permission(perms); while (ofs < (uint32_t)p_manifest.size()) { uint32_t chunk = decode_uint32(&p_manifest[ofs]); @@ -935,7 +940,7 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref &p } if (tname == "application" && attrname == "requestLegacyExternalStorage") { - encode_uint32(has_storage_permission ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]); + encode_uint32(has_read_write_storage_permission ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]); } if (tname == "application" && attrname == "allowBackup") { diff --git a/platform/android/export/export_plugin.h b/platform/android/export/export_plugin.h index 12627c5ea98..8898cf69552 100644 --- a/platform/android/export/export_plugin.h +++ b/platform/android/export/export_plugin.h @@ -134,7 +134,9 @@ class EditorExportPlatformAndroid : public EditorExportPlatform { static Error copy_gradle_so(void *p_userdata, const SharedObject &p_so); - bool _has_storage_permission(const Vector &p_permissions); + bool _has_read_write_storage_permission(const Vector &p_permissions); + + bool _has_manage_external_storage_permission(const Vector &p_permissions); void _get_permissions(const Ref &p_preset, bool p_give_internet, Vector &r_permissions); diff --git a/platform/android/export/gradle_export_util.cpp b/platform/android/export/gradle_export_util.cpp index 1e092fd6f6f..b81dba53b16 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -256,7 +256,7 @@ String _get_activity_tag(const Ref &p_preset) { return manifest_activity_text; } -String _get_application_tag(const Ref &p_preset, bool p_has_storage_permission) { +String _get_application_tag(const Ref &p_preset, bool p_has_read_write_storage_permission) { int xr_mode_index = (int)(p_preset->get("xr_features/xr_mode")); bool uses_xr = xr_mode_index == XR_MODE_OVR || xr_mode_index == XR_MODE_OPENXR; String manifest_application_text = vformat( @@ -274,7 +274,7 @@ String _get_application_tag(const Ref &p_preset, bool p_has_ bool_to_string(p_preset->get("user_data_backup/allow")), bool_to_string(p_preset->get("package/classify_as_game")), bool_to_string(p_preset->get("package/retain_data_on_uninstall")), - bool_to_string(p_has_storage_permission)); + bool_to_string(p_has_read_write_storage_permission)); if (uses_xr) { if (xr_mode_index == XR_MODE_OVR) { diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h index c31bf047eed..81923a25d0d 100644 --- a/platform/android/export/gradle_export_util.h +++ b/platform/android/export/gradle_export_util.h @@ -105,6 +105,6 @@ String _get_xr_features_tag(const Ref &p_preset); String _get_activity_tag(const Ref &p_preset); -String _get_application_tag(const Ref &p_preset, bool p_has_storage_permission); +String _get_application_tag(const Ref &p_preset, bool p_has_read_write_storage_permission); #endif // GODOT_GRADLE_EXPORT_UTIL_H diff --git a/platform/android/file_access_android.cpp b/platform/android/file_access_android.cpp index 3fb14bb3965..cc74f5a9094 100644 --- a/platform/android/file_access_android.cpp +++ b/platform/android/file_access_android.cpp @@ -33,12 +33,18 @@ AAssetManager *FileAccessAndroid::asset_manager = nullptr; -FileAccess *FileAccessAndroid::create_android() { - return memnew(FileAccessAndroid); +String FileAccessAndroid::get_path() const { + return path_src; +} + +String FileAccessAndroid::get_path_absolute() const { + return absolute_path; } Error FileAccessAndroid::_open(const String &p_path, int p_mode_flags) { + path_src = p_path; String path = fix_path(p_path).simplify_path(); + absolute_path = path; if (path.begins_with("/")) { path = path.substr(1, path.length()); } else if (path.begins_with("res://")) { @@ -131,7 +137,7 @@ uint64_t FileAccessAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const } Error FileAccessAndroid::get_error() const { - return eof ? ERR_FILE_EOF : OK; //not sure what else it may happen + return eof ? ERR_FILE_EOF : OK; // not sure what else it may happen } void FileAccessAndroid::flush() { diff --git a/platform/android/file_access_android.h b/platform/android/file_access_android.h index e2e78b2b917..80204d01f60 100644 --- a/platform/android/file_access_android.h +++ b/platform/android/file_access_android.h @@ -37,12 +37,12 @@ #include class FileAccessAndroid : public FileAccess { - static FileAccess *create_android(); mutable AAsset *asset = nullptr; mutable uint64_t len = 0; mutable uint64_t pos = 0; mutable bool eof = false; - ; + String absolute_path; + String path_src; public: static AAssetManager *asset_manager; @@ -51,6 +51,11 @@ public: virtual void close(); // close a file virtual bool is_open() const; // true when file is open + /// returns the path for the current open file + virtual String get_path() const; + /// returns the absolute path for the current open file + virtual String get_path_absolute() const; + virtual void seek(uint64_t p_position); // seek to a given position virtual void seek_end(int64_t p_position = 0); // seek from the end of file virtual uint64_t get_position() const; // get position in the file diff --git a/platform/android/file_access_filesystem_jandroid.cpp b/platform/android/file_access_filesystem_jandroid.cpp new file mode 100644 index 00000000000..301c429245b --- /dev/null +++ b/platform/android/file_access_filesystem_jandroid.cpp @@ -0,0 +1,276 @@ +/*************************************************************************/ +/* file_access_filesystem_jandroid.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ + +#include "file_access_filesystem_jandroid.h" +#include "core/os/os.h" +#include "thread_jandroid.h" +#include + +jobject FileAccessFilesystemJAndroid::file_access_handler = nullptr; +jclass FileAccessFilesystemJAndroid::cls; + +jmethodID FileAccessFilesystemJAndroid::_file_open = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_get_size = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_seek = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_seek_end = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_read = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_tell = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_eof = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_close = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_write = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_flush = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_exists = nullptr; +jmethodID FileAccessFilesystemJAndroid::_file_last_modified = nullptr; + +String FileAccessFilesystemJAndroid::get_path() const { + return path_src; +} + +String FileAccessFilesystemJAndroid::get_path_absolute() const { + return absolute_path; +} + +Error FileAccessFilesystemJAndroid::_open(const String &p_path, int p_mode_flags) { + if (is_open()) { + close(); + } + + if (_file_open) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED); + + String path = fix_path(p_path).simplify_path(); + jstring js = env->NewStringUTF(path.utf8().get_data()); + int res = env->CallIntMethod(file_access_handler, _file_open, js, p_mode_flags); + env->DeleteLocalRef(js); + + if (res <= 0) { + return ERR_FILE_CANT_OPEN; + } + + id = res; + path_src = p_path; + absolute_path = path; + return OK; + } else { + return ERR_UNCONFIGURED; + } +} + +void FileAccessFilesystemJAndroid::close() { + if (!is_open()) { + return; + } + + if (_file_close) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + env->CallVoidMethod(file_access_handler, _file_close, id); + } + id = 0; +} + +bool FileAccessFilesystemJAndroid::is_open() const { + return id != 0; +} + +void FileAccessFilesystemJAndroid::seek(uint64_t p_position) { + if (_file_seek) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use."); + env->CallVoidMethod(file_access_handler, _file_seek, id, p_position); + } +} + +void FileAccessFilesystemJAndroid::seek_end(int64_t p_position) { + if (_file_seek_end) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use."); + env->CallVoidMethod(file_access_handler, _file_seek_end, id, p_position); + } +} + +uint64_t FileAccessFilesystemJAndroid::get_position() const { + if (_file_tell) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, 0); + ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); + return env->CallLongMethod(file_access_handler, _file_tell, id); + } else { + return 0; + } +} + +uint64_t FileAccessFilesystemJAndroid::get_len() const { + if (_file_get_size) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, 0); + ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); + return env->CallLongMethod(file_access_handler, _file_get_size, id); + } else { + return 0; + } +} + +bool FileAccessFilesystemJAndroid::eof_reached() const { + if (_file_eof) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, false); + ERR_FAIL_COND_V_MSG(!is_open(), false, "File must be opened before use."); + return env->CallBooleanMethod(file_access_handler, _file_eof, id); + } else { + return false; + } +} + +uint8_t FileAccessFilesystemJAndroid::get_8() const { + ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); + uint8_t byte; + get_buffer(&byte, 1); + return byte; +} + +uint64_t FileAccessFilesystemJAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const { + if (_file_read) { + ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use."); + if (p_length == 0) { + return 0; + } + + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, 0); + + jobject j_buffer = env->NewDirectByteBuffer(p_dst, p_length); + int length = env->CallIntMethod(file_access_handler, _file_read, id, j_buffer); + env->DeleteLocalRef(j_buffer); + return length; + } else { + return 0; + } +} + +void FileAccessFilesystemJAndroid::store_8(uint8_t p_dest) { + store_buffer(&p_dest, 1); +} + +void FileAccessFilesystemJAndroid::store_buffer(const uint8_t *p_src, uint64_t p_length) { + if (_file_write) { + ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use."); + if (p_length == 0) { + return; + } + + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + + jobject j_buffer = env->NewDirectByteBuffer((void *)p_src, p_length); + env->CallVoidMethod(file_access_handler, _file_write, id, j_buffer); + env->DeleteLocalRef(j_buffer); + } +} + +Error FileAccessFilesystemJAndroid::get_error() const { + if (eof_reached()) { + return ERR_FILE_EOF; + } + return OK; +} + +void FileAccessFilesystemJAndroid::flush() { + if (_file_flush) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND(env == nullptr); + ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use."); + env->CallVoidMethod(file_access_handler, _file_flush, id); + } +} + +bool FileAccessFilesystemJAndroid::file_exists(const String &p_path) { + if (_file_exists) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, false); + + String path = fix_path(p_path).simplify_path(); + jstring js = env->NewStringUTF(path.utf8().get_data()); + bool result = env->CallBooleanMethod(file_access_handler, _file_exists, js); + env->DeleteLocalRef(js); + return result; + } else { + return false; + } +} + +uint64_t FileAccessFilesystemJAndroid::_get_modified_time(const String &p_file) { + if (_file_last_modified) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, false); + + String path = fix_path(p_file).simplify_path(); + jstring js = env->NewStringUTF(path.utf8().get_data()); + uint64_t result = env->CallLongMethod(file_access_handler, _file_last_modified, js); + env->DeleteLocalRef(js); + return result; + } else { + return 0; + } +} + +void FileAccessFilesystemJAndroid::setup(jobject p_file_access_handler) { + JNIEnv *env = get_jni_env(); + file_access_handler = env->NewGlobalRef(p_file_access_handler); + + jclass c = env->GetObjectClass(file_access_handler); + cls = (jclass)env->NewGlobalRef(c); + + _file_open = env->GetMethodID(cls, "fileOpen", "(Ljava/lang/String;I)I"); + _file_get_size = env->GetMethodID(cls, "fileGetSize", "(I)J"); + _file_tell = env->GetMethodID(cls, "fileGetPosition", "(I)J"); + _file_eof = env->GetMethodID(cls, "isFileEof", "(I)Z"); + _file_seek = env->GetMethodID(cls, "fileSeek", "(IJ)V"); + _file_seek_end = env->GetMethodID(cls, "fileSeekFromEnd", "(IJ)V"); + _file_read = env->GetMethodID(cls, "fileRead", "(ILjava/nio/ByteBuffer;)I"); + _file_close = env->GetMethodID(cls, "fileClose", "(I)V"); + _file_write = env->GetMethodID(cls, "fileWrite", "(ILjava/nio/ByteBuffer;)V"); + _file_flush = env->GetMethodID(cls, "fileFlush", "(I)V"); + _file_exists = env->GetMethodID(cls, "fileExists", "(Ljava/lang/String;)Z"); + _file_last_modified = env->GetMethodID(cls, "fileLastModified", "(Ljava/lang/String;)J"); +} + +FileAccessFilesystemJAndroid::FileAccessFilesystemJAndroid() { + id = 0; +} + +FileAccessFilesystemJAndroid::~FileAccessFilesystemJAndroid() { + if (is_open()) { + close(); + } +} diff --git a/platform/android/file_access_filesystem_jandroid.h b/platform/android/file_access_filesystem_jandroid.h new file mode 100644 index 00000000000..2ec9567395f --- /dev/null +++ b/platform/android/file_access_filesystem_jandroid.h @@ -0,0 +1,96 @@ +/*************************************************************************/ +/* file_access_filesystem_jandroid.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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. */ +/*************************************************************************/ + +#ifndef FILE_ACCESS_FILESYSTEM_JANDROID_H +#define FILE_ACCESS_FILESYSTEM_JANDROID_H + +#include "core/os/file_access.h" +#include "java_godot_lib_jni.h" + +class FileAccessFilesystemJAndroid : public FileAccess { + static jobject file_access_handler; + static jclass cls; + + static jmethodID _file_open; + static jmethodID _file_get_size; + static jmethodID _file_seek; + static jmethodID _file_seek_end; + static jmethodID _file_tell; + static jmethodID _file_eof; + static jmethodID _file_read; + static jmethodID _file_write; + static jmethodID _file_flush; + static jmethodID _file_close; + static jmethodID _file_exists; + static jmethodID _file_last_modified; + + int id; + String absolute_path; + String path_src; + +public: + virtual Error _open(const String &p_path, int p_mode_flags) override; ///< open a file + virtual void close() override; ///< close a file + virtual bool is_open() const override; ///< true when file is open + + /// returns the path for the current open file + virtual String get_path() const override; + /// returns the absolute path for the current open file + virtual String get_path_absolute() const override; + + virtual void seek(uint64_t p_position) override; ///< seek to a given position + virtual void seek_end(int64_t p_position = 0) override; ///< seek from the end of file + virtual uint64_t get_position() const override; ///< get position in the file + virtual uint64_t get_len() const override; ///< get size of the file + + virtual bool eof_reached() const override; ///< reading passed EOF + + virtual uint8_t get_8() const override; ///< get a byte + virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override; + + virtual Error get_error() const override; ///< get last error + + virtual void flush() override; + virtual void store_8(uint8_t p_dest) override; ///< store a byte + virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override; + + virtual bool file_exists(const String &p_path) override; ///< return true if a file exists + + static void setup(jobject p_file_access_handler); + + virtual uint64_t _get_modified_time(const String &p_file) override; + virtual uint32_t _get_unix_permissions(const String &p_file) override { return 0; } + virtual Error _set_unix_permissions(const String &p_file, uint32_t p_permissions) override { return FAILED; } + + FileAccessFilesystemJAndroid(); + ~FileAccessFilesystemJAndroid(); +}; + +#endif // FILE_ACCESS_FILESYSTEM_JANDROID_H diff --git a/platform/android/java/app/AndroidManifest.xml b/platform/android/java/app/AndroidManifest.xml index d38c2d6ed87..b4e3a41c029 100644 --- a/platform/android/java/app/AndroidManifest.xml +++ b/platform/android/java/app/AndroidManifest.xml @@ -27,7 +27,6 @@ - diff --git a/platform/android/java/app/config.gradle b/platform/android/java/app/config.gradle index d0b3e626671..fb296d072fa 100644 --- a/platform/android/java/app/config.gradle +++ b/platform/android/java/app/config.gradle @@ -1,9 +1,9 @@ ext.versions = [ androidGradlePlugin: '7.0.3', - compileSdk : 31, + compileSdk : 32, minSdk : 19, // Also update 'platform/android/java/lib/AndroidManifest.xml#minSdkVersion' & 'platform/android/export/export_plugin.cpp#DEFAULT_MIN_SDK_VERSION' - targetSdk : 31, // Also update 'platform/android/java/lib/AndroidManifest.xml#targetSdkVersion' & 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION' - buildTools : '30.0.3', + targetSdk : 32, // Also update 'platform/android/java/lib/AndroidManifest.xml#targetSdkVersion' & 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION' + buildTools : '32.0.0', kotlinVersion : '1.6.21', fragmentVersion : '1.3.6', nexusPublishVersion: '1.1.0', diff --git a/platform/android/java/editor/build.gradle b/platform/android/java/editor/build.gradle index 9cadd0a755a..bdd725d76d9 100644 --- a/platform/android/java/editor/build.gradle +++ b/platform/android/java/editor/build.gradle @@ -23,8 +23,7 @@ android { versionCode getGodotLibraryVersionCode() versionName getGodotLibraryVersionName() minSdkVersion versions.minSdk - //noinspection ExpiredTargetSdkVersion - Restrict to version 29 until https://github.com/godotengine/godot/pull/51815 is submitted - targetSdkVersion 29 // versions.targetSdk + targetSdkVersion versions.targetSdk missingDimensionStrategy 'products', 'editor' } diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml index 93cbb47400b..abf506a83c2 100644 --- a/platform/android/java/editor/src/main/AndroidManifest.xml +++ b/platform/android/java/editor/src/main/AndroidManifest.xml @@ -14,8 +14,12 @@ android:glEsVersion="0x00020000" android:required="true" /> - - + + + = Build.VERSION_CODES.R) { + if (!Environment.isExternalStorageManager()) { + Toast.makeText( + this, + R.string.denied_storage_permission_error_msg, + Toast.LENGTH_LONG + ).show() + } + } + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + // Check if we got access to the necessary storage permissions + if (requestCode == PermissionsUtil.REQUEST_ALL_PERMISSION_REQ_CODE) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + var hasReadAccess = false + var hasWriteAccess = false + for (i in permissions.indices) { + if (Manifest.permission.READ_EXTERNAL_STORAGE == permissions[i] && grantResults[i] == PackageManager.PERMISSION_GRANTED) { + hasReadAccess = true + } + if (Manifest.permission.WRITE_EXTERNAL_STORAGE == permissions[i] && grantResults[i] == PackageManager.PERMISSION_GRANTED) { + hasWriteAccess = true + } + } + if (!hasReadAccess || !hasWriteAccess) { + Toast.makeText( + this, + R.string.denied_storage_permission_error_msg, + Toast.LENGTH_LONG + ).show() + } + } + } + } } diff --git a/platform/android/java/editor/src/main/res/values/strings.xml b/platform/android/java/editor/src/main/res/values/strings.xml index fed1308ef63..162909f06ab 100644 --- a/platform/android/java/editor/src/main/res/values/strings.xml +++ b/platform/android/java/editor/src/main/res/values/strings.xml @@ -1,4 +1,6 @@ Godot Editor + + Missing storage access permission! diff --git a/platform/android/java/lib/AndroidManifest.xml b/platform/android/java/lib/AndroidManifest.xml index 228d8d45fa9..79b5aadf2a8 100644 --- a/platform/android/java/lib/AndroidManifest.xml +++ b/platform/android/java/lib/AndroidManifest.xml @@ -5,7 +5,7 @@ android:versionName="1.0"> - + diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.java b/platform/android/java/lib/src/org/godotengine/godot/Godot.java index b8554583697..c397928e365 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/Godot.java +++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.java @@ -34,6 +34,8 @@ import static android.content.Context.MODE_PRIVATE; import static android.content.Context.WINDOW_SERVICE; import org.godotengine.godot.input.GodotEditText; +import org.godotengine.godot.io.directory.DirectoryAccessHandler; +import org.godotengine.godot.io.file.FileAccessHandler; import org.godotengine.godot.plugin.GodotPlugin; import org.godotengine.godot.plugin.GodotPluginRegistry; import org.godotengine.godot.utils.GodotNetUtils; @@ -250,8 +252,8 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC private Sensor mMagnetometer; private Sensor mGyroscope; - public static GodotIO io; - public static GodotNetUtils netUtils; + public GodotIO io; + public GodotNetUtils netUtils; static SingletonBase[] singletons = new SingletonBase[MAX_SINGLETONS]; static int singleton_count = 0; @@ -572,15 +574,17 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC final Activity activity = getActivity(); io = new GodotIO(activity); - GodotLib.io = io; netUtils = new GodotNetUtils(activity); + Context context = getContext(); + DirectoryAccessHandler directoryAccessHandler = new DirectoryAccessHandler(context); + FileAccessHandler fileAccessHandler = new FileAccessHandler(context); mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE); mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); mGravity = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY); mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); mGyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); - GodotLib.initialize(activity, this, activity.getAssets(), use_apk_expansion); + GodotLib.initialize(activity, this, activity.getAssets(), io, netUtils, directoryAccessHandler, fileAccessHandler, use_apk_expansion); result_callback = null; diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java index dd794200102..b482a29f258 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotIO.java @@ -36,7 +36,6 @@ import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.pm.ActivityInfo; -import android.content.res.AssetManager; import android.graphics.Point; import android.graphics.Rect; import android.net.Uri; @@ -46,12 +45,10 @@ import android.provider.Settings; import android.text.TextUtils; import android.util.DisplayMetrics; import android.util.Log; -import android.util.SparseArray; import android.view.Display; import android.view.DisplayCutout; import android.view.WindowInsets; -import java.io.IOException; import java.util.List; import java.util.Locale; @@ -60,7 +57,6 @@ import java.util.Locale; public class GodotIO { private static final String TAG = GodotIO.class.getSimpleName(); - private final AssetManager am; private final Activity activity; private final String uniqueId; GodotEditText edit; @@ -73,100 +69,8 @@ public class GodotIO { final int SCREEN_SENSOR_PORTRAIT = 5; final int SCREEN_SENSOR = 6; - ///////////////////////// - /// DIRECTORIES - ///////////////////////// - - static class AssetDir { - public String[] files; - public int current; - public String path; - } - - private int last_dir_id = 1; - - private final SparseArray dirs; - - public int dir_open(String path) { - AssetDir ad = new AssetDir(); - ad.current = 0; - ad.path = path; - - try { - ad.files = am.list(path); - // no way to find path is directory or file exactly. - // but if ad.files.length==0, then it's an empty directory or file. - if (ad.files.length == 0) { - return -1; - } - } catch (IOException e) { - System.out.printf("Exception on dir_open: %s\n", e); - return -1; - } - - ++last_dir_id; - dirs.put(last_dir_id, ad); - - return last_dir_id; - } - - public boolean dir_is_dir(int id) { - if (dirs.get(id) == null) { - System.out.printf("dir_next: invalid dir id: %d\n", id); - return false; - } - AssetDir ad = dirs.get(id); - //System.out.printf("go next: %d,%d\n",ad.current,ad.files.length); - int idx = ad.current; - if (idx > 0) - idx--; - - if (idx >= ad.files.length) - return false; - String fname = ad.files[idx]; - - try { - if (ad.path.equals("")) - am.open(fname); - else - am.open(ad.path + "/" + fname); - return false; - } catch (Exception e) { - return true; - } - } - - public String dir_next(int id) { - if (dirs.get(id) == null) { - System.out.printf("dir_next: invalid dir id: %d\n", id); - return ""; - } - - AssetDir ad = dirs.get(id); - //System.out.printf("go next: %d,%d\n",ad.current,ad.files.length); - - if (ad.current >= ad.files.length) { - ad.current++; - return ""; - } - String r = ad.files[ad.current]; - ad.current++; - return r; - } - - public void dir_close(int id) { - if (dirs.get(id) == null) { - System.out.printf("dir_close: invalid dir id: %d\n", id); - return; - } - - dirs.remove(id); - } - GodotIO(Activity p_activity) { - am = p_activity.getAssets(); activity = p_activity; - dirs = new SparseArray<>(); String androidId = Settings.Secure.getString(activity.getContentResolver(), Settings.Secure.ANDROID_ID); if (androidId == null) { diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java index 4c5ddec0313..bf0c6c51d6d 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -30,7 +30,12 @@ package org.godotengine.godot; +import org.godotengine.godot.io.directory.DirectoryAccessHandler; +import org.godotengine.godot.io.file.FileAccessHandler; +import org.godotengine.godot.utils.GodotNetUtils; + import android.app.Activity; +import android.content.res.AssetManager; import android.hardware.SensorEvent; import javax.microedition.khronos.egl.EGLConfig; @@ -40,8 +45,6 @@ import javax.microedition.khronos.opengles.GL10; * Wrapper for native library */ public class GodotLib { - public static GodotIO io; - static { System.loadLibrary("godot_android"); } @@ -49,7 +52,7 @@ public class GodotLib { /** * Invoked on the main thread to initialize Godot native layer. */ - public static native void initialize(Activity activity, Godot p_instance, Object p_asset_manager, boolean use_apk_expansion); + public static native void initialize(Activity activity, Godot p_instance, AssetManager p_asset_manager, GodotIO godotIO, GodotNetUtils netUtils, DirectoryAccessHandler directoryAccessHandler, FileAccessHandler fileAccessHandler, boolean use_apk_expansion); /** * Invoked on the main thread to clean up Godot native layer. diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt new file mode 100644 index 00000000000..c7bd55b6208 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt @@ -0,0 +1,114 @@ +/*************************************************************************/ +/* StorageScope.kt */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 + +import android.content.Context +import android.os.Build +import android.os.Environment +import java.io.File + +/** + * Represents the different storage scopes. + */ +internal enum class StorageScope { + /** + * Covers internal and external directories accessible to the app without restrictions. + */ + APP, + + /** + * Covers shared directories (from Android 10 and higher). + */ + SHARED, + + /** + * Everything else.. + */ + UNKNOWN; + + companion object { + /** + * Determines which [StorageScope] the given path falls under. + */ + fun getStorageScope(context: Context, path: String?): StorageScope { + if (path == null) { + return UNKNOWN + } + + val pathFile = File(path) + if (!pathFile.isAbsolute) { + return UNKNOWN + } + + val canonicalPathFile = pathFile.canonicalPath + + val internalAppDir = context.filesDir.canonicalPath ?: return UNKNOWN + if (canonicalPathFile.startsWith(internalAppDir)) { + return APP + } + + val internalCacheDir = context.cacheDir.canonicalPath ?: return UNKNOWN + if (canonicalPathFile.startsWith(internalCacheDir)) { + return APP + } + + val externalAppDir = context.getExternalFilesDir(null)?.canonicalPath ?: return UNKNOWN + if (canonicalPathFile.startsWith(externalAppDir)) { + return APP + } + + val sharedDir = Environment.getExternalStorageDirectory().canonicalPath ?: return UNKNOWN + if (canonicalPathFile.startsWith(sharedDir)) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + // Before R, apps had access to shared storage so long as they have the right + // permissions (and flag on Q). + return APP + } + + // Post R, access is limited based on the target destination + // 'Downloads' and 'Documents' are still accessible + val downloadsSharedDir = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).canonicalPath + ?: return SHARED + val documentsSharedDir = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).canonicalPath + ?: return SHARED + if (canonicalPathFile.startsWith(downloadsSharedDir) || canonicalPathFile.startsWith(documentsSharedDir)) { + return APP + } + + return SHARED + } + + return UNKNOWN + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt new file mode 100644 index 00000000000..098b10ae36c --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt @@ -0,0 +1,177 @@ +/*************************************************************************/ +/* AssetsDirectoryAccess.kt */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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.directory + +import android.content.Context +import android.util.Log +import android.util.SparseArray +import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.INVALID_DIR_ID +import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_DIR_ID +import java.io.File +import java.io.IOException + +/** + * Handles directories access within the Android assets directory. + */ +internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.DirectoryAccess { + + companion object { + private val TAG = AssetsDirectoryAccess::class.java.simpleName + } + + private data class AssetDir(val path: String, val files: Array, var current: Int = 0) + + private val assetManager = context.assets + + private var lastDirId = STARTING_DIR_ID + private val dirs = SparseArray() + + private fun getAssetsPath(originalPath: String): String { + if (originalPath.startsWith(File.separatorChar)) { + return originalPath.substring(1) + } + return originalPath + } + + override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0 + + override fun dirOpen(path: String): Int { + val assetsPath = getAssetsPath(path) ?: return INVALID_DIR_ID + 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.isEmpty()) { + return INVALID_DIR_ID + } + + val ad = AssetDir(assetsPath, files) + + dirs.put(++lastDirId, ad) + return lastDirId + } catch (e: IOException) { + Log.e(TAG, "Exception on dirOpen", e) + return INVALID_DIR_ID + } + } + + override fun dirExists(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.isNotEmpty() + } catch (e: IOException) { + Log.e(TAG, "Exception on dirExists", e) + return false + } + } + + override fun fileExists(path: String): Boolean { + val assetsPath = getAssetsPath(path) ?: return false + 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 { + val ad: AssetDir = dirs[dirId] + + var idx = ad.current + if (idx > 0) { + idx-- + } + + if (idx >= ad.files.size) { + return false + } + + val fileName = ad.files[idx] + // List the contents of $fileName. If it's a file, it will be empty, otherwise it'll be a + // directory + val filePath = if (ad.path == "") fileName else "${ad.path}/${fileName}" + val fileContents = assetManager.list(filePath) + return (fileContents?.size?: 0) > 0 + } + + override fun isCurrentHidden(dirId: Int): Boolean { + val ad = dirs[dirId] + + var idx = ad.current + if (idx > 0) { + idx-- + } + + if (idx >= ad.files.size) { + return false + } + + val fileName = ad.files[idx] + return fileName.startsWith('.') + } + + override fun dirNext(dirId: Int): String { + val ad: AssetDir = dirs[dirId] + + if (ad.current >= ad.files.size) { + ad.current++ + return "" + } + + return ad.files[ad.current++] + } + + override fun dirClose(dirId: Int) { + dirs.remove(dirId) + } + + override fun getDriveCount() = 0 + + override fun getDrive(drive: Int) = "" + + override fun makeDir(dir: String) = false + + override fun getSpaceLeft() = 0L + + override fun rename(from: String, to: String) = false + + override fun remove(filename: String) = false +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt new file mode 100644 index 00000000000..fedcf4843fa --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt @@ -0,0 +1,224 @@ +/*************************************************************************/ +/* DirectoryAccessHandler.kt */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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.directory + +import android.content.Context +import android.util.Log +import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_FILESYSTEM +import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_RESOURCES + +/** + * Handles files and directories access and manipulation for the Android platform + */ +class DirectoryAccessHandler(context: Context) { + + companion object { + private val TAG = DirectoryAccessHandler::class.java.simpleName + + 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) + } + + internal interface DirectoryAccess { + fun dirOpen(path: String): Int + fun dirNext(dirId: Int): String + fun dirClose(dirId: Int) + fun dirIsDir(dirId: Int): Boolean + fun dirExists(path: String): Boolean + fun fileExists(path: String): Boolean + fun hasDirId(dirId: Int): Boolean + fun isCurrentHidden(dirId: Int): Boolean + fun getDriveCount() : Int + fun getDrive(drive: Int): String + fun makeDir(dir: String): Boolean + fun getSpaceLeft(): Long + fun rename(from: String, to: String): Boolean + fun remove(filename: String): Boolean + } + + private val assetsDirAccess = AssetsDirectoryAccess(context) + private val fileSystemDirAccess = FilesystemDirectoryAccess(context) + + private fun hasDirId(accessType: AccessType, dirId: Int): Boolean { + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId) + ACCESS_FILESYSTEM -> fileSystemDirAccess.hasDirId(dirId) + } + } + + fun dirOpen(nativeAccessType: Int, path: String?): Int { + val accessType = getAccessTypeFromNative(nativeAccessType) + if (path == null || accessType == null) { + return INVALID_DIR_ID + } + + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.dirOpen(path) + ACCESS_FILESYSTEM -> fileSystemDirAccess.dirOpen(path) + } + } + + fun dirNext(nativeAccessType: Int, dirId: Int): String { + val accessType = getAccessTypeFromNative(nativeAccessType) + if (accessType == null || !hasDirId(accessType, dirId)) { + Log.w(TAG, "dirNext: Invalid dir id: $dirId") + return "" + } + + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.dirNext(dirId) + ACCESS_FILESYSTEM -> fileSystemDirAccess.dirNext(dirId) + } + } + + fun dirClose(nativeAccessType: Int, dirId: Int) { + val accessType = getAccessTypeFromNative(nativeAccessType) + if (accessType == null || !hasDirId(accessType, dirId)) { + Log.w(TAG, "dirClose: Invalid dir id: $dirId") + return + } + + when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.dirClose(dirId) + ACCESS_FILESYSTEM -> fileSystemDirAccess.dirClose(dirId) + } + } + + fun dirIsDir(nativeAccessType: Int, dirId: Int): Boolean { + val accessType = getAccessTypeFromNative(nativeAccessType) + if (accessType == null || !hasDirId(accessType, dirId)) { + Log.w(TAG, "dirIsDir: Invalid dir id: $dirId") + return false + } + + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.dirIsDir(dirId) + ACCESS_FILESYSTEM -> fileSystemDirAccess.dirIsDir(dirId) + } + } + + fun isCurrentHidden(nativeAccessType: Int, dirId: Int): Boolean { + val accessType = getAccessTypeFromNative(nativeAccessType) + if (accessType == null || !hasDirId(accessType, dirId)) { + return false + } + + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.isCurrentHidden(dirId) + ACCESS_FILESYSTEM -> fileSystemDirAccess.isCurrentHidden(dirId) + } + } + + fun dirExists(nativeAccessType: Int, path: String?): Boolean { + val accessType = getAccessTypeFromNative(nativeAccessType) + if (path == null || accessType == null) { + return false + } + + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.dirExists(path) + ACCESS_FILESYSTEM -> fileSystemDirAccess.dirExists(path) + } + } + + fun fileExists(nativeAccessType: Int, path: String?): Boolean { + val accessType = getAccessTypeFromNative(nativeAccessType) + if (path == null || accessType == null) { + return false + } + + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.fileExists(path) + ACCESS_FILESYSTEM -> fileSystemDirAccess.fileExists(path) + } + } + + fun getDriveCount(nativeAccessType: Int): Int { + val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0 + return when(accessType) { + ACCESS_RESOURCES -> assetsDirAccess.getDriveCount() + ACCESS_FILESYSTEM -> fileSystemDirAccess.getDriveCount() + } + } + + fun getDrive(nativeAccessType: Int, drive: Int): String { + val accessType = getAccessTypeFromNative(nativeAccessType) ?: return "" + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.getDrive(drive) + ACCESS_FILESYSTEM -> fileSystemDirAccess.getDrive(drive) + } + } + + fun makeDir(nativeAccessType: Int, dir: String): Boolean { + val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.makeDir(dir) + ACCESS_FILESYSTEM -> fileSystemDirAccess.makeDir(dir) + } + } + + fun getSpaceLeft(nativeAccessType: Int): Long { + val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0L + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.getSpaceLeft() + ACCESS_FILESYSTEM -> fileSystemDirAccess.getSpaceLeft() + } + } + + fun rename(nativeAccessType: Int, from: String, to: String): Boolean { + val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.rename(from, to) + ACCESS_FILESYSTEM -> fileSystemDirAccess.rename(from, to) + } + } + + fun remove(nativeAccessType: Int, filename: String): Boolean { + val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false + return when (accessType) { + ACCESS_RESOURCES -> assetsDirAccess.remove(filename) + ACCESS_FILESYSTEM -> fileSystemDirAccess.remove(filename) + } + } + +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt new file mode 100644 index 00000000000..c3acf425684 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt @@ -0,0 +1,230 @@ +/*************************************************************************/ +/* FileSystemDirectoryAccess.kt */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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.directory + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Build +import android.os.storage.StorageManager +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.FileAccessHandler +import java.io.File + +/** + * Handles directories access with the internal and external filesystem. + */ +internal class FilesystemDirectoryAccess(private val context: Context): + DirectoryAccessHandler.DirectoryAccess { + + companion object { + private val TAG = FilesystemDirectoryAccess::class.java.simpleName + } + + private data class DirData(val dirFile: File, val files: Array, var current: Int = 0) + + private val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager + private var lastDirId = STARTING_DIR_ID + private val dirs = SparseArray() + + private fun inScope(path: String): Boolean { + // 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 StorageScope.getStorageScope(context, path) != StorageScope.UNKNOWN + } + + override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0 + + override fun dirOpen(path: String): Int { + if (!inScope(path)) { + Log.w(TAG, "Path $path is not accessible.") + return INVALID_DIR_ID + } + + // Check this is a directory. + val dirFile = File(path) + if (!dirFile.isDirectory) { + return INVALID_DIR_ID + } + + // Get the files in the directory + val files = dirFile.listFiles()?: return INVALID_DIR_ID + + // Create the data representing this directory + val dirData = DirData(dirFile, files) + + dirs.put(++lastDirId, dirData) + return lastDirId + } + + override fun dirExists(path: String): Boolean { + if (!inScope(path)) { + Log.w(TAG, "Path $path is not accessible.") + return false + } + + try { + return File(path).isDirectory + } catch (e: SecurityException) { + return false + } + } + + override fun fileExists(path: String) = FileAccessHandler.fileExists(context, path) + + override fun dirNext(dirId: Int): String { + val dirData = dirs[dirId] + if (dirData.current >= dirData.files.size) { + dirData.current++ + return "" + } + + return dirData.files[dirData.current++].name + } + + override fun dirClose(dirId: Int) { + dirs.remove(dirId) + } + + override fun dirIsDir(dirId: Int): Boolean { + val dirData = dirs[dirId] + + var index = dirData.current + if (index > 0) { + index-- + } + + if (index >= dirData.files.size) { + return false + } + + return dirData.files[index].isDirectory + } + + override fun isCurrentHidden(dirId: Int): Boolean { + val dirData = dirs[dirId] + + var index = dirData.current + if (index > 0) { + index-- + } + + if (index >= dirData.files.size) { + return false + } + + return dirData.files[index].isHidden + } + + override fun getDriveCount(): Int { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + storageManager.storageVolumes.size + } else { + 0 + } + } + + override fun getDrive(drive: Int): String { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return "" + } + + if (drive < 0 || drive >= storageManager.storageVolumes.size) { + return "" + } + + val storageVolume = storageManager.storageVolumes[drive] + return storageVolume.getDescription(context) + } + + override fun makeDir(dir: String): Boolean { + if (!inScope(dir)) { + Log.w(TAG, "Directory $dir is not accessible.") + return false + } + + try { + val dirFile = File(dir) + return dirFile.isDirectory || dirFile.mkdirs() + } catch (e: SecurityException) { + return false + } + } + + @SuppressLint("UsableSpace") + override fun getSpaceLeft() = context.getExternalFilesDir(null)?.usableSpace ?: 0L + + override fun rename(from: String, to: String): Boolean { + if (!inScope(from) || !inScope(to)) { + Log.w(TAG, "Argument filenames are not accessible:\n" + + "from: $from\n" + + "to: $to") + return false + } + + return try { + val fromFile = File(from) + if (fromFile.isDirectory) { + fromFile.renameTo(File(to)) + } else { + FileAccessHandler.renameFile(context, from, to) + } + } catch (e: SecurityException) { + false + } + } + + override fun remove(filename: String): Boolean { + if (!inScope(filename)) { + Log.w(TAG, "Filename $filename is not accessible.") + return false + } + + return try { + val deleteFile = File(filename) + if (deleteFile.exists()) { + if (deleteFile.isDirectory) { + deleteFile.delete() + } else { + FileAccessHandler.removeFile(context, filename) + } + } else { + true + } + } catch (e: SecurityException) { + false + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt new file mode 100644 index 00000000000..0fe51662a4d --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt @@ -0,0 +1,174 @@ +/*************************************************************************/ +/* DataAccess.kt */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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.os.Build +import android.util.Log +import org.godotengine.godot.io.StorageScope +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.channels.FileChannel +import kotlin.math.max + +/** + * Base class for file IO operations. + * + * 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) { + + companion object { + private val TAG = DataAccess::class.java.simpleName + + fun generateDataAccess( + storageScope: StorageScope, + context: Context, + filePath: String, + accessFlag: FileAccessFlags + ): DataAccess? { + return when (storageScope) { + StorageScope.APP -> FileData(filePath, accessFlag) + + StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + MediaStoreData(context, filePath, accessFlag) + } else { + null + } + + StorageScope.UNKNOWN -> null + } + } + + fun fileExists(storageScope: StorageScope, context: Context, path: String): Boolean { + return when(storageScope) { + StorageScope.APP -> FileData.fileExists(path) + StorageScope.SHARED -> MediaStoreData.fileExists(context, path) + StorageScope.UNKNOWN -> false + } + } + + fun fileLastModified(storageScope: StorageScope, context: Context, path: String): Long { + return when(storageScope) { + StorageScope.APP -> FileData.fileLastModified(path) + StorageScope.SHARED -> MediaStoreData.fileLastModified(context, path) + StorageScope.UNKNOWN -> 0L + } + } + + fun removeFile(storageScope: StorageScope, context: Context, path: String): Boolean { + return when(storageScope) { + StorageScope.APP -> FileData.delete(path) + StorageScope.SHARED -> MediaStoreData.delete(context, path) + StorageScope.UNKNOWN -> false + } + } + + 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.UNKNOWN -> false + } + } + } + + protected abstract val fileChannel: FileChannel + + 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) + } catch (e: Exception) { + Log.w(TAG, "Exception when seeking file $filePath.", e) + } + } + + fun seekFromEnd(positionFromEnd: Long) { + val positionFromBeginning = max(0, size() - positionFromEnd) + seek(positionFromBeginning) + } + + fun position(): Long { + return try { + fileChannel.position() + } catch (e: IOException) { + Log.w( + TAG, + "Exception when retrieving position 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 isEndOfFile() = position() >= size() + + fun read(buffer: ByteBuffer): Int { + return try { + fileChannel.read(buffer) + } catch (e: IOException) { + Log.w(TAG, "Exception while reading from file $filePath.", e) + 0 + } + } + + fun write(buffer: ByteBuffer) { + try { + fileChannel.write(buffer) + } catch (e: IOException) { + Log.w(TAG, "Exception while writing to file $filePath.", e) + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt new file mode 100644 index 00000000000..c6b242a4b65 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt @@ -0,0 +1,87 @@ +/*************************************************************************/ +/* FileAccessFlags.kt */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 + +/** + * Android representation of Godot native access flags. + */ +internal enum class FileAccessFlags(val nativeValue: Int) { + /** + * Opens the file for read operations. + * The cursor is positioned at the beginning of the file. + */ + READ(1), + + /** + * Opens the file for write operations. + * The file is created if it does not exist, and truncated if it does. + */ + WRITE(2), + + /** + * Opens the file for read and write operations. + * Does not truncate the file. The cursor is positioned at the beginning of the file. + */ + READ_WRITE(3), + + /** + * Opens the file for read and write operations. + * The file is created if it does not exist, and truncated if it does. + * The cursor is positioned at the beginning of the file. + */ + WRITE_READ(7); + + fun getMode(): String { + return when (this) { + READ -> "r" + WRITE -> "w" + READ_WRITE, WRITE_READ -> "rw" + } + } + + fun shouldTruncate(): Boolean { + return when (this) { + READ, READ_WRITE -> false + WRITE, WRITE_READ -> true + } + } + + companion object { + fun fromNativeModeFlags(modeFlag: Int): FileAccessFlags? { + for (flag in values()) { + if (flag.nativeValue == modeFlag) { + return flag + } + } + return null + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt new file mode 100644 index 00000000000..e493f2449e2 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt @@ -0,0 +1,198 @@ +/*************************************************************************/ +/* FileAccessHandler.kt */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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.util.Log +import android.util.SparseArray +import org.godotengine.godot.io.StorageScope +import java.nio.ByteBuffer + +/** + * Handles regular and media store file access and interactions. + */ +class FileAccessHandler(val context: Context) { + + companion object { + private val TAG = FileAccessHandler::class.java.simpleName + + private const val INVALID_FILE_ID = 0 + private const val STARTING_FILE_ID = 1 + + fun fileExists(context: Context, path: String?): Boolean { + val storageScope = StorageScope.getStorageScope(context, path) + if (storageScope == StorageScope.UNKNOWN) { + return false + } + + return try { + DataAccess.fileExists(storageScope, context, path!!) + } catch (e: SecurityException) { + false + } + } + + fun removeFile(context: Context, path: String?): Boolean { + val storageScope = StorageScope.getStorageScope(context, path) + if (storageScope == StorageScope.UNKNOWN) { + return false + } + + return try { + DataAccess.removeFile(storageScope, context, path!!) + } catch (e: Exception) { + false + } + } + + fun renameFile(context: Context, from: String?, to: String?): Boolean { + val storageScope = StorageScope.getStorageScope(context, from) + if (storageScope == StorageScope.UNKNOWN) { + return false + } + + return try { + DataAccess.renameFile(storageScope, context, from!!, to!!) + } catch (e: Exception) { + false + } + } + } + + private val files = SparseArray() + private var lastFileId = STARTING_FILE_ID + + private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0 + + fun fileOpen(path: String?, modeFlags: Int): Int { + val storageScope = StorageScope.getStorageScope(context, path) + if (storageScope == StorageScope.UNKNOWN) { + return INVALID_FILE_ID + } + + try { + val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID + val dataAccess = DataAccess.generateDataAccess(storageScope, context, path!!, accessFlag) ?: return INVALID_FILE_ID + + files.put(++lastFileId, dataAccess) + return lastFileId + } catch (e: Exception) { + Log.w(TAG, "Error while opening $path", e) + return INVALID_FILE_ID + } + } + + fun fileGetSize(fileId: Int): Long { + if (!hasFileId(fileId)) { + return 0L + } + + return files[fileId].size() + } + + fun fileSeek(fileId: Int, position: Long) { + if (!hasFileId(fileId)) { + return + } + + files[fileId].seek(position) + } + + fun fileSeekFromEnd(fileId: Int, position: Long) { + if (!hasFileId(fileId)) { + return + } + + files[fileId].seekFromEnd(position) + } + + fun fileRead(fileId: Int, byteBuffer: ByteBuffer?): Int { + if (!hasFileId(fileId) || byteBuffer == null) { + return 0 + } + + return files[fileId].read(byteBuffer) + } + + fun fileWrite(fileId: Int, byteBuffer: ByteBuffer?) { + if (!hasFileId(fileId) || byteBuffer == null) { + return + } + + files[fileId].write(byteBuffer) + } + + fun fileFlush(fileId: Int) { + if (!hasFileId(fileId)) { + return + } + + files[fileId].flush() + } + + fun fileExists(path: String?) = Companion.fileExists(context, path) + + fun fileLastModified(filepath: String?): Long { + val storageScope = StorageScope.getStorageScope(context, filepath) + if (storageScope == StorageScope.UNKNOWN) { + return 0L + } + + return try { + DataAccess.fileLastModified(storageScope, context, filepath!!) + } catch (e: SecurityException) { + 0L + } + } + + fun fileGetPosition(fileId: Int): Long { + if (!hasFileId(fileId)) { + return 0L + } + + return files[fileId].position() + } + + fun isFileEof(fileId: Int): Boolean { + if (!hasFileId(fileId)) { + return false + } + + return files[fileId].isEndOfFile() + } + + fun fileClose(fileId: Int) { + if (hasFileId(fileId)) { + files[fileId].close() + files.remove(fileId) + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt new file mode 100644 index 00000000000..5af694ad990 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt @@ -0,0 +1,93 @@ +/*************************************************************************/ +/* FileData.kt */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 java.io.File +import java.io.FileOutputStream +import java.io.RandomAccessFile +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) { + + companion object { + private val TAG = FileData::class.java.simpleName + + fun fileExists(path: String): Boolean { + return try { + File(path).isFile + } catch (e: SecurityException) { + false + } + } + + fun fileLastModified(filepath: String): Long { + return try { + File(filepath).lastModified() + } catch (e: SecurityException) { + 0L + } + } + + fun delete(filepath: String): Boolean { + return try { + File(filepath).delete() + } catch (e: Exception) { + false + } + } + + fun rename(from: String, to: String): Boolean { + return try { + val fromFile = File(from) + fromFile.renameTo(File(to)) + } catch (e: Exception) { + false + } + } + } + + override val fileChannel: FileChannel + + init { + if (accessFlag == FileAccessFlags.WRITE) { + fileChannel = FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel + } else { + fileChannel = RandomAccessFile(filePath, accessFlag.getMode()).channel + } + + if (accessFlag.shouldTruncate()) { + fileChannel.truncate(0) + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt new file mode 100644 index 00000000000..81a7dd17058 --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt @@ -0,0 +1,284 @@ +/*************************************************************************/ +/* MediaStoreData.kt */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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.ContentUris +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.MediaStore +import androidx.annotation.RequiresApi + +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.nio.channels.FileChannel + +/** + * Implementation of [DataAccess] which handles access and interactions with file and data + * under scoped storage via the MediaStore API. + */ +@RequiresApi(Build.VERSION_CODES.Q) +internal class MediaStoreData(context: Context, filePath: String, accessFlag: FileAccessFlags) : + DataAccess(filePath) { + + private data class DataItem( + val id: Long, + val uri: Uri, + val displayName: String, + val relativePath: String, + val size: Int, + val dateModified: Int, + val mediaType: Int + ) + + companion object { + private val TAG = MediaStoreData::class.java.simpleName + + private val COLLECTION = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) + + private val PROJECTION = arrayOf( + MediaStore.Files.FileColumns._ID, + MediaStore.Files.FileColumns.DISPLAY_NAME, + MediaStore.Files.FileColumns.RELATIVE_PATH, + MediaStore.Files.FileColumns.SIZE, + MediaStore.Files.FileColumns.DATE_MODIFIED, + MediaStore.Files.FileColumns.MEDIA_TYPE, + ) + + private const val SELECTION_BY_PATH = "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ? " + + " AND ${MediaStore.Files.FileColumns.RELATIVE_PATH} = ?" + + private fun getSelectionByPathArguments(path: String): Array { + return arrayOf(getMediaStoreDisplayName(path), getMediaStoreRelativePath(path)) + } + + private const val SELECTION_BY_ID = "${MediaStore.Files.FileColumns._ID} = ? " + + private fun getSelectionByIdArgument(id: Long) = arrayOf(id.toString()) + + private fun getMediaStoreDisplayName(path: String) = File(path).name + + private fun getMediaStoreRelativePath(path: String): String { + val pathFile = File(path) + val environmentDir = Environment.getExternalStorageDirectory() + var relativePath = (pathFile.parent?.replace(environmentDir.absolutePath, "") ?: "").trim('/') + if (relativePath.isNotBlank()) { + relativePath += "/" + } + return relativePath + } + + private fun queryById(context: Context, id: Long): List { + val query = context.contentResolver.query( + COLLECTION, + PROJECTION, + SELECTION_BY_ID, + getSelectionByIdArgument(id), + null + ) + return dataItemFromCursor(query) + } + + private fun queryByPath(context: Context, path: String): List { + val query = context.contentResolver.query( + COLLECTION, + PROJECTION, + SELECTION_BY_PATH, + getSelectionByPathArguments(path), + null + ) + return dataItemFromCursor(query) + } + + private fun dataItemFromCursor(query: Cursor?): List { + query?.use { cursor -> + cursor.count + if (cursor.count == 0) { + return emptyList() + } + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID) + val displayNameColumn = + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME) + val relativePathColumn = + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.RELATIVE_PATH) + val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE) + val dateModifiedColumn = + cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED) + val mediaTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE) + + val result = ArrayList() + while (cursor.moveToNext()) { + val id = cursor.getLong(idColumn) + result.add( + DataItem( + id, + ContentUris.withAppendedId(COLLECTION, id), + cursor.getString(displayNameColumn), + cursor.getString(relativePathColumn), + cursor.getInt(sizeColumn), + cursor.getInt(dateModifiedColumn), + cursor.getInt(mediaTypeColumn) + ) + ) + } + return result + } + return emptyList() + } + + private fun addFile(context: Context, path: String): DataItem? { + val fileDetails = ContentValues().apply { + put(MediaStore.Files.FileColumns._ID, 0) + put(MediaStore.Files.FileColumns.DISPLAY_NAME, getMediaStoreDisplayName(path)) + put(MediaStore.Files.FileColumns.RELATIVE_PATH, getMediaStoreRelativePath(path)) + } + + context.contentResolver.insert(COLLECTION, fileDetails) ?: return null + + // File was successfully added, let's retrieve its info + val infos = queryByPath(context, path) + if (infos.isEmpty()) { + return null + } + + return infos[0] + } + + fun delete(context: Context, path: String): Boolean { + val itemsToDelete = queryByPath(context, path) + if (itemsToDelete.isEmpty()) { + return false + } + + val resolver = context.contentResolver + var itemsDeleted = 0 + for (item in itemsToDelete) { + itemsDeleted += resolver.delete(item.uri, null, null) + } + + return itemsDeleted > 0 + } + + fun fileExists(context: Context, path: String): Boolean { + return queryByPath(context, path).isNotEmpty() + } + + fun fileLastModified(context: Context, path: String): Long { + val result = queryByPath(context, path) + if (result.isEmpty()) { + return 0L + } + + val dataItem = result[0] + return dataItem.dateModified.toLong() + } + + fun rename(context: Context, from: String, to: String): Boolean { + // Ensure the source exists. + val sources = queryByPath(context, from) + if (sources.isEmpty()) { + return false + } + + // Take the first source + val source = sources[0] + + // Set up the updated values + val updatedDetails = ContentValues().apply { + put(MediaStore.Files.FileColumns.DISPLAY_NAME, getMediaStoreDisplayName(to)) + put(MediaStore.Files.FileColumns.RELATIVE_PATH, getMediaStoreRelativePath(to)) + } + + val updated = context.contentResolver.update( + source.uri, + updatedDetails, + SELECTION_BY_ID, + getSelectionByIdArgument(source.id) + ) + return updated > 0 + } + } + + private val id: Long + private val uri: Uri + override val fileChannel: FileChannel + + init { + val contentResolver = context.contentResolver + val dataItems = queryByPath(context, filePath) + + val dataItem = when (accessFlag) { + FileAccessFlags.READ -> { + // The file should already exist + if (dataItems.isEmpty()) { + throw FileNotFoundException("Unable to access file $filePath") + } + + val dataItem = dataItems[0] + dataItem + } + + FileAccessFlags.WRITE, FileAccessFlags.READ_WRITE, FileAccessFlags.WRITE_READ -> { + // Create the file if it doesn't exist + val dataItem = if (dataItems.isEmpty()) { + addFile(context, filePath) + } else { + dataItems[0] + } + + if (dataItem == null) { + throw FileNotFoundException("Unable to access file $filePath") + } + dataItem + } + } + + id = dataItem.id + uri = dataItem.uri + + val parcelFileDescriptor = contentResolver.openFileDescriptor(uri, accessFlag.getMode()) + ?: throw IllegalStateException("Unable to access file descriptor") + fileChannel = if (accessFlag == FileAccessFlags.READ) { + FileInputStream(parcelFileDescriptor.fileDescriptor).channel + } else { + FileOutputStream(parcelFileDescriptor.fileDescriptor).channel + } + + if (accessFlag.shouldTruncate()) { + fileChannel.truncate(0) + } + } +} diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java index 9ea787ac867..11cfeb56b13 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java +++ b/platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java @@ -32,10 +32,14 @@ package org.godotengine.godot.utils; import android.Manifest; import android.app.Activity; +import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PermissionInfo; +import android.net.Uri; import android.os.Build; +import android.os.Environment; +import android.provider.Settings; import android.util.Log; import androidx.core.content.ContextCompat; @@ -53,7 +57,8 @@ public final class PermissionsUtil { static final int REQUEST_RECORD_AUDIO_PERMISSION = 1; static final int REQUEST_CAMERA_PERMISSION = 2; static final int REQUEST_VIBRATE_PERMISSION = 3; - static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001; + public static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001; + public static final int REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE = 2002; private PermissionsUtil() { } @@ -108,13 +113,26 @@ public final class PermissionsUtil { if (manifestPermissions.length == 0) return true; - List dangerousPermissions = new ArrayList<>(); + List requestedPermissions = new ArrayList<>(); for (String manifestPermission : manifestPermissions) { try { - PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission); - int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; - if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) { - dangerousPermissions.add(manifestPermission); + if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { + try { + Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION); + intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName()))); + activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE); + } catch (Exception ignored) { + Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); + activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE); + } + } + } else { + PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission); + int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; + if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) { + requestedPermissions.add(manifestPermission); + } } } catch (PackageManager.NameNotFoundException e) { // Skip this permission and continue. @@ -122,13 +140,12 @@ public final class PermissionsUtil { } } - if (dangerousPermissions.isEmpty()) { + if (requestedPermissions.isEmpty()) { // If list is empty, all of dangerous permissions were granted. return true; } - String[] requestedPermissions = dangerousPermissions.toArray(new String[0]); - activity.requestPermissions(requestedPermissions, REQUEST_ALL_PERMISSION_REQ_CODE); + activity.requestPermissions(requestedPermissions.toArray(new String[0]), REQUEST_ALL_PERMISSION_REQ_CODE); return false; } @@ -148,13 +165,19 @@ public final class PermissionsUtil { if (manifestPermissions.length == 0) return manifestPermissions; - List dangerousPermissions = new ArrayList<>(); + List grantedPermissions = new ArrayList<>(); for (String manifestPermission : manifestPermissions) { try { - PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission); - int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; - if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) == PackageManager.PERMISSION_GRANTED) { - dangerousPermissions.add(manifestPermission); + if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) { + grantedPermissions.add(manifestPermission); + } + } else { + PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission); + int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel; + if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) == PackageManager.PERMISSION_GRANTED) { + grantedPermissions.add(manifestPermission); + } } } catch (PackageManager.NameNotFoundException e) { // Skip this permission and continue. @@ -162,7 +185,7 @@ public final class PermissionsUtil { } } - return dangerousPermissions.toArray(new String[0]); + return grantedPermissions.toArray(new String[0]); } /** @@ -177,7 +200,7 @@ public final class PermissionsUtil { if (permission.equals(p)) return true; } - } catch (PackageManager.NameNotFoundException e) { + } catch (PackageManager.NameNotFoundException ignored) { } return false; diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp index f217b34a2d8..ab688d4a8f6 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -40,6 +40,7 @@ #include "core/project_settings.h" #include "dir_access_jandroid.h" #include "file_access_android.h" +#include "file_access_filesystem_jandroid.h" #include "jni_utils.h" #include "main/input_default.h" #include "main/main.h" @@ -118,13 +119,13 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHei } } -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject activity, jobject godot_instance, jobject p_asset_manager, jboolean p_use_apk_expansion) { +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion) { JavaVM *jvm; env->GetJavaVM(&jvm); // create our wrapper classes - godot_java = new GodotJavaWrapper(env, activity, godot_instance); - godot_io_java = new GodotIOJavaWrapper(env, godot_java->get_member_object("io", "Lorg/godotengine/godot/GodotIO;", env)); + godot_java = new GodotJavaWrapper(env, p_activity, p_godot_instance); + godot_io_java = new GodotIOJavaWrapper(env, p_godot_io); init_thread_jandroid(jvm, env); @@ -132,8 +133,9 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *en FileAccessAndroid::asset_manager = AAssetManager_fromJava(env, amgr); - DirAccessJAndroid::setup(godot_io_java->get_instance()); - NetSocketAndroid::setup(godot_java->get_member_object("netUtils", "Lorg/godotengine/godot/utils/GodotNetUtils;", env)); + DirAccessJAndroid::setup(p_directory_access_handler); + FileAccessFilesystemJAndroid::setup(p_file_access_handler); + NetSocketAndroid::setup(p_net_utils); os_android = new OS_Android(godot_java, godot_io_java, p_use_apk_expansion); diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h index b8a638eb82a..570d5a71572 100644 --- a/platform/android/java_godot_lib_jni.h +++ b/platform/android/java_godot_lib_jni.h @@ -37,7 +37,7 @@ // These functions can be called from within JAVA and are the means by which our JAVA implementation calls back into our C++ code. // See java/src/org/godotengine/godot/GodotLib.java for the JAVA side of this (yes that's why we have the long names) extern "C" { -JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject activity, jobject godot_instance, jobject p_asset_manager, jboolean p_use_apk_expansion); +JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline); JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, jclass clazz, jint width, jint height); diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index 700fc37db0c..a18cc7a572b 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -43,6 +43,7 @@ #include "dir_access_jandroid.h" #include "file_access_android.h" +#include "file_access_filesystem_jandroid.h" #include "net_socket_android.h" #include @@ -114,7 +115,7 @@ void OS_Android::initialize_core() { } #endif FileAccess::make_default(FileAccess::ACCESS_USERDATA); - FileAccess::make_default(FileAccess::ACCESS_FILESYSTEM); + FileAccess::make_default(FileAccess::ACCESS_FILESYSTEM); #ifdef TOOLS_ENABLED DirAccess::make_default(DirAccess::ACCESS_RESOURCES); @@ -126,7 +127,7 @@ void OS_Android::initialize_core() { } #endif DirAccess::make_default(DirAccess::ACCESS_USERDATA); - DirAccess::make_default(DirAccess::ACCESS_FILESYSTEM); + DirAccess::make_default(DirAccess::ACCESS_FILESYSTEM); NetSocketAndroid::make_default(); }