Add full support for Android scoped storage.

This was done by refactoring directory and file access handling for the Android platform so that any general filesystem access type go through the Android layer.
This allows us to validate whether the access is unrestricted, or whether it falls under scoped storage and thus act appropriately.
This commit is contained in:
Fredia Huya-Kouadio 2021-07-10 18:39:31 -07:00 committed by Fredia Huya-Kouadio
parent 100d223736
commit f9c19298ce
40 changed files with 2435 additions and 299 deletions

View file

@ -181,6 +181,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: {

View file

@ -57,6 +57,7 @@ protected:
String _get_root_path() const;
String _get_root_string() const;
AccessType get_access_type() const;
String fix_path(String p_path) const;
template <class T>

View file

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

View file

@ -43,13 +43,11 @@
class DirAccessUnix : public DirAccess {
DIR *dir_stream = nullptr;
static Ref<DirAccess> create_fs();
String current_dir;
bool _cisdir = false;
bool _cishidden = false;
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);

View file

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

View file

@ -49,7 +49,6 @@ class FileAccessUnix : public FileAccess {
String path;
String path_src;
static Ref<FileAccess> create_libc();
void _close();
public:

View file

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

View file

@ -31,30 +31,32 @@
#include "dir_access_jandroid.h"
#include "core/string/print_string.h"
#include "file_access_android.h"
#include "string_android.h"
#include "thread_jandroid.h"
jobject DirAccessJAndroid::io = nullptr;
jobject DirAccessJAndroid::dir_access_handler = nullptr;
jclass DirAccessJAndroid::cls = nullptr;
jmethodID DirAccessJAndroid::_dir_open = nullptr;
jmethodID DirAccessJAndroid::_dir_next = nullptr;
jmethodID DirAccessJAndroid::_dir_close = nullptr;
jmethodID DirAccessJAndroid::_dir_is_dir = nullptr;
Ref<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;
@ -62,169 +64,236 @@ Error DirAccessJAndroid::list_dir_begin() {
String DirAccessJAndroid::get_next() {
ERR_FAIL_COND_V(id == 0, "");
if (_dir_next) {
JNIEnv *env = get_jni_env();
ERR_FAIL_COND_V(env == nullptr, "");
jstring str = (jstring)env->CallObjectMethod(dir_access_handler, _dir_next, get_access_type(), id);
if (!str) {
return "";
}
JNIEnv *env = get_jni_env();
jstring str = (jstring)env->CallObjectMethod(io, _dir_next, id);
if (!str) {
String ret = jstring_to_string((jstring)str, env);
env->DeleteLocalRef((jobject)str);
return ret;
} else {
return "";
}
String ret = jstring_to_string((jstring)str, env);
env->DeleteLocalRef((jobject)str);
return ret;
}
bool DirAccessJAndroid::current_is_dir() const {
JNIEnv *env = get_jni_env();
return env->CallBooleanMethod(io, _dir_is_dir, id);
if (_dir_is_dir) {
JNIEnv *env = get_jni_env();
ERR_FAIL_COND_V(env == nullptr, false);
return env->CallBooleanMethod(dir_access_handler, _dir_is_dir, get_access_type(), id);
} else {
return false;
}
}
bool DirAccessJAndroid::current_is_hidden() const {
return current != "." && current != ".." && current.begins_with(".");
if (_current_is_hidden) {
JNIEnv *env = get_jni_env();
ERR_FAIL_COND_V(env == nullptr, false);
return env->CallBooleanMethod(dir_access_handler, _current_is_hidden, get_access_type(), id);
}
return false;
}
void DirAccessJAndroid::list_dir_end() {
if (id == 0) {
return;
}
JNIEnv *env = get_jni_env();
env->CallVoidMethod(io, _dir_close, id);
dir_close(id);
id = 0;
}
int DirAccessJAndroid::get_drive_count() {
return 0;
if (_get_drive_count) {
JNIEnv *env = get_jni_env();
ERR_FAIL_COND_V(env == nullptr, 0);
return env->CallIntMethod(dir_access_handler, _get_drive_count, get_access_type());
} else {
return 0;
}
}
String DirAccessJAndroid::get_drive(int p_drive) {
return "";
if (_get_drive) {
JNIEnv *env = get_jni_env();
ERR_FAIL_COND_V(env == nullptr, "");
jstring j_drive = (jstring)env->CallObjectMethod(dir_access_handler, _get_drive, get_access_type(), p_drive);
if (!j_drive) {
return "";
}
String drive = jstring_to_string(j_drive, env);
env->DeleteLocalRef(j_drive);
return drive;
} else {
return "";
}
}
Error DirAccessJAndroid::change_dir(String p_dir) {
JNIEnv *env = get_jni_env();
if (p_dir.is_empty() || p_dir == "." || (p_dir == ".." && current_dir.is_empty())) {
String new_dir = get_absolute_path(p_dir);
if (new_dir == current_dir) {
return OK;
}
String new_dir;
if (p_dir != "res://" && p_dir.length() > 1 && p_dir.ends_with("/")) {
p_dir = p_dir.substr(0, p_dir.length() - 1);
}
if (p_dir.begins_with("/")) {
new_dir = p_dir.substr(1, p_dir.length());
} else if (p_dir.begins_with("res://")) {
new_dir = p_dir.substr(6, p_dir.length());
} else if (current_dir.is_empty()) {
new_dir = p_dir;
} else {
new_dir = current_dir.plus_file(p_dir);
}
//test if newdir exists
new_dir = new_dir.simplify_path();
jstring js = env->NewStringUTF(new_dir.utf8().get_data());
int res = env->CallIntMethod(io, _dir_open, js);
env->DeleteLocalRef(js);
if (res <= 0) {
if (!dir_exists(new_dir)) {
return ERR_INVALID_PARAMETER;
}
env->CallVoidMethod(io, _dir_close, res);
current_dir = new_dir;
return OK;
}
String DirAccessJAndroid::get_current_dir(bool p_include_drive) const {
return "res://" + current_dir;
String DirAccessJAndroid::get_absolute_path(String p_path) {
if (current_dir != "" && p_path == current_dir) {
return current_dir;
}
if (p_path.is_relative_path()) {
p_path = get_current_dir().plus_file(p_path);
}
p_path = fix_path(p_path);
p_path = p_path.simplify_path();
return p_path;
}
bool DirAccessJAndroid::file_exists(String p_file) {
String sd;
if (current_dir.is_empty()) {
sd = p_file;
if (_file_exists) {
JNIEnv *env = get_jni_env();
ERR_FAIL_COND_V(env == nullptr, false);
String path = get_absolute_path(p_file);
jstring j_path = env->NewStringUTF(path.utf8().get_data());
bool result = env->CallBooleanMethod(dir_access_handler, _file_exists, get_access_type(), j_path);
env->DeleteLocalRef(j_path);
return result;
} else {
sd = current_dir.plus_file(p_file);
return false;
}
Ref<FileAccessAndroid> f;
f.instantiate();
bool exists = f->file_exists(sd);
return exists;
}
bool DirAccessJAndroid::dir_exists(String p_dir) {
JNIEnv *env = get_jni_env();
if (_dir_exists) {
JNIEnv *env = get_jni_env();
ERR_FAIL_COND_V(env == nullptr, false);
String sd;
if (current_dir.is_empty()) {
sd = p_dir;
String path = get_absolute_path(p_dir);
jstring j_path = env->NewStringUTF(path.utf8().get_data());
bool result = env->CallBooleanMethod(dir_access_handler, _dir_exists, get_access_type(), j_path);
env->DeleteLocalRef(j_path);
return result;
} else {
if (p_dir.is_relative_path()) {
sd = current_dir.plus_file(p_dir);
} else {
sd = fix_path(p_dir);
}
}
String path = sd.simplify_path();
if (path.begins_with("/")) {
path = path.substr(1, path.length());
} else if (path.begins_with("res://")) {
path = path.substr(6, path.length());
}
jstring js = env->NewStringUTF(path.utf8().get_data());
int res = env->CallIntMethod(io, _dir_open, js);
env->DeleteLocalRef(js);
if (res <= 0) {
return false;
}
}
env->CallVoidMethod(io, _dir_close, res);
Error DirAccessJAndroid::make_dir_recursive(String p_dir) {
// Check if the directory exists already
if (dir_exists(p_dir)) {
return ERR_ALREADY_EXISTS;
}
return true;
if (_make_dir) {
JNIEnv *env = get_jni_env();
ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED);
String path = get_absolute_path(p_dir);
jstring j_dir = env->NewStringUTF(path.utf8().get_data());
bool result = env->CallBooleanMethod(dir_access_handler, _make_dir, get_access_type(), j_dir);
env->DeleteLocalRef(j_dir);
if (result) {
return OK;
} else {
return FAILED;
}
} else {
return ERR_UNCONFIGURED;
}
}
Error DirAccessJAndroid::make_dir(String p_dir) {
ERR_FAIL_V(ERR_UNAVAILABLE);
return make_dir_recursive(p_dir);
}
Error DirAccessJAndroid::rename(String p_from, String p_to) {
ERR_FAIL_V(ERR_UNAVAILABLE);
if (_rename) {
JNIEnv *env = get_jni_env();
ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED);
String from_path = get_absolute_path(p_from);
jstring j_from = env->NewStringUTF(from_path.utf8().get_data());
String to_path = get_absolute_path(p_to);
jstring j_to = env->NewStringUTF(to_path.utf8().get_data());
bool result = env->CallBooleanMethod(dir_access_handler, _rename, get_access_type(), j_from, j_to);
env->DeleteLocalRef(j_from);
env->DeleteLocalRef(j_to);
if (result) {
return OK;
} else {
return FAILED;
}
} else {
return ERR_UNCONFIGURED;
}
}
Error DirAccessJAndroid::remove(String p_name) {
ERR_FAIL_V(ERR_UNAVAILABLE);
}
if (_remove) {
JNIEnv *env = get_jni_env();
ERR_FAIL_COND_V(env == nullptr, ERR_UNCONFIGURED);
String DirAccessJAndroid::get_filesystem_type() const {
return "APK";
String path = get_absolute_path(p_name);
jstring j_name = env->NewStringUTF(path.utf8().get_data());
bool result = env->CallBooleanMethod(dir_access_handler, _remove, get_access_type(), j_name);
env->DeleteLocalRef(j_name);
if (result) {
return OK;
} else {
return FAILED;
}
} else {
return ERR_UNCONFIGURED;
}
}
uint64_t DirAccessJAndroid::get_space_left() {
return 0;
if (_get_space_left) {
JNIEnv *env = get_jni_env();
ERR_FAIL_COND_V(env == nullptr, 0);
return env->CallLongMethod(dir_access_handler, _get_space_left, get_access_type());
} else {
return 0;
}
}
void DirAccessJAndroid::setup(jobject p_io) {
void DirAccessJAndroid::setup(jobject p_dir_access_handler) {
JNIEnv *env = get_jni_env();
io = p_io;
dir_access_handler = env->NewGlobalRef(p_dir_access_handler);
jclass c = env->GetObjectClass(io);
jclass c = env->GetObjectClass(dir_access_handler);
cls = (jclass)env->NewGlobalRef(c);
_dir_open = env->GetMethodID(cls, "dir_open", "(Ljava/lang/String;)I");
_dir_next = env->GetMethodID(cls, "dir_next", "(I)Ljava/lang/String;");
_dir_close = env->GetMethodID(cls, "dir_close", "(I)V");
_dir_is_dir = env->GetMethodID(cls, "dir_is_dir", "(I)Z");
_dir_open = env->GetMethodID(cls, "dirOpen", "(ILjava/lang/String;)I");
_dir_next = env->GetMethodID(cls, "dirNext", "(II)Ljava/lang/String;");
_dir_close = env->GetMethodID(cls, "dirClose", "(II)V");
_dir_is_dir = env->GetMethodID(cls, "dirIsDir", "(II)Z");
_dir_exists = env->GetMethodID(cls, "dirExists", "(ILjava/lang/String;)Z");
_file_exists = env->GetMethodID(cls, "fileExists", "(ILjava/lang/String;)Z");
_get_drive_count = env->GetMethodID(cls, "getDriveCount", "(I)I");
_get_drive = env->GetMethodID(cls, "getDrive", "(II)Ljava/lang/String;");
_make_dir = env->GetMethodID(cls, "makeDir", "(ILjava/lang/String;)Z");
_get_space_left = env->GetMethodID(cls, "getSpaceLeft", "(I)J");
_rename = env->GetMethodID(cls, "rename", "(ILjava/lang/String;Ljava/lang/String;)Z");
_remove = env->GetMethodID(cls, "remove", "(ILjava/lang/String;)Z");
_current_is_hidden = env->GetMethodID(cls, "isCurrentHidden", "(II)Z");
}
DirAccessJAndroid::DirAccessJAndroid() {
@ -233,3 +302,26 @@ DirAccessJAndroid::DirAccessJAndroid() {
DirAccessJAndroid::~DirAccessJAndroid() {
list_dir_end();
}
int DirAccessJAndroid::dir_open(String p_path) {
if (_dir_open) {
JNIEnv *env = get_jni_env();
ERR_FAIL_COND_V(env == nullptr, 0);
String path = get_absolute_path(p_path);
jstring js = env->NewStringUTF(path.utf8().get_data());
int dirId = env->CallIntMethod(dir_access_handler, _dir_open, get_access_type(), js);
env->DeleteLocalRef(js);
return dirId;
} else {
return 0;
}
}
void DirAccessJAndroid::dir_close(int p_id) {
if (_dir_close) {
JNIEnv *env = get_jni_env();
ERR_FAIL_COND(env == nullptr);
env->CallVoidMethod(dir_access_handler, _dir_close, get_access_type(), p_id);
}
}

View file

@ -32,58 +32,70 @@
#define DIR_ACCESS_JANDROID_H
#include "core/io/dir_access.h"
#include "drivers/unix/dir_access_unix.h"
#include "java_godot_lib_jni.h"
#include <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 Ref<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(bool p_include_drive = true) const; ///< return current dir location
virtual Error change_dir(String p_dir) override; ///< can be relative or absolute, return false on success
virtual bool file_exists(String p_file);
virtual bool dir_exists(String p_dir);
virtual bool file_exists(String p_file) override;
virtual bool dir_exists(String p_dir) override;
virtual Error make_dir(String p_dir);
virtual Error make_dir(String p_dir) override;
virtual Error make_dir_recursive(String p_dir) override;
virtual Error rename(String p_from, String p_to);
virtual Error remove(String p_name);
virtual Error rename(String p_from, String p_to) override;
virtual Error remove(String p_name) override;
virtual bool is_link(String p_file) { return false; }
virtual String read_link(String p_file) { return p_file; }
virtual Error create_link(String p_source, String p_target) { return FAILED; }
virtual bool is_link(String p_file) override { return false; }
virtual String read_link(String p_file) override { return p_file; }
virtual Error create_link(String p_source, String p_target) override { return FAILED; }
virtual String get_filesystem_type() const;
virtual uint64_t get_space_left() override;
uint64_t get_space_left();
static void setup(jobject p_io);
static void setup(jobject p_dir_access_handler);
DirAccessJAndroid();
~DirAccessJAndroid();
private:
int id = 0;
int dir_open(String p_path);
void dir_close(int p_id);
String get_absolute_path(String p_path);
};
#endif // DIR_ACCESS_JANDROID_H

View file

@ -123,6 +123,7 @@ static const char *android_perms[] = {
"MANAGE_ACCOUNTS",
"MANAGE_APP_TOKENS",
"MANAGE_DOCUMENTS",
"MANAGE_EXTERNAL_STORAGE",
"MASTER_CLEAR",
"MEDIA_CONTENT_CONTROL",
"MODIFY_AUDIO_SETTINGS",
@ -245,7 +246,7 @@ static const char *APK_ASSETS_DIRECTORY = "res://android/build/assets";
static const char *AAB_ASSETS_DIRECTORY = "res://android/build/assetPacks/installTime/src/main/assets";
static const int DEFAULT_MIN_SDK_VERSION = 19; // Should match the value in 'platform/android/java/app/config.gradle#minSdk'
static const int DEFAULT_TARGET_SDK_VERSION = 31; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk'
static const int DEFAULT_TARGET_SDK_VERSION = 32; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk'
void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
EditorExportPlatformAndroid *ea = static_cast<EditorExportPlatformAndroid *>(ud);
@ -276,6 +277,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
}
}
#ifndef ANDROID_ENABLED
// Check for devices updates
String adb = get_adb_path();
if (FileAccess::exists(adb)) {
@ -387,6 +389,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
ea->devices_changed.set();
}
}
#endif
uint64_t sleep = 200;
uint64_t wait = 3000000;
@ -399,6 +402,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
}
}
#ifndef ANDROID_ENABLED
if (EditorSettings::get_singleton()->get("export/android/shutdown_adb_on_exit")) {
String adb = get_adb_path();
if (!FileAccess::exists(adb)) {
@ -409,6 +413,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
args.push_back("kill-server");
OS::get_singleton()->execute(adb, args);
}
#endif
}
String EditorExportPlatformAndroid::get_project_name(const String &p_name) const {
@ -747,10 +752,14 @@ Error EditorExportPlatformAndroid::copy_gradle_so(void *p_userdata, const Shared
return OK;
}
bool EditorExportPlatformAndroid::_has_storage_permission(const Vector<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) {
@ -798,7 +807,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);
@ -806,7 +815,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"));
@ -864,7 +873,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]);
@ -948,7 +957,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") {

View file

@ -116,7 +116,9 @@ class EditorExportPlatformAndroid : public EditorExportPlatform {
static Error copy_gradle_so(void *p_userdata, const SharedObject &p_so);
bool _has_storage_permission(const Vector<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);

View file

@ -254,7 +254,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_OPENXR;
String manifest_application_text = vformat(
@ -271,7 +271,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) {
bool hand_tracking_enabled = (int)(p_preset->get("xr_features/hand_tracking")) > XR_HAND_TRACKING_NONE;

View file

@ -104,6 +104,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

View file

@ -34,14 +34,20 @@
AAssetManager *FileAccessAndroid::asset_manager = nullptr;
Ref<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) {
_close();
path_src = p_path;
String path = fix_path(p_path).simplify_path();
absolute_path = path;
if (path.begins_with("/")) {
path = path.substr(1, path.length());
} else if (path.begins_with("res://")) {
@ -134,7 +140,7 @@ uint64_t FileAccessAndroid::get_buffer(uint8_t *p_dst, uint64_t p_length) const
}
Error FileAccessAndroid::get_error() const {
return eof ? ERR_FILE_EOF : OK; //not sure what else it may happen
return eof ? ERR_FILE_EOF : OK; // not sure what else it may happen
}
void FileAccessAndroid::flush() {

View file

@ -37,11 +37,12 @@
#include <stdio.h>
class FileAccessAndroid : public FileAccess {
static Ref<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;
void _close();
@ -51,6 +52,11 @@ public:
virtual Error _open(const String &p_path, int p_mode_flags); // open a file
virtual bool is_open() const; // true when file is open
/// returns the path for the current open file
virtual String get_path() const;
/// returns the absolute path for the current open file
virtual String get_path_absolute() const;
virtual void seek(uint64_t p_position); // seek to a given position
virtual void seek_end(int64_t p_position = 0); // seek from the end of file
virtual uint64_t get_position() const; // get position in the file

View file

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

View file

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

View file

@ -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',

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -34,6 +34,8 @@ import static android.content.Context.MODE_PRIVATE;
import static android.content.Context.WINDOW_SERVICE;
import org.godotengine.godot.input.GodotEditText;
import org.godotengine.godot.io.directory.DirectoryAccessHandler;
import org.godotengine.godot.io.file.FileAccessHandler;
import org.godotengine.godot.plugin.GodotPlugin;
import org.godotengine.godot.plugin.GodotPluginRegistry;
import org.godotengine.godot.tts.GodotTTS;
@ -164,9 +166,9 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
private Sensor mMagnetometer;
private Sensor mGyroscope;
public static GodotIO io;
public static GodotNetUtils netUtils;
public static GodotTTS tts;
public GodotIO io;
public GodotNetUtils netUtils;
public GodotTTS tts;
public interface ResultCallback {
void callback(int requestCode, int resultCode, Intent data);
@ -458,16 +460,26 @@ public class Godot extends Fragment implements SensorEventListener, IDownloaderC
final Activity activity = getActivity();
io = new GodotIO(activity);
GodotLib.io = io;
netUtils = new GodotNetUtils(activity);
tts = new GodotTTS(activity);
Context context = getContext();
DirectoryAccessHandler directoryAccessHandler = new DirectoryAccessHandler(context);
FileAccessHandler fileAccessHandler = new FileAccessHandler(context);
mSensorManager = (SensorManager)activity.getSystemService(Context.SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
mGravity = mSensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY);
mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
mGyroscope = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
GodotLib.initialize(activity, this, activity.getAssets(), use_apk_expansion);
GodotLib.initialize(activity,
this,
activity.getAssets(),
io,
netUtils,
directoryAccessHandler,
fileAccessHandler,
use_apk_expansion,
tts);
result_callback = null;

View file

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

View file

@ -31,8 +31,13 @@
package org.godotengine.godot;
import org.godotengine.godot.gl.GodotRenderer;
import org.godotengine.godot.io.directory.DirectoryAccessHandler;
import org.godotengine.godot.io.file.FileAccessHandler;
import org.godotengine.godot.tts.GodotTTS;
import org.godotengine.godot.utils.GodotNetUtils;
import android.app.Activity;
import android.content.res.AssetManager;
import android.hardware.SensorEvent;
import android.view.Surface;
@ -42,8 +47,6 @@ import javax.microedition.khronos.opengles.GL10;
* Wrapper for native library
*/
public class GodotLib {
public static GodotIO io;
static {
System.loadLibrary("godot_android");
}
@ -51,7 +54,15 @@ public class GodotLib {
/**
* Invoked on the main thread to initialize Godot native layer.
*/
public static native void initialize(Activity activity, Godot p_instance, Object p_asset_manager, boolean use_apk_expansion);
public static native void initialize(Activity activity,
Godot p_instance,
AssetManager p_asset_manager,
GodotIO godotIO,
GodotNetUtils netUtils,
DirectoryAccessHandler directoryAccessHandler,
FileAccessHandler fileAccessHandler,
boolean use_apk_expansion,
GodotTTS tts);
/**
* Invoked on the main thread to clean up Godot native layer.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -40,6 +40,7 @@
#include "dir_access_jandroid.h"
#include "file_access_android.h"
#include "file_access_filesystem_jandroid.h"
#include "net_socket_android.h"
#include <dlfcn.h>
@ -93,7 +94,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);
@ -105,7 +106,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();
}
@ -300,6 +301,33 @@ String OS_Android::get_system_dir(SystemDir p_dir, bool p_shared_storage) const
return godot_io_java->get_system_dir(p_dir, p_shared_storage);
}
Error OS_Android::move_to_trash(const String &p_path) {
Ref<DirAccess> da_ref = DirAccess::create_for_path(p_path);
if (da_ref.is_null()) {
return FAILED;
}
// Check if it's a directory
if (da_ref->dir_exists(p_path)) {
Error err = da_ref->change_dir(p_path);
if (err) {
return err;
}
// This is directory, let's erase its contents
err = da_ref->erase_contents_recursive();
if (err) {
return err;
}
// Remove the top directory
return da_ref->remove(p_path);
} else if (da_ref->file_exists(p_path)) {
// This is a file, let's remove it.
return da_ref->remove(p_path);
} else {
return FAILED;
}
}
void OS_Android::set_display_size(const Size2i &p_size) {
display_size = p_size;
}

View file

@ -122,6 +122,8 @@ public:
virtual String get_system_dir(SystemDir p_dir, bool p_shared_storage = true) const override;
virtual Error move_to_trash(const String &p_path) override;
void vibrate_handheld(int p_duration_ms) override;
virtual String get_config_path() const override;