From f9c19298ce9ba7a9a9943949b40c757979706a5a Mon Sep 17 00:00:00 2001 From: Fredia Huya-Kouadio Date: Sat, 10 Jul 2021 18:39:31 -0700 Subject: Add full support for Android scoped storage. This was done by refactoring directory and file access handling for the Android platform so that any general filesystem access type go through the Android layer. This allows us to validate whether the access is unrestricted, or whether it falls under scoped storage and thus act appropriately. --- platform/android/SCsub | 1 + platform/android/dir_access_jandroid.cpp | 304 ++++++++++++++------- platform/android/dir_access_jandroid.h | 72 +++-- platform/android/export/export_plugin.cpp | 21 +- platform/android/export/export_plugin.h | 4 +- platform/android/export/gradle_export_util.cpp | 4 +- platform/android/export/gradle_export_util.h | 2 +- platform/android/file_access_android.cpp | 12 +- platform/android/file_access_android.h | 8 +- .../android/file_access_filesystem_jandroid.cpp | 283 +++++++++++++++++++ platform/android/file_access_filesystem_jandroid.h | 97 +++++++ platform/android/java/app/config.gradle | 6 +- platform/android/java/editor/build.gradle | 3 +- .../java/editor/src/main/AndroidManifest.xml | 8 +- .../java/org/godotengine/editor/GodotEditor.kt | 52 +++- .../java/editor/src/main/res/values/strings.xml | 2 + platform/android/java/lib/AndroidManifest.xml | 2 +- .../java/lib/src/org/godotengine/godot/Godot.java | 22 +- .../lib/src/org/godotengine/godot/GodotIO.java | 96 ------- .../lib/src/org/godotengine/godot/GodotLib.java | 17 +- .../src/org/godotengine/godot/io/StorageScope.kt | 114 ++++++++ .../godot/io/directory/AssetsDirectoryAccess.kt | 177 ++++++++++++ .../godot/io/directory/DirectoryAccessHandler.kt | 224 +++++++++++++++ .../io/directory/FilesystemDirectoryAccess.kt | 230 ++++++++++++++++ .../org/godotengine/godot/io/file/DataAccess.kt | 186 +++++++++++++ .../godotengine/godot/io/file/FileAccessFlags.kt | 87 ++++++ .../godotengine/godot/io/file/FileAccessHandler.kt | 202 ++++++++++++++ .../src/org/godotengine/godot/io/file/FileData.kt | 93 +++++++ .../godotengine/godot/io/file/MediaStoreData.kt | 284 +++++++++++++++++++ .../godotengine/godot/utils/PermissionsUtil.java | 55 ++-- platform/android/java_godot_lib_jni.cpp | 14 +- platform/android/java_godot_lib_jni.h | 2 +- platform/android/os_android.cpp | 32 ++- platform/android/os_android.h | 2 + 34 files changed, 2430 insertions(+), 288 deletions(-) create mode 100644 platform/android/file_access_filesystem_jandroid.cpp create mode 100644 platform/android/file_access_filesystem_jandroid.h create mode 100644 platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt create mode 100644 platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt create mode 100644 platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt create mode 100644 platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt create mode 100644 platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt create mode 100644 platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt create mode 100644 platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt create mode 100644 platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt create mode 100644 platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt (limited to 'platform/android') diff --git a/platform/android/SCsub b/platform/android/SCsub index ad226255bc..d370a4d18d 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", "tts_android.cpp", diff --git a/platform/android/dir_access_jandroid.cpp b/platform/android/dir_access_jandroid.cpp index 5b9eee8117..eb344d3b43 100644 --- a/platform/android/dir_access_jandroid.cpp +++ b/platform/android/dir_access_jandroid.cpp @@ -31,30 +31,32 @@ #include "dir_access_jandroid.h" #include "core/string/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; - -Ref 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; @@ -62,169 +64,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.is_empty() || p_dir == "." || (p_dir == ".." && current_dir.is_empty())) { + 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.is_empty()) { - 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(bool p_include_drive) const { - 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_relative_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.is_empty()) { - 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; } - - Ref f; - f.instantiate(); - bool exists = f->file_exists(sd); - - return exists; } bool DirAccessJAndroid::dir_exists(String p_dir) { - JNIEnv *env = get_jni_env(); - - String sd; - - if (current_dir.is_empty()) { - sd = p_dir; + if (_dir_exists) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, false); + + 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_relative_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()); + return false; } +} - jstring js = env->NewStringUTF(path.utf8().get_data()); - int res = env->CallIntMethod(io, _dir_open, js); - env->DeleteLocalRef(js); - if (res <= 0) { - return false; +Error DirAccessJAndroid::make_dir_recursive(String p_dir) { + // Check if the directory exists already + if (dir_exists(p_dir)) { + return ERR_ALREADY_EXISTS; } - env->CallVoidMethod(io, _dir_close, res); + if (_make_dir) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED); - return true; + 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); -Error DirAccessJAndroid::remove(String p_name) { - ERR_FAIL_V(ERR_UNAVAILABLE); + 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; + } } -String DirAccessJAndroid::get_filesystem_type() const { - return "APK"; +Error DirAccessJAndroid::remove(String p_name) { + if (_remove) { + JNIEnv *env = get_jni_env(); + ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED); + + 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() { @@ -233,3 +302,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 0e1b12cb58..d469c9d317 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/io/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 Ref 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(bool p_include_drive = true) const; ///< 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 e94ca87d81..d72137e523 100644 --- a/platform/android/export/export_plugin.cpp +++ b/platform/android/export/export_plugin.cpp @@ -123,6 +123,7 @@ static const char *android_perms[] = { "MANAGE_ACCOUNTS", "MANAGE_APP_TOKENS", "MANAGE_DOCUMENTS", + "MANAGE_EXTERNAL_STORAGE", "MASTER_CLEAR", "MEDIA_CONTENT_CONTROL", "MODIFY_AUDIO_SETTINGS", @@ -245,7 +246,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' void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { EditorExportPlatformAndroid *ea = static_cast(ud); @@ -276,6 +277,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { } } +#ifndef ANDROID_ENABLED // Check for devices updates String adb = get_adb_path(); if (FileAccess::exists(adb)) { @@ -387,6 +389,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { ea->devices_changed.set(); } } +#endif uint64_t sleep = 200; uint64_t wait = 3000000; @@ -399,6 +402,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { } } +#ifndef ANDROID_ENABLED if (EditorSettings::get_singleton()->get("export/android/shutdown_adb_on_exit")) { String adb = get_adb_path(); if (!FileAccess::exists(adb)) { @@ -409,6 +413,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) { args.push_back("kill-server"); OS::get_singleton()->execute(adb, args); } +#endif } String EditorExportPlatformAndroid::get_project_name(const String &p_name) const { @@ -747,10 +752,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) { @@ -798,7 +807,7 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref\n", permission); } else { manifest_text += vformat(" \n", permission); @@ -806,7 +815,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]); @@ -948,7 +957,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 eeb5aae0f1..15ac8091be 100644 --- a/platform/android/export/export_plugin.h +++ b/platform/android/export/export_plugin.h @@ -116,7 +116,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 9a470edfdd..8d370a31a4 100644 --- a/platform/android/export/gradle_export_util.cpp +++ b/platform/android/export/gradle_export_util.cpp @@ -254,7 +254,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_OPENXR; String manifest_application_text = vformat( @@ -271,7 +271,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) { bool hand_tracking_enabled = (int)(p_preset->get("xr_features/hand_tracking")) > XR_HAND_TRACKING_NONE; diff --git a/platform/android/export/gradle_export_util.h b/platform/android/export/gradle_export_util.h index 109852bdfc..7896392d16 100644 --- a/platform/android/export/gradle_export_util.h +++ b/platform/android/export/gradle_export_util.h @@ -104,6 +104,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 4bb8a13bb6..ace7636e6c 100644 --- a/platform/android/file_access_android.cpp +++ b/platform/android/file_access_android.cpp @@ -34,14 +34,20 @@ AAssetManager *FileAccessAndroid::asset_manager = nullptr; -Ref 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) { _close(); + 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://")) { @@ -134,7 +140,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 c16f74ac43..e6fd8c857b 100644 --- a/platform/android/file_access_android.h +++ b/platform/android/file_access_android.h @@ -37,11 +37,12 @@ #include class FileAccessAndroid : public FileAccess { - static Ref 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; void _close(); @@ -51,6 +52,11 @@ public: virtual Error _open(const String &p_path, int p_mode_flags); // open 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 0000000000..c1a48e025e --- /dev/null +++ b/platform/android/file_access_filesystem_jandroid.cpp @@ -0,0 +1,283 @@ +/*************************************************************************/ +/* 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) { + switch (res) { + case 0: + default: + return ERR_FILE_CANT_OPEN; + + case -1: + return ERR_FILE_NOT_FOUND; + } + } + + 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_length() 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 0000000000..18d5df1628 --- /dev/null +++ b/platform/android/file_access_filesystem_jandroid.h @@ -0,0 +1,97 @@ +/*************************************************************************/ +/* 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/io/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; + + void _close(); ///< close a file + +public: + virtual Error _open(const String &p_path, int p_mode_flags) override; ///< open 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_length() 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/config.gradle b/platform/android/java/app/config.gradle index 3daf628e63..fbd97fae0b 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 dd167c3880..729966ee69 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 93cbb47400..abf506a83c 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 e8ce34f34d..837a5d62e1 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 4.x + + Missing storage access permission! diff --git a/platform/android/java/lib/AndroidManifest.xml b/platform/android/java/lib/AndroidManifest.xml index 228d8d45fa..79b5aadf2a 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 cafae94d62..28e689e63a 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.tts.GodotTTS; @@ -164,9 +166,9 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC private Sensor mMagnetometer; private Sensor mGyroscope; - public static GodotIO io; - public static GodotNetUtils netUtils; - public static GodotTTS tts; + public GodotIO io; + public GodotNetUtils netUtils; + public GodotTTS tts; public interface ResultCallback { void callback(int requestCode, int resultCode, Intent data); @@ -458,16 +460,26 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC final Activity activity = getActivity(); io = new GodotIO(activity); - GodotLib.io = io; netUtils = new GodotNetUtils(activity); tts = new GodotTTS(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, + tts); 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 a8e3669ac6..0434efdf4c 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 3182ab0666..e2ae62d9cf 100644 --- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java +++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java @@ -31,8 +31,13 @@ package org.godotengine.godot; import org.godotengine.godot.gl.GodotRenderer; +import org.godotengine.godot.io.directory.DirectoryAccessHandler; +import org.godotengine.godot.io.file.FileAccessHandler; +import org.godotengine.godot.tts.GodotTTS; +import org.godotengine.godot.utils.GodotNetUtils; import android.app.Activity; +import android.content.res.AssetManager; import android.hardware.SensorEvent; import android.view.Surface; @@ -42,8 +47,6 @@ import javax.microedition.khronos.opengles.GL10; * Wrapper for native library */ public class GodotLib { - public static GodotIO io; - static { System.loadLibrary("godot_android"); } @@ -51,7 +54,15 @@ 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, + GodotTTS tts); /** * 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 0000000000..c7bd55b620 --- /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 0000000000..098b10ae36 --- /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 0000000000..fedcf4843f --- /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 0000000000..c3acf42568 --- /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 0000000000..aef1bed8ce --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt @@ -0,0 +1,186 @@ +/*************************************************************************/ +/* 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 + internal var endOfFile = false + private set + + 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) + if (position <= size()) { + endOfFile = false + } + } 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 read(buffer: ByteBuffer): Int { + return try { + val readBytes = fileChannel.read(buffer) + if (readBytes == -1) { + endOfFile = true + 0 + } else { + readBytes + } + } catch (e: IOException) { + Log.w(TAG, "Exception while reading from file $filePath.", e) + 0 + } + } + + fun write(buffer: ByteBuffer) { + try { + val writtenBytes = fileChannel.write(buffer) + if (writtenBytes > 0) { + endOfFile = false + } + } 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 0000000000..c6b242a4b6 --- /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 0000000000..a4e0a82d6e --- /dev/null +++ b/platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt @@ -0,0 +1,202 @@ +/*************************************************************************/ +/* 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.io.FileNotFoundException +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 FILE_NOT_FOUND_ERROR_ID = -1 + 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: FileNotFoundException) { + return FILE_NOT_FOUND_ERROR_ID + } 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].endOfFile + } + + 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 0000000000..5af694ad99 --- /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 0000000000..81a7dd1705 --- /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 e5b4f41153..57db0709f0 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 6e716c34a6..f4de4acfad 100644 --- a/platform/android/java_godot_lib_jni.cpp +++ b/platform/android/java_godot_lib_jni.cpp @@ -43,6 +43,7 @@ #include "dir_access_jandroid.h" #include "display_server_android.h" #include "file_access_android.h" +#include "file_access_filesystem_jandroid.h" #include "jni_utils.h" #include "main/main.h" #include "net_socket_android.h" @@ -78,13 +79,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, jobject p_godot_tts) { 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); @@ -92,9 +93,10 @@ 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)); - TTS_Android::setup(godot_java->get_member_object("tts", "Lorg/godotengine/godot/tts/GodotTTS;", env)); + DirAccessJAndroid::setup(p_directory_access_handler); + FileAccessFilesystemJAndroid::setup(p_file_access_handler); + NetSocketAndroid::setup(p_net_utils); + TTS_Android::setup(p_godot_tts); 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 aa8d67cf46..de16f197b8 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, jobject p_godot_tts); 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, jobject p_surface, jint p_width, jint p_height); diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp index 6674428de8..0f551e7f4f 100644 --- a/platform/android/os_android.cpp +++ b/platform/android/os_android.cpp @@ -40,6 +40,7 @@ #include "dir_access_jandroid.h" #include "file_access_android.h" +#include "file_access_filesystem_jandroid.h" #include "net_socket_android.h" #include @@ -93,7 +94,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); @@ -105,7 +106,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(); } @@ -300,6 +301,33 @@ String OS_Android::get_system_dir(SystemDir p_dir, bool p_shared_storage) const return godot_io_java->get_system_dir(p_dir, p_shared_storage); } +Error OS_Android::move_to_trash(const String &p_path) { + Ref da_ref = DirAccess::create_for_path(p_path); + if (da_ref.is_null()) { + return FAILED; + } + + // Check if it's a directory + if (da_ref->dir_exists(p_path)) { + Error err = da_ref->change_dir(p_path); + if (err) { + return err; + } + // This is directory, let's erase its contents + err = da_ref->erase_contents_recursive(); + if (err) { + return err; + } + // Remove the top directory + return da_ref->remove(p_path); + } else if (da_ref->file_exists(p_path)) { + // This is a file, let's remove it. + return da_ref->remove(p_path); + } else { + return FAILED; + } +} + void OS_Android::set_display_size(const Size2i &p_size) { display_size = p_size; } diff --git a/platform/android/os_android.h b/platform/android/os_android.h index 3f607eac48..96c06d715c 100644 --- a/platform/android/os_android.h +++ b/platform/android/os_android.h @@ -122,6 +122,8 @@ public: virtual String get_system_dir(SystemDir p_dir, bool p_shared_storage = true) const override; + virtual Error move_to_trash(const String &p_path) override; + void vibrate_handheld(int p_duration_ms) override; virtual String get_config_path() const override; -- cgit v1.2.3