瀏覽代碼

Merge pull request #51815 from m4gr3d/refactor_android_storage_handling

Rémi Verschelde 3 年之前
父節點
當前提交
ae60597930
共有 40 個文件被更改,包括 2361 次插入301 次删除
  1. 4 0
      core/os/dir_access.cpp
  2. 1 0
      core/os/dir_access.h
  3. 0 4
      drivers/unix/dir_access_unix.cpp
  4. 1 3
      drivers/unix/dir_access_unix.h
  5. 0 4
      drivers/unix/file_access_unix.cpp
  6. 0 2
      drivers/unix/file_access_unix.h
  7. 1 0
      platform/android/SCsub
  8. 198 106
      platform/android/dir_access_jandroid.cpp
  9. 42 30
      platform/android/dir_access_jandroid.h
  10. 11 6
      platform/android/export/export_plugin.cpp
  11. 3 1
      platform/android/export/export_plugin.h
  12. 2 2
      platform/android/export/gradle_export_util.cpp
  13. 1 1
      platform/android/export/gradle_export_util.h
  14. 9 3
      platform/android/file_access_android.cpp
  15. 7 2
      platform/android/file_access_android.h
  16. 276 0
      platform/android/file_access_filesystem_jandroid.cpp
  17. 96 0
      platform/android/file_access_filesystem_jandroid.h
  18. 0 1
      platform/android/java/app/AndroidManifest.xml
  19. 3 3
      platform/android/java/app/config.gradle
  20. 1 2
      platform/android/java/editor/build.gradle
  21. 6 2
      platform/android/java/editor/src/main/AndroidManifest.xml
  22. 51 1
      platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt
  23. 2 0
      platform/android/java/editor/src/main/res/values/strings.xml
  24. 1 1
      platform/android/java/lib/AndroidManifest.xml
  25. 8 4
      platform/android/java/lib/src/org/godotengine/godot/Godot.java
  26. 0 96
      platform/android/java/lib/src/org/godotengine/godot/GodotIO.java
  27. 6 3
      platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
  28. 114 0
      platform/android/java/lib/src/org/godotengine/godot/io/StorageScope.kt
  29. 177 0
      platform/android/java/lib/src/org/godotengine/godot/io/directory/AssetsDirectoryAccess.kt
  30. 224 0
      platform/android/java/lib/src/org/godotengine/godot/io/directory/DirectoryAccessHandler.kt
  31. 230 0
      platform/android/java/lib/src/org/godotengine/godot/io/directory/FilesystemDirectoryAccess.kt
  32. 174 0
      platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt
  33. 87 0
      platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessFlags.kt
  34. 198 0
      platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt
  35. 93 0
      platform/android/java/lib/src/org/godotengine/godot/io/file/FileData.kt
  36. 284 0
      platform/android/java/lib/src/org/godotengine/godot/io/file/MediaStoreData.kt
  37. 39 16
      platform/android/java/lib/src/org/godotengine/godot/utils/PermissionsUtil.java
  38. 7 5
      platform/android/java_godot_lib_jni.cpp
  39. 1 1
      platform/android/java_godot_lib_jni.h
  40. 3 2
      platform/android/os_android.cpp

+ 4 - 0
core/os/dir_access.cpp

@@ -184,6 +184,10 @@ Error DirAccess::make_dir_recursive(String p_dir) {
 	return OK;
 }
 
+DirAccess::AccessType DirAccess::get_access_type() const {
+	return _access_type;
+}
+
 String DirAccess::fix_path(String p_path) const {
 	switch (_access_type) {
 		case ACCESS_RESOURCES: {

+ 1 - 0
core/os/dir_access.h

@@ -56,6 +56,7 @@ protected:
 	String _get_root_path() const;
 	String _get_root_string() const;
 
+	AccessType get_access_type() const;
 	String fix_path(String p_path) const;
 	bool next_is_dir;
 

+ 0 - 4
drivers/unix/dir_access_unix.cpp

@@ -49,10 +49,6 @@
 #include <mntent.h>
 #endif
 
-DirAccess *DirAccessUnix::create_fs() {
-	return memnew(DirAccessUnix);
-}
-
 Error DirAccessUnix::list_dir_begin() {
 	list_dir_end(); //close any previous dir opening!
 

+ 1 - 3
drivers/unix/dir_access_unix.h

@@ -43,13 +43,11 @@
 class DirAccessUnix : public DirAccess {
 	DIR *dir_stream;
 
-	static DirAccess *create_fs();
-
-	String current_dir;
 	bool _cisdir;
 	bool _cishidden;
 
 protected:
+	String current_dir;
 	virtual String fix_unicode_name(const char *p_name) const { return String::utf8(p_name); }
 	virtual bool is_hidden(const String &p_name);
 

+ 0 - 4
drivers/unix/file_access_unix.cpp

@@ -335,10 +335,6 @@ Error FileAccessUnix::_set_unix_permissions(const String &p_file, uint32_t p_per
 	return FAILED;
 }
 
-FileAccess *FileAccessUnix::create_libc() {
-	return memnew(FileAccessUnix);
-}
-
 CloseNotificationFunc FileAccessUnix::close_notification_func = nullptr;
 
 FileAccessUnix::FileAccessUnix() :

+ 0 - 2
drivers/unix/file_access_unix.h

@@ -49,8 +49,6 @@ class FileAccessUnix : public FileAccess {
 	String path;
 	String path_src;
 
-	static FileAccess *create_libc();
-
 public:
 	static CloseNotificationFunc close_notification_func;
 

+ 1 - 0
platform/android/SCsub

@@ -6,6 +6,7 @@ android_files = [
     "os_android.cpp",
     "android_input_handler.cpp",
     "file_access_android.cpp",
+    "file_access_filesystem_jandroid.cpp",
     "audio_driver_opensl.cpp",
     "dir_access_jandroid.cpp",
     "thread_jandroid.cpp",

+ 198 - 106
platform/android/dir_access_jandroid.cpp

@@ -30,30 +30,32 @@
 
 #include "dir_access_jandroid.h"
 #include "core/print_string.h"
-#include "file_access_android.h"
 #include "string_android.h"
 #include "thread_jandroid.h"
 
-jobject DirAccessJAndroid::io = nullptr;
+jobject DirAccessJAndroid::dir_access_handler = nullptr;
 jclass DirAccessJAndroid::cls = nullptr;
 jmethodID DirAccessJAndroid::_dir_open = nullptr;
 jmethodID DirAccessJAndroid::_dir_next = nullptr;
 jmethodID DirAccessJAndroid::_dir_close = nullptr;
 jmethodID DirAccessJAndroid::_dir_is_dir = nullptr;
-
-DirAccess *DirAccessJAndroid::create_fs() {
-	return memnew(DirAccessJAndroid);
-}
+jmethodID DirAccessJAndroid::_dir_exists = nullptr;
+jmethodID DirAccessJAndroid::_file_exists = nullptr;
+jmethodID DirAccessJAndroid::_get_drive_count = nullptr;
+jmethodID DirAccessJAndroid::_get_drive = nullptr;
+jmethodID DirAccessJAndroid::_make_dir = nullptr;
+jmethodID DirAccessJAndroid::_get_space_left = nullptr;
+jmethodID DirAccessJAndroid::_rename = nullptr;
+jmethodID DirAccessJAndroid::_remove = nullptr;
+jmethodID DirAccessJAndroid::_current_is_hidden = nullptr;
 
 Error DirAccessJAndroid::list_dir_begin() {
 	list_dir_end();
-	JNIEnv *env = get_jni_env();
-
-	jstring js = env->NewStringUTF(current_dir.utf8().get_data());
-	int res = env->CallIntMethod(io, _dir_open, js);
+	int res = dir_open(current_dir);
 	if (res <= 0) {
 		return ERR_CANT_OPEN;
 	}
+
 	id = res;
 
 	return OK;
@@ -61,169 +63,236 @@ Error DirAccessJAndroid::list_dir_begin() {
 
 String DirAccessJAndroid::get_next() {
 	ERR_FAIL_COND_V(id == 0, "");
+	if (_dir_next) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND_V(env == nullptr, "");
+		jstring str = (jstring)env->CallObjectMethod(dir_access_handler, _dir_next, get_access_type(), id);
+		if (!str) {
+			return "";
+		}
 
-	JNIEnv *env = get_jni_env();
-	jstring str = (jstring)env->CallObjectMethod(io, _dir_next, id);
-	if (!str) {
+		String ret = jstring_to_string((jstring)str, env);
+		env->DeleteLocalRef((jobject)str);
+		return ret;
+	} else {
 		return "";
 	}
-	String ret = jstring_to_string((jstring)str, env);
-	env->DeleteLocalRef((jobject)str);
-	return ret;
 }
 
 bool DirAccessJAndroid::current_is_dir() const {
-	JNIEnv *env = get_jni_env();
-
-	return env->CallBooleanMethod(io, _dir_is_dir, id);
+	if (_dir_is_dir) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND_V(env == nullptr, false);
+		return env->CallBooleanMethod(dir_access_handler, _dir_is_dir, get_access_type(), id);
+	} else {
+		return false;
+	}
 }
 
 bool DirAccessJAndroid::current_is_hidden() const {
-	return current != "." && current != ".." && current.begins_with(".");
+	if (_current_is_hidden) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND_V(env == nullptr, false);
+		return env->CallBooleanMethod(dir_access_handler, _current_is_hidden, get_access_type(), id);
+	}
+	return false;
 }
 
 void DirAccessJAndroid::list_dir_end() {
 	if (id == 0) {
 		return;
 	}
-	JNIEnv *env = get_jni_env();
-	env->CallVoidMethod(io, _dir_close, id);
+
+	dir_close(id);
 	id = 0;
 }
 
 int DirAccessJAndroid::get_drive_count() {
-	return 0;
+	if (_get_drive_count) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND_V(env == nullptr, 0);
+		return env->CallIntMethod(dir_access_handler, _get_drive_count, get_access_type());
+	} else {
+		return 0;
+	}
 }
 
 String DirAccessJAndroid::get_drive(int p_drive) {
-	return "";
+	if (_get_drive) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND_V(env == nullptr, "");
+		jstring j_drive = (jstring)env->CallObjectMethod(dir_access_handler, _get_drive, get_access_type(), p_drive);
+		if (!j_drive) {
+			return "";
+		}
+
+		String drive = jstring_to_string(j_drive, env);
+		env->DeleteLocalRef(j_drive);
+		return drive;
+	} else {
+		return "";
+	}
 }
 
 Error DirAccessJAndroid::change_dir(String p_dir) {
-	JNIEnv *env = get_jni_env();
-
-	if (p_dir == "" || p_dir == "." || (p_dir == ".." && current_dir == "")) {
+	String new_dir = get_absolute_path(p_dir);
+	if (new_dir == current_dir) {
 		return OK;
 	}
 
-	String new_dir;
-
-	if (p_dir != "res://" && p_dir.length() > 1 && p_dir.ends_with("/")) {
-		p_dir = p_dir.substr(0, p_dir.length() - 1);
-	}
-
-	if (p_dir.begins_with("/")) {
-		new_dir = p_dir.substr(1, p_dir.length());
-	} else if (p_dir.begins_with("res://")) {
-		new_dir = p_dir.substr(6, p_dir.length());
-	} else if (current_dir == "") {
-		new_dir = p_dir;
-	} else {
-		new_dir = current_dir.plus_file(p_dir);
-	}
-
-	//test if newdir exists
-	new_dir = new_dir.simplify_path();
-
-	jstring js = env->NewStringUTF(new_dir.utf8().get_data());
-	int res = env->CallIntMethod(io, _dir_open, js);
-	env->DeleteLocalRef(js);
-	if (res <= 0) {
+	if (!dir_exists(new_dir)) {
 		return ERR_INVALID_PARAMETER;
 	}
 
-	env->CallVoidMethod(io, _dir_close, res);
-
 	current_dir = new_dir;
-
 	return OK;
 }
 
-String DirAccessJAndroid::get_current_dir() {
-	return "res://" + current_dir;
+String DirAccessJAndroid::get_absolute_path(String p_path) {
+	if (current_dir != "" && p_path == current_dir) {
+		return current_dir;
+	}
+
+	if (p_path.is_rel_path()) {
+		p_path = get_current_dir().plus_file(p_path);
+	}
+
+	p_path = fix_path(p_path);
+	p_path = p_path.simplify_path();
+	return p_path;
 }
 
 bool DirAccessJAndroid::file_exists(String p_file) {
-	String sd;
-	if (current_dir == "") {
-		sd = p_file;
+	if (_file_exists) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND_V(env == nullptr, false);
+
+		String path = get_absolute_path(p_file);
+		jstring j_path = env->NewStringUTF(path.utf8().get_data());
+		bool result = env->CallBooleanMethod(dir_access_handler, _file_exists, get_access_type(), j_path);
+		env->DeleteLocalRef(j_path);
+		return result;
 	} else {
-		sd = current_dir.plus_file(p_file);
+		return false;
 	}
-
-	FileAccessAndroid *f = memnew(FileAccessAndroid);
-	bool exists = f->file_exists(sd);
-	memdelete(f);
-
-	return exists;
 }
 
 bool DirAccessJAndroid::dir_exists(String p_dir) {
-	JNIEnv *env = get_jni_env();
-
-	String sd;
-
-	if (current_dir == "") {
-		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_rel_path()) {
-			sd = current_dir.plus_file(p_dir);
-		} else {
-			sd = fix_path(p_dir);
-		}
-	}
-
-	String path = sd.simplify_path();
-
-	if (path.begins_with("/")) {
-		path = path.substr(1, path.length());
-	} else if (path.begins_with("res://")) {
-		path = path.substr(6, path.length());
+		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() {
@@ -232,3 +301,26 @@ DirAccessJAndroid::DirAccessJAndroid() {
 DirAccessJAndroid::~DirAccessJAndroid() {
 	list_dir_end();
 }
+
+int DirAccessJAndroid::dir_open(String p_path) {
+	if (_dir_open) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND_V(env == nullptr, 0);
+
+		String path = get_absolute_path(p_path);
+		jstring js = env->NewStringUTF(path.utf8().get_data());
+		int dirId = env->CallIntMethod(dir_access_handler, _dir_open, get_access_type(), js);
+		env->DeleteLocalRef(js);
+		return dirId;
+	} else {
+		return 0;
+	}
+}
+
+void DirAccessJAndroid::dir_close(int p_id) {
+	if (_dir_close) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND(env == nullptr);
+		env->CallVoidMethod(dir_access_handler, _dir_close, get_access_type(), p_id);
+	}
+}

+ 42 - 30
platform/android/dir_access_jandroid.h

@@ -32,58 +32,70 @@
 #define DIR_ACCESS_JANDROID_H
 
 #include "core/os/dir_access.h"
+#include "drivers/unix/dir_access_unix.h"
 #include "java_godot_lib_jni.h"
 #include <stdio.h>
 
-class DirAccessJAndroid : public DirAccess {
-	static jobject io;
+/// Android implementation of the DirAccess interface used to provide access to
+/// ACCESS_FILESYSTEM and ACCESS_RESOURCES directory resources.
+/// The implementation use jni in order to comply with Android filesystem
+/// access restriction.
+class DirAccessJAndroid : public DirAccessUnix {
+	static jobject dir_access_handler;
 	static jclass cls;
 
 	static jmethodID _dir_open;
 	static jmethodID _dir_next;
 	static jmethodID _dir_close;
 	static jmethodID _dir_is_dir;
-
-	int id = 0;
-
-	String current_dir;
-	String current;
-
-	static DirAccess *create_fs();
+	static jmethodID _dir_exists;
+	static jmethodID _file_exists;
+	static jmethodID _get_drive_count;
+	static jmethodID _get_drive;
+	static jmethodID _make_dir;
+	static jmethodID _get_space_left;
+	static jmethodID _rename;
+	static jmethodID _remove;
+	static jmethodID _current_is_hidden;
 
 public:
-	virtual Error list_dir_begin(); ///< This starts dir listing
-	virtual String get_next();
-	virtual bool current_is_dir() const;
-	virtual bool current_is_hidden() const;
-	virtual void list_dir_end(); ///<
+	virtual Error list_dir_begin() override; ///< This starts dir listing
+	virtual String get_next() override;
+	virtual bool current_is_dir() const override;
+	virtual bool current_is_hidden() const override;
+	virtual void list_dir_end() override; ///<
 
-	virtual int get_drive_count();
-	virtual String get_drive(int p_drive);
+	virtual int get_drive_count() override;
+	virtual String get_drive(int p_drive) override;
 
-	virtual Error change_dir(String p_dir); ///< can be relative or absolute, return false on success
-	virtual String get_current_dir(); ///< return current dir location
+	virtual Error change_dir(String p_dir) override; ///< can be relative or absolute, return false on success
 
-	virtual bool file_exists(String p_file);
-	virtual bool dir_exists(String p_dir);
+	virtual bool file_exists(String p_file) override;
+	virtual bool dir_exists(String p_dir) override;
 
-	virtual Error make_dir(String p_dir);
+	virtual Error make_dir(String p_dir) override;
+	virtual Error make_dir_recursive(String p_dir) override;
 
-	virtual Error rename(String p_from, String p_to);
-	virtual Error remove(String p_name);
+	virtual Error rename(String p_from, String p_to) override;
+	virtual Error remove(String p_name) override;
 
-	virtual bool is_link(String p_file) { return false; }
-	virtual String read_link(String p_file) { return p_file; }
-	virtual Error create_link(String p_source, String p_target) { return FAILED; }
+	virtual bool is_link(String p_file) override { return false; }
+	virtual String read_link(String p_file) override { return p_file; }
+	virtual Error create_link(String p_source, String p_target) override { return FAILED; }
 
-	virtual String get_filesystem_type() const;
+	virtual uint64_t get_space_left() override;
 
-	uint64_t get_space_left();
-
-	static void setup(jobject p_io);
+	static void setup(jobject p_dir_access_handler);
 
 	DirAccessJAndroid();
 	~DirAccessJAndroid();
+
+private:
+	int id = 0;
+
+	int dir_open(String p_path);
+	void dir_close(int p_id);
+	String get_absolute_path(String p_path);
 };
 
 #endif // DIR_ACCESS_JANDROID_H

+ 11 - 6
platform/android/export/export_plugin.cpp

@@ -103,6 +103,7 @@ static const char *android_perms[] = {
 	"MANAGE_ACCOUNTS",
 	"MANAGE_APP_TOKENS",
 	"MANAGE_DOCUMENTS",
+	"MANAGE_EXTERNAL_STORAGE",
 	"MASTER_CLEAR",
 	"MEDIA_CONTENT_CONTROL",
 	"MODIFY_AUDIO_SETTINGS",
@@ -225,7 +226,7 @@ static const char *APK_ASSETS_DIRECTORY = "res://android/build/assets";
 static const char *AAB_ASSETS_DIRECTORY = "res://android/build/assetPacks/installTime/src/main/assets";
 
 static const int DEFAULT_MIN_SDK_VERSION = 19; // Should match the value in 'platform/android/java/app/config.gradle#minSdk'
-static const int DEFAULT_TARGET_SDK_VERSION = 31; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk'
+static const int DEFAULT_TARGET_SDK_VERSION = 32; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk'
 const String SDK_VERSION_RANGE = vformat("%s,%s,1", DEFAULT_MIN_SDK_VERSION, DEFAULT_TARGET_SDK_VERSION);
 
 void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
@@ -732,10 +733,14 @@ Error EditorExportPlatformAndroid::copy_gradle_so(void *p_userdata, const Shared
 	return OK;
 }
 
-bool EditorExportPlatformAndroid::_has_storage_permission(const Vector<String> &p_permissions) {
+bool EditorExportPlatformAndroid::_has_read_write_storage_permission(const Vector<String> &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<String> &p_permissions) {
+	return p_permissions.find("android.permission.MANAGE_EXTERNAL_STORAGE") != -1;
+}
+
 void EditorExportPlatformAndroid::_get_permissions(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, Vector<String> &r_permissions) {
 	const char **aperms = android_perms;
 	while (*aperms) {
@@ -783,7 +788,7 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPres
 	_get_permissions(p_preset, p_give_internet, perms);
 	for (int i = 0; i < perms.size(); i++) {
 		String permission = perms.get(i);
-		if (permission == "android.permission.WRITE_EXTERNAL_STORAGE" || permission == "android.permission.READ_EXTERNAL_STORAGE") {
+		if (permission == "android.permission.WRITE_EXTERNAL_STORAGE" || (permission == "android.permission.READ_EXTERNAL_STORAGE" && _has_manage_external_storage_permission(perms))) {
 			manifest_text += vformat("    <uses-permission android:name=\"%s\" android:maxSdkVersion=\"29\" />\n", permission);
 		} else {
 			manifest_text += vformat("    <uses-permission android:name=\"%s\" />\n", permission);
@@ -791,7 +796,7 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPres
 	}
 
 	manifest_text += _get_xr_features_tag(p_preset);
-	manifest_text += _get_application_tag(p_preset, _has_storage_permission(perms));
+	manifest_text += _get_application_tag(p_preset, _has_read_write_storage_permission(perms));
 	manifest_text += "</manifest>\n";
 	String manifest_path = vformat("res://android/build/src/%s/AndroidManifest.xml", (p_debug ? "debug" : "release"));
 
@@ -851,7 +856,7 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p
 	Vector<String> perms;
 	// Write permissions into the perms variable.
 	_get_permissions(p_preset, p_give_internet, perms);
-	bool has_storage_permission = _has_storage_permission(perms);
+	bool has_read_write_storage_permission = _has_read_write_storage_permission(perms);
 
 	while (ofs < (uint32_t)p_manifest.size()) {
 		uint32_t chunk = decode_uint32(&p_manifest[ofs]);
@@ -935,7 +940,7 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &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") {

+ 3 - 1
platform/android/export/export_plugin.h

@@ -134,7 +134,9 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
 
 	static Error copy_gradle_so(void *p_userdata, const SharedObject &p_so);
 
-	bool _has_storage_permission(const Vector<String> &p_permissions);
+	bool _has_read_write_storage_permission(const Vector<String> &p_permissions);
+
+	bool _has_manage_external_storage_permission(const Vector<String> &p_permissions);
 
 	void _get_permissions(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, Vector<String> &r_permissions);
 

+ 2 - 2
platform/android/export/gradle_export_util.cpp

@@ -256,7 +256,7 @@ String _get_activity_tag(const Ref<EditorExportPreset> &p_preset) {
 	return manifest_activity_text;
 }
 
-String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_storage_permission) {
+String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission) {
 	int xr_mode_index = (int)(p_preset->get("xr_features/xr_mode"));
 	bool uses_xr = xr_mode_index == XR_MODE_OVR || xr_mode_index == XR_MODE_OPENXR;
 	String manifest_application_text = vformat(
@@ -274,7 +274,7 @@ String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_
 			bool_to_string(p_preset->get("user_data_backup/allow")),
 			bool_to_string(p_preset->get("package/classify_as_game")),
 			bool_to_string(p_preset->get("package/retain_data_on_uninstall")),
-			bool_to_string(p_has_storage_permission));
+			bool_to_string(p_has_read_write_storage_permission));
 
 	if (uses_xr) {
 		if (xr_mode_index == XR_MODE_OVR) {

+ 1 - 1
platform/android/export/gradle_export_util.h

@@ -105,6 +105,6 @@ String _get_xr_features_tag(const Ref<EditorExportPreset> &p_preset);
 
 String _get_activity_tag(const Ref<EditorExportPreset> &p_preset);
 
-String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_storage_permission);
+String _get_application_tag(const Ref<EditorExportPreset> &p_preset, bool p_has_read_write_storage_permission);
 
 #endif // GODOT_GRADLE_EXPORT_UTIL_H

+ 9 - 3
platform/android/file_access_android.cpp

@@ -33,12 +33,18 @@
 
 AAssetManager *FileAccessAndroid::asset_manager = nullptr;
 
-FileAccess *FileAccessAndroid::create_android() {
-	return memnew(FileAccessAndroid);
+String FileAccessAndroid::get_path() const {
+	return path_src;
+}
+
+String FileAccessAndroid::get_path_absolute() const {
+	return absolute_path;
 }
 
 Error FileAccessAndroid::_open(const String &p_path, int p_mode_flags) {
+	path_src = p_path;
 	String path = fix_path(p_path).simplify_path();
+	absolute_path = path;
 	if (path.begins_with("/")) {
 		path = path.substr(1, path.length());
 	} else if (path.begins_with("res://")) {
@@ -131,7 +137,7 @@ uint64_t FileAccessAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const
 }
 
 Error FileAccessAndroid::get_error() const {
-	return eof ? ERR_FILE_EOF : OK; //not sure what else it may happen
+	return eof ? ERR_FILE_EOF : OK; // not sure what else it may happen
 }
 
 void FileAccessAndroid::flush() {

+ 7 - 2
platform/android/file_access_android.h

@@ -37,12 +37,12 @@
 #include <stdio.h>
 
 class FileAccessAndroid : public FileAccess {
-	static FileAccess *create_android();
 	mutable AAsset *asset = nullptr;
 	mutable uint64_t len = 0;
 	mutable uint64_t pos = 0;
 	mutable bool eof = false;
-	;
+	String absolute_path;
+	String path_src;
 
 public:
 	static AAssetManager *asset_manager;
@@ -51,6 +51,11 @@ public:
 	virtual void close(); // close a file
 	virtual bool is_open() const; // true when file is open
 
+	/// returns the path for the current open file
+	virtual String get_path() const;
+	/// returns the absolute path for the current open file
+	virtual String get_path_absolute() const;
+
 	virtual void seek(uint64_t p_position); // seek to a given position
 	virtual void seek_end(int64_t p_position = 0); // seek from the end of file
 	virtual uint64_t get_position() const; // get position in the file

+ 276 - 0
platform/android/file_access_filesystem_jandroid.cpp

@@ -0,0 +1,276 @@
+/*************************************************************************/
+/*  file_access_filesystem_jandroid.cpp                                  */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#include "file_access_filesystem_jandroid.h"
+#include "core/os/os.h"
+#include "thread_jandroid.h"
+#include <unistd.h>
+
+jobject FileAccessFilesystemJAndroid::file_access_handler = nullptr;
+jclass FileAccessFilesystemJAndroid::cls;
+
+jmethodID FileAccessFilesystemJAndroid::_file_open = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_get_size = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_seek = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_seek_end = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_read = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_tell = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_eof = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_close = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_write = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_flush = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_exists = nullptr;
+jmethodID FileAccessFilesystemJAndroid::_file_last_modified = nullptr;
+
+String FileAccessFilesystemJAndroid::get_path() const {
+	return path_src;
+}
+
+String FileAccessFilesystemJAndroid::get_path_absolute() const {
+	return absolute_path;
+}
+
+Error FileAccessFilesystemJAndroid::_open(const String &p_path, int p_mode_flags) {
+	if (is_open()) {
+		close();
+	}
+
+	if (_file_open) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED);
+
+		String path = fix_path(p_path).simplify_path();
+		jstring js = env->NewStringUTF(path.utf8().get_data());
+		int res = env->CallIntMethod(file_access_handler, _file_open, js, p_mode_flags);
+		env->DeleteLocalRef(js);
+
+		if (res <= 0) {
+			return ERR_FILE_CANT_OPEN;
+		}
+
+		id = res;
+		path_src = p_path;
+		absolute_path = path;
+		return OK;
+	} else {
+		return ERR_UNCONFIGURED;
+	}
+}
+
+void FileAccessFilesystemJAndroid::close() {
+	if (!is_open()) {
+		return;
+	}
+
+	if (_file_close) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND(env == nullptr);
+		env->CallVoidMethod(file_access_handler, _file_close, id);
+	}
+	id = 0;
+}
+
+bool FileAccessFilesystemJAndroid::is_open() const {
+	return id != 0;
+}
+
+void FileAccessFilesystemJAndroid::seek(uint64_t p_position) {
+	if (_file_seek) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND(env == nullptr);
+		ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use.");
+		env->CallVoidMethod(file_access_handler, _file_seek, id, p_position);
+	}
+}
+
+void FileAccessFilesystemJAndroid::seek_end(int64_t p_position) {
+	if (_file_seek_end) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND(env == nullptr);
+		ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use.");
+		env->CallVoidMethod(file_access_handler, _file_seek_end, id, p_position);
+	}
+}
+
+uint64_t FileAccessFilesystemJAndroid::get_position() const {
+	if (_file_tell) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND_V(env == nullptr, 0);
+		ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use.");
+		return env->CallLongMethod(file_access_handler, _file_tell, id);
+	} else {
+		return 0;
+	}
+}
+
+uint64_t FileAccessFilesystemJAndroid::get_len() const {
+	if (_file_get_size) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND_V(env == nullptr, 0);
+		ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use.");
+		return env->CallLongMethod(file_access_handler, _file_get_size, id);
+	} else {
+		return 0;
+	}
+}
+
+bool FileAccessFilesystemJAndroid::eof_reached() const {
+	if (_file_eof) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND_V(env == nullptr, false);
+		ERR_FAIL_COND_V_MSG(!is_open(), false, "File must be opened before use.");
+		return env->CallBooleanMethod(file_access_handler, _file_eof, id);
+	} else {
+		return false;
+	}
+}
+
+uint8_t FileAccessFilesystemJAndroid::get_8() const {
+	ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use.");
+	uint8_t byte;
+	get_buffer(&byte, 1);
+	return byte;
+}
+
+uint64_t FileAccessFilesystemJAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const {
+	if (_file_read) {
+		ERR_FAIL_COND_V_MSG(!is_open(), 0, "File must be opened before use.");
+		if (p_length == 0) {
+			return 0;
+		}
+
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND_V(env == nullptr, 0);
+
+		jobject j_buffer = env->NewDirectByteBuffer(p_dst, p_length);
+		int length = env->CallIntMethod(file_access_handler, _file_read, id, j_buffer);
+		env->DeleteLocalRef(j_buffer);
+		return length;
+	} else {
+		return 0;
+	}
+}
+
+void FileAccessFilesystemJAndroid::store_8(uint8_t p_dest) {
+	store_buffer(&p_dest, 1);
+}
+
+void FileAccessFilesystemJAndroid::store_buffer(const uint8_t *p_src, uint64_t p_length) {
+	if (_file_write) {
+		ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use.");
+		if (p_length == 0) {
+			return;
+		}
+
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND(env == nullptr);
+
+		jobject j_buffer = env->NewDirectByteBuffer((void *)p_src, p_length);
+		env->CallVoidMethod(file_access_handler, _file_write, id, j_buffer);
+		env->DeleteLocalRef(j_buffer);
+	}
+}
+
+Error FileAccessFilesystemJAndroid::get_error() const {
+	if (eof_reached()) {
+		return ERR_FILE_EOF;
+	}
+	return OK;
+}
+
+void FileAccessFilesystemJAndroid::flush() {
+	if (_file_flush) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND(env == nullptr);
+		ERR_FAIL_COND_MSG(!is_open(), "File must be opened before use.");
+		env->CallVoidMethod(file_access_handler, _file_flush, id);
+	}
+}
+
+bool FileAccessFilesystemJAndroid::file_exists(const String &p_path) {
+	if (_file_exists) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND_V(env == nullptr, false);
+
+		String path = fix_path(p_path).simplify_path();
+		jstring js = env->NewStringUTF(path.utf8().get_data());
+		bool result = env->CallBooleanMethod(file_access_handler, _file_exists, js);
+		env->DeleteLocalRef(js);
+		return result;
+	} else {
+		return false;
+	}
+}
+
+uint64_t FileAccessFilesystemJAndroid::_get_modified_time(const String &p_file) {
+	if (_file_last_modified) {
+		JNIEnv *env = get_jni_env();
+		ERR_FAIL_COND_V(env == nullptr, false);
+
+		String path = fix_path(p_file).simplify_path();
+		jstring js = env->NewStringUTF(path.utf8().get_data());
+		uint64_t result = env->CallLongMethod(file_access_handler, _file_last_modified, js);
+		env->DeleteLocalRef(js);
+		return result;
+	} else {
+		return 0;
+	}
+}
+
+void FileAccessFilesystemJAndroid::setup(jobject p_file_access_handler) {
+	JNIEnv *env = get_jni_env();
+	file_access_handler = env->NewGlobalRef(p_file_access_handler);
+
+	jclass c = env->GetObjectClass(file_access_handler);
+	cls = (jclass)env->NewGlobalRef(c);
+
+	_file_open = env->GetMethodID(cls, "fileOpen", "(Ljava/lang/String;I)I");
+	_file_get_size = env->GetMethodID(cls, "fileGetSize", "(I)J");
+	_file_tell = env->GetMethodID(cls, "fileGetPosition", "(I)J");
+	_file_eof = env->GetMethodID(cls, "isFileEof", "(I)Z");
+	_file_seek = env->GetMethodID(cls, "fileSeek", "(IJ)V");
+	_file_seek_end = env->GetMethodID(cls, "fileSeekFromEnd", "(IJ)V");
+	_file_read = env->GetMethodID(cls, "fileRead", "(ILjava/nio/ByteBuffer;)I");
+	_file_close = env->GetMethodID(cls, "fileClose", "(I)V");
+	_file_write = env->GetMethodID(cls, "fileWrite", "(ILjava/nio/ByteBuffer;)V");
+	_file_flush = env->GetMethodID(cls, "fileFlush", "(I)V");
+	_file_exists = env->GetMethodID(cls, "fileExists", "(Ljava/lang/String;)Z");
+	_file_last_modified = env->GetMethodID(cls, "fileLastModified", "(Ljava/lang/String;)J");
+}
+
+FileAccessFilesystemJAndroid::FileAccessFilesystemJAndroid() {
+	id = 0;
+}
+
+FileAccessFilesystemJAndroid::~FileAccessFilesystemJAndroid() {
+	if (is_open()) {
+		close();
+	}
+}

+ 96 - 0
platform/android/file_access_filesystem_jandroid.h

@@ -0,0 +1,96 @@
+/*************************************************************************/
+/*  file_access_filesystem_jandroid.h                                    */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+#ifndef FILE_ACCESS_FILESYSTEM_JANDROID_H
+#define FILE_ACCESS_FILESYSTEM_JANDROID_H
+
+#include "core/os/file_access.h"
+#include "java_godot_lib_jni.h"
+
+class FileAccessFilesystemJAndroid : public FileAccess {
+	static jobject file_access_handler;
+	static jclass cls;
+
+	static jmethodID _file_open;
+	static jmethodID _file_get_size;
+	static jmethodID _file_seek;
+	static jmethodID _file_seek_end;
+	static jmethodID _file_tell;
+	static jmethodID _file_eof;
+	static jmethodID _file_read;
+	static jmethodID _file_write;
+	static jmethodID _file_flush;
+	static jmethodID _file_close;
+	static jmethodID _file_exists;
+	static jmethodID _file_last_modified;
+
+	int id;
+	String absolute_path;
+	String path_src;
+
+public:
+	virtual Error _open(const String &p_path, int p_mode_flags) override; ///< open a file
+	virtual void close() override; ///< close a file
+	virtual bool is_open() const override; ///< true when file is open
+
+	/// returns the path for the current open file
+	virtual String get_path() const override;
+	/// returns the absolute path for the current open file
+	virtual String get_path_absolute() const override;
+
+	virtual void seek(uint64_t p_position) override; ///< seek to a given position
+	virtual void seek_end(int64_t p_position = 0) override; ///< seek from the end of file
+	virtual uint64_t get_position() const override; ///< get position in the file
+	virtual uint64_t get_len() const override; ///< get size of the file
+
+	virtual bool eof_reached() const override; ///< reading passed EOF
+
+	virtual uint8_t get_8() const override; ///< get a byte
+	virtual uint64_t get_buffer(uint8_t *p_dst, uint64_t p_length) const override;
+
+	virtual Error get_error() const override; ///< get last error
+
+	virtual void flush() override;
+	virtual void store_8(uint8_t p_dest) override; ///< store a byte
+	virtual void store_buffer(const uint8_t *p_src, uint64_t p_length) override;
+
+	virtual bool file_exists(const String &p_path) override; ///< return true if a file exists
+
+	static void setup(jobject p_file_access_handler);
+
+	virtual uint64_t _get_modified_time(const String &p_file) override;
+	virtual uint32_t _get_unix_permissions(const String &p_file) override { return 0; }
+	virtual Error _set_unix_permissions(const String &p_file, uint32_t p_permissions) override { return FAILED; }
+
+	FileAccessFilesystemJAndroid();
+	~FileAccessFilesystemJAndroid();
+};
+
+#endif // FILE_ACCESS_FILESYSTEM_JANDROID_H

+ 0 - 1
platform/android/java/app/AndroidManifest.xml

@@ -27,7 +27,6 @@
     <!-- Any tag in this line after android:icon will be erased when doing custom builds. -->
     <!-- If you want to add tags manually, do before it. -->
     <!-- WARNING: This should stay on a single line until the parsing code is improved. See GH-32414. -->
-    <!-- TODO: Remove the 'requestLegacyExternalStorage' attribute when https://github.com/godotengine/godot/issues/38913 is resolved -->
     <application android:label="@string/godot_project_name_string" android:allowBackup="false" android:isGame="true" android:hasFragileUserData="false" android:requestLegacyExternalStorage="false" tools:ignore="GoogleAppIndexingWarning" android:icon="@mipmap/icon" >
 
         <!-- Records the version of the Godot editor used for building -->

+ 3 - 3
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',

+ 1 - 2
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'
     }

+ 6 - 2
platform/android/java/editor/src/main/AndroidManifest.xml

@@ -14,8 +14,12 @@
         android:glEsVersion="0x00020000"
         android:required="true" />
 
-    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
+        tools:ignore="ScopedStorage" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+        android:maxSdkVersion="29"/>
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
+        android:maxSdkVersion="29"/>
     <uses-permission android:name="android.permission.INTERNET" />
 
     <application

+ 51 - 1
platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt

@@ -30,10 +30,14 @@
 
 package org.godotengine.editor
 
+import android.Manifest
 import android.content.Intent
+import android.content.pm.PackageManager
 import android.os.Build
 import android.os.Bundle
 import android.os.Debug
+import android.os.Environment
+import android.widget.Toast
 import androidx.window.layout.WindowMetricsCalculator
 import org.godotengine.godot.FullScreenGodotApp
 import org.godotengine.godot.utils.PermissionsUtil
@@ -68,7 +72,7 @@ open class GodotEditor : FullScreenGodotApp() {
 		val params = intent.getStringArrayExtra(COMMAND_LINE_PARAMS)
 		updateCommandLineParams(params)
 
-		if (BuildConfig.BUILD_TYPE == "debug" && WAIT_FOR_DEBUGGER) {
+		if (BuildConfig.BUILD_TYPE == "dev" && WAIT_FOR_DEBUGGER) {
 			Debug.waitForDebugger()
 		}
 
@@ -143,4 +147,50 @@ open class GodotEditor : FullScreenGodotApp() {
 	 * The Godot Android Editor sets its own orientation via its AndroidManifest
 	 */
 	protected open fun overrideOrientationRequest() = true
+
+	override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+		super.onActivityResult(requestCode, resultCode, data)
+		// Check if we got the MANAGE_EXTERNAL_STORAGE permission
+		if (requestCode == PermissionsUtil.REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE) {
+			if (Build.VERSION.SDK_INT >= 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<String?>,
+		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()
+				}
+			}
+		}
+	}
 }

+ 2 - 0
platform/android/java/editor/src/main/res/values/strings.xml

@@ -1,4 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
 <resources>
 	<string name="godot_editor_name_string">Godot Editor</string>
+
+	<string name="denied_storage_permission_error_msg">Missing storage access permission!</string>
 </resources>

+ 1 - 1
platform/android/java/lib/AndroidManifest.xml

@@ -5,7 +5,7 @@
     android:versionName="1.0">
 
     <!-- Should match the mindSdk and targetSdk values in platform/android/java/app/config.gradle -->
-    <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="31" />
+    <uses-sdk android:minSdkVersion="19" android:targetSdkVersion="32" />
 
     <application>
 

+ 8 - 4
platform/android/java/lib/src/org/godotengine/godot/Godot.java

@@ -34,6 +34,8 @@ import static android.content.Context.MODE_PRIVATE;
 import static android.content.Context.WINDOW_SERVICE;
 
 import org.godotengine.godot.input.GodotEditText;
+import org.godotengine.godot.io.directory.DirectoryAccessHandler;
+import org.godotengine.godot.io.file.FileAccessHandler;
 import org.godotengine.godot.plugin.GodotPlugin;
 import org.godotengine.godot.plugin.GodotPluginRegistry;
 import org.godotengine.godot.utils.GodotNetUtils;
@@ -250,8 +252,8 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
 	private Sensor mMagnetometer;
 	private Sensor mGyroscope;
 
-	public static GodotIO io;
-	public static GodotNetUtils netUtils;
+	public GodotIO io;
+	public GodotNetUtils netUtils;
 
 	static SingletonBase[] singletons = new SingletonBase[MAX_SINGLETONS];
 	static int singleton_count = 0;
@@ -572,15 +574,17 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
 
 		final Activity activity = getActivity();
 		io = new GodotIO(activity);
-		GodotLib.io = io;
 		netUtils = new GodotNetUtils(activity);
+		Context context = getContext();
+		DirectoryAccessHandler directoryAccessHandler = new DirectoryAccessHandler(context);
+		FileAccessHandler fileAccessHandler = new FileAccessHandler(context);
 		mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE);
 		mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
 		mGravity = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY);
 		mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
 		mGyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
 
-		GodotLib.initialize(activity, this, activity.getAssets(), use_apk_expansion);
+		GodotLib.initialize(activity, this, activity.getAssets(), io, netUtils, directoryAccessHandler, fileAccessHandler, use_apk_expansion);
 
 		result_callback = null;
 

+ 0 - 96
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<AssetDir> 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) {

+ 6 - 3
platform/android/java/lib/src/org/godotengine/godot/GodotLib.java

@@ -30,7 +30,12 @@
 
 package org.godotengine.godot;
 
+import org.godotengine.godot.io.directory.DirectoryAccessHandler;
+import org.godotengine.godot.io.file.FileAccessHandler;
+import org.godotengine.godot.utils.GodotNetUtils;
+
 import android.app.Activity;
+import android.content.res.AssetManager;
 import android.hardware.SensorEvent;
 
 import javax.microedition.khronos.egl.EGLConfig;
@@ -40,8 +45,6 @@ import javax.microedition.khronos.opengles.GL10;
  * Wrapper for native library
  */
 public class GodotLib {
-	public static GodotIO io;
-
 	static {
 		System.loadLibrary("godot_android");
 	}
@@ -49,7 +52,7 @@ public class GodotLib {
 	/**
 	 * Invoked on the main thread to initialize Godot native layer.
 	 */
-	public static native void initialize(Activity activity, Godot p_instance, Object p_asset_manager, boolean use_apk_expansion);
+	public static native void initialize(Activity activity, Godot p_instance, AssetManager p_asset_manager, GodotIO godotIO, GodotNetUtils netUtils, DirectoryAccessHandler directoryAccessHandler, FileAccessHandler fileAccessHandler, boolean use_apk_expansion);
 
 	/**
 	 * Invoked on the main thread to clean up Godot native layer.

+ 114 - 0
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
+		}
+	}
+}

+ 177 - 0
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<String>, var current: Int = 0)
+
+	private val assetManager = context.assets
+
+	private var lastDirId = STARTING_DIR_ID
+	private val dirs = SparseArray<AssetDir>()
+
+	private fun getAssetsPath(originalPath: String): String {
+		if (originalPath.startsWith(File.separatorChar)) {
+			return originalPath.substring(1)
+		}
+		return originalPath
+	}
+
+	override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0
+
+	override fun dirOpen(path: String): Int {
+		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
+}

+ 224 - 0
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)
+		}
+	}
+
+}

+ 230 - 0
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<File>, var current: Int = 0)
+
+	private val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
+	private var lastDirId = STARTING_DIR_ID
+	private val dirs = SparseArray<DirData>()
+
+	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
+		}
+	}
+}

+ 174 - 0
platform/android/java/lib/src/org/godotengine/godot/io/file/DataAccess.kt

@@ -0,0 +1,174 @@
+/*************************************************************************/
+/*  DataAccess.kt                                                        */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+package org.godotengine.godot.io.file
+
+import android.content.Context
+import android.os.Build
+import android.util.Log
+import org.godotengine.godot.io.StorageScope
+import java.io.IOException
+import java.nio.ByteBuffer
+import java.nio.channels.FileChannel
+import kotlin.math.max
+
+/**
+ * Base class for file IO operations.
+ *
+ * Its derived instances provide concrete implementations to handle regular file access, as well
+ * as file access through the media store API on versions of Android were scoped storage is enabled.
+ */
+internal abstract class DataAccess(private val filePath: String) {
+
+	companion object {
+		private val TAG = DataAccess::class.java.simpleName
+
+		fun generateDataAccess(
+			storageScope: StorageScope,
+			context: Context,
+			filePath: String,
+			accessFlag: FileAccessFlags
+		): DataAccess? {
+			return when (storageScope) {
+				StorageScope.APP -> FileData(filePath, accessFlag)
+
+				StorageScope.SHARED -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+					MediaStoreData(context, filePath, accessFlag)
+				} else {
+					null
+				}
+
+				StorageScope.UNKNOWN -> null
+			}
+		}
+
+		fun fileExists(storageScope: StorageScope, context: Context, path: String): Boolean {
+			return when(storageScope) {
+				StorageScope.APP -> FileData.fileExists(path)
+				StorageScope.SHARED -> MediaStoreData.fileExists(context, path)
+				StorageScope.UNKNOWN -> false
+			}
+		}
+
+		fun fileLastModified(storageScope: StorageScope, context: Context, path: String): Long {
+			return when(storageScope) {
+				StorageScope.APP -> FileData.fileLastModified(path)
+				StorageScope.SHARED -> MediaStoreData.fileLastModified(context, path)
+				StorageScope.UNKNOWN -> 0L
+			}
+		}
+
+		fun removeFile(storageScope: StorageScope, context: Context, path: String): Boolean {
+			return when(storageScope) {
+				StorageScope.APP -> FileData.delete(path)
+				StorageScope.SHARED -> MediaStoreData.delete(context, path)
+				StorageScope.UNKNOWN -> false
+			}
+		}
+
+		fun renameFile(storageScope: StorageScope, context: Context, from: String, to: String): Boolean {
+			return when(storageScope) {
+				StorageScope.APP -> FileData.rename(from, to)
+				StorageScope.SHARED -> MediaStoreData.rename(context, from, to)
+				StorageScope.UNKNOWN -> false
+			}
+		}
+	}
+
+	protected abstract val fileChannel: FileChannel
+
+	fun close() {
+		try {
+			fileChannel.close()
+		} catch (e: IOException) {
+			Log.w(TAG, "Exception when closing file $filePath.", e)
+		}
+	}
+
+	fun flush() {
+		try {
+			fileChannel.force(false)
+		} catch (e: IOException) {
+			Log.w(TAG, "Exception when flushing file $filePath.", e)
+		}
+	}
+
+	fun seek(position: Long) {
+		try {
+			fileChannel.position(position)
+		} catch (e: Exception) {
+			Log.w(TAG, "Exception when seeking file $filePath.", e)
+		}
+	}
+
+	fun seekFromEnd(positionFromEnd: Long) {
+		val positionFromBeginning = max(0, size() - positionFromEnd)
+		seek(positionFromBeginning)
+	}
+
+	fun position(): Long {
+		return try {
+			fileChannel.position()
+		} catch (e: IOException) {
+			Log.w(
+				TAG,
+				"Exception when retrieving position for file $filePath.",
+				e
+			)
+			0L
+		}
+	}
+
+	fun size() = try {
+		fileChannel.size()
+	} catch (e: IOException) {
+		Log.w(TAG, "Exception when retrieving size for file $filePath.", e)
+		0L
+	}
+
+	fun isEndOfFile() = position() >= size()
+
+	fun read(buffer: ByteBuffer): Int {
+		return try {
+			fileChannel.read(buffer)
+		} catch (e: IOException) {
+			Log.w(TAG, "Exception while reading from file $filePath.", e)
+			0
+		}
+	}
+
+	fun write(buffer: ByteBuffer) {
+		try {
+			fileChannel.write(buffer)
+		} catch (e: IOException) {
+			Log.w(TAG, "Exception while writing to file $filePath.", e)
+		}
+	}
+}

+ 87 - 0
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
+        }
+    }
+}

+ 198 - 0
platform/android/java/lib/src/org/godotengine/godot/io/file/FileAccessHandler.kt

@@ -0,0 +1,198 @@
+/*************************************************************************/
+/*  FileAccessHandler.kt                                                 */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the       */
+/* "Software"), to deal in the Software without restriction, including   */
+/* without limitation the rights to use, copy, modify, merge, publish,   */
+/* distribute, sublicense, and/or sell copies of the Software, and to    */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions:                                             */
+/*                                                                       */
+/* The above copyright notice and this permission notice shall be        */
+/* included in all copies or substantial portions of the Software.       */
+/*                                                                       */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,       */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF    */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY  */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,  */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE     */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                */
+/*************************************************************************/
+
+package org.godotengine.godot.io.file
+
+import android.content.Context
+import android.util.Log
+import android.util.SparseArray
+import org.godotengine.godot.io.StorageScope
+import java.nio.ByteBuffer
+
+/**
+ * Handles regular and media store file access and interactions.
+ */
+class FileAccessHandler(val context: Context) {
+
+	companion object {
+		private val TAG = FileAccessHandler::class.java.simpleName
+
+		private const val INVALID_FILE_ID = 0
+		private const val STARTING_FILE_ID = 1
+
+		fun fileExists(context: Context, path: String?): Boolean {
+			val storageScope = StorageScope.getStorageScope(context, path)
+			if (storageScope == StorageScope.UNKNOWN) {
+				return false
+			}
+
+			return try {
+				DataAccess.fileExists(storageScope, context, path!!)
+			} catch (e: SecurityException) {
+				false
+			}
+		}
+
+		fun removeFile(context: Context, path: String?): Boolean {
+			val storageScope = StorageScope.getStorageScope(context, path)
+			if (storageScope == StorageScope.UNKNOWN) {
+				return false
+			}
+
+			return try {
+				DataAccess.removeFile(storageScope, context, path!!)
+			} catch (e: Exception) {
+				false
+			}
+		}
+
+		fun renameFile(context: Context, from: String?, to: String?): Boolean {
+			val storageScope = StorageScope.getStorageScope(context, from)
+			if (storageScope == StorageScope.UNKNOWN) {
+				return false
+			}
+
+			return try {
+				DataAccess.renameFile(storageScope, context, from!!, to!!)
+			} catch (e: Exception) {
+				false
+			}
+		}
+	}
+
+	private val files = SparseArray<DataAccess>()
+	private var lastFileId = STARTING_FILE_ID
+
+	private fun hasFileId(fileId: Int) = files.indexOfKey(fileId) >= 0
+
+	fun fileOpen(path: String?, modeFlags: Int): Int {
+		val storageScope = StorageScope.getStorageScope(context, path)
+		if (storageScope == StorageScope.UNKNOWN) {
+			return INVALID_FILE_ID
+		}
+
+		try {
+			val accessFlag = FileAccessFlags.fromNativeModeFlags(modeFlags) ?: return INVALID_FILE_ID
+			val dataAccess = DataAccess.generateDataAccess(storageScope, context, path!!, accessFlag) ?: return INVALID_FILE_ID
+
+			files.put(++lastFileId, dataAccess)
+			return lastFileId
+		} catch (e: Exception) {
+			Log.w(TAG, "Error while opening $path", e)
+			return INVALID_FILE_ID
+		}
+	}
+
+	fun fileGetSize(fileId: Int): Long {
+		if (!hasFileId(fileId)) {
+			return 0L
+		}
+
+		return files[fileId].size()
+	}
+
+	fun fileSeek(fileId: Int, position: Long) {
+		if (!hasFileId(fileId)) {
+			return
+		}
+
+		files[fileId].seek(position)
+	}
+
+	fun fileSeekFromEnd(fileId: Int, position: Long) {
+		if (!hasFileId(fileId)) {
+			return
+		}
+
+		files[fileId].seekFromEnd(position)
+	}
+
+	fun fileRead(fileId: Int, byteBuffer: ByteBuffer?): Int {
+		if (!hasFileId(fileId) || byteBuffer == null) {
+			return 0
+		}
+
+		return files[fileId].read(byteBuffer)
+	}
+
+	fun fileWrite(fileId: Int, byteBuffer: ByteBuffer?) {
+		if (!hasFileId(fileId) || byteBuffer == null) {
+			return
+		}
+
+		files[fileId].write(byteBuffer)
+	}
+
+	fun fileFlush(fileId: Int) {
+		if (!hasFileId(fileId)) {
+			return
+		}
+
+		files[fileId].flush()
+	}
+
+	fun fileExists(path: String?) = Companion.fileExists(context, path)
+
+	fun fileLastModified(filepath: String?): Long {
+		val storageScope = StorageScope.getStorageScope(context, filepath)
+		if (storageScope == StorageScope.UNKNOWN) {
+			return 0L
+		}
+
+		return try {
+			DataAccess.fileLastModified(storageScope, context, filepath!!)
+		} catch (e: SecurityException) {
+			0L
+		}
+	}
+
+	fun fileGetPosition(fileId: Int): Long {
+		if (!hasFileId(fileId)) {
+			return 0L
+		}
+
+		return files[fileId].position()
+	}
+
+	fun isFileEof(fileId: Int): Boolean {
+		if (!hasFileId(fileId)) {
+			return false
+		}
+
+		return files[fileId].isEndOfFile()
+	}
+
+	fun fileClose(fileId: Int) {
+		if (hasFileId(fileId)) {
+			files[fileId].close()
+			files.remove(fileId)
+		}
+	}
+}

+ 93 - 0
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)
+		}
+	}
+}

+ 284 - 0
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<String> {
+			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<DataItem> {
+			val query = context.contentResolver.query(
+				COLLECTION,
+				PROJECTION,
+				SELECTION_BY_ID,
+				getSelectionByIdArgument(id),
+				null
+			)
+			return dataItemFromCursor(query)
+		}
+
+		private fun queryByPath(context: Context, path: String): List<DataItem> {
+			val query = context.contentResolver.query(
+				COLLECTION,
+				PROJECTION,
+				SELECTION_BY_PATH,
+				getSelectionByPathArguments(path),
+				null
+			)
+			return dataItemFromCursor(query)
+		}
+
+		private fun dataItemFromCursor(query: Cursor?): List<DataItem> {
+			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<DataItem>()
+				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)
+		}
+	}
+}

+ 39 - 16
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<String> dangerousPermissions = new ArrayList<>();
+		List<String> 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<String> dangerousPermissions = new ArrayList<>();
+		List<String> 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;

+ 7 - 5
platform/android/java_godot_lib_jni.cpp

@@ -40,6 +40,7 @@
 #include "core/project_settings.h"
 #include "dir_access_jandroid.h"
 #include "file_access_android.h"
+#include "file_access_filesystem_jandroid.h"
 #include "jni_utils.h"
 #include "main/input_default.h"
 #include "main/main.h"
@@ -118,13 +119,13 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHei
 	}
 }
 
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject activity, jobject godot_instance, jobject p_asset_manager, jboolean p_use_apk_expansion) {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion) {
 	JavaVM *jvm;
 	env->GetJavaVM(&jvm);
 
 	// create our wrapper classes
-	godot_java = new GodotJavaWrapper(env, activity, godot_instance);
-	godot_io_java = new GodotIOJavaWrapper(env, godot_java->get_member_object("io", "Lorg/godotengine/godot/GodotIO;", env));
+	godot_java = new GodotJavaWrapper(env, p_activity, p_godot_instance);
+	godot_io_java = new GodotIOJavaWrapper(env, p_godot_io);
 
 	init_thread_jandroid(jvm, env);
 
@@ -132,8 +133,9 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *en
 
 	FileAccessAndroid::asset_manager = AAssetManager_fromJava(env, amgr);
 
-	DirAccessJAndroid::setup(godot_io_java->get_instance());
-	NetSocketAndroid::setup(godot_java->get_member_object("netUtils", "Lorg/godotengine/godot/utils/GodotNetUtils;", env));
+	DirAccessJAndroid::setup(p_directory_access_handler);
+	FileAccessFilesystemJAndroid::setup(p_file_access_handler);
+	NetSocketAndroid::setup(p_net_utils);
 
 	os_android = new OS_Android(godot_java, godot_io_java, p_use_apk_expansion);
 

+ 1 - 1
platform/android/java_godot_lib_jni.h

@@ -37,7 +37,7 @@
 // These functions can be called from within JAVA and are the means by which our JAVA implementation calls back into our C++ code.
 // See java/src/org/godotengine/godot/GodotLib.java for the JAVA side of this (yes that's why we have the long names)
 extern "C" {
-JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject activity, jobject godot_instance, jobject p_asset_manager, jboolean p_use_apk_expansion);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_initialize(JNIEnv *env, jclass clazz, jobject p_activity, jobject p_godot_instance, jobject p_asset_manager, jobject p_godot_io, jobject p_net_utils, jobject p_directory_access_handler, jobject p_file_access_handler, jboolean p_use_apk_expansion);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_ondestroy(JNIEnv *env, jclass clazz);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setup(JNIEnv *env, jclass clazz, jobjectArray p_cmdline);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_resize(JNIEnv *env, jclass clazz, jint width, jint height);

+ 3 - 2
platform/android/os_android.cpp

@@ -43,6 +43,7 @@
 
 #include "dir_access_jandroid.h"
 #include "file_access_android.h"
+#include "file_access_filesystem_jandroid.h"
 #include "net_socket_android.h"
 
 #include <android/input.h>
@@ -114,7 +115,7 @@ void OS_Android::initialize_core() {
 	}
 #endif
 	FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_USERDATA);
-	FileAccess::make_default<FileAccessUnix>(FileAccess::ACCESS_FILESYSTEM);
+	FileAccess::make_default<FileAccessFilesystemJAndroid>(FileAccess::ACCESS_FILESYSTEM);
 
 #ifdef TOOLS_ENABLED
 	DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_RESOURCES);
@@ -126,7 +127,7 @@ void OS_Android::initialize_core() {
 	}
 #endif
 	DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_USERDATA);
-	DirAccess::make_default<DirAccessUnix>(DirAccess::ACCESS_FILESYSTEM);
+	DirAccess::make_default<DirAccessJAndroid>(DirAccess::ACCESS_FILESYSTEM);
 
 	NetSocketAndroid::make_default();
 }