mirror of
https://github.com/godotengine/godot.git
synced 2024-11-29 15:42:29 +00:00
Merge pull request #62459 from m4gr3d/refactor_android_storage_handling_main
This commit is contained in:
commit
4e7223ce49
@ -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: {
|
||||
|
@ -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>
|
||||
|
@ -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!
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -49,7 +49,6 @@ class FileAccessUnix : public FileAccess {
|
||||
String path;
|
||||
String path_src;
|
||||
|
||||
static Ref<FileAccess> create_libc();
|
||||
void _close();
|
||||
|
||||
public:
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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") {
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
283
platform/android/file_access_filesystem_jandroid.cpp
Normal file
283
platform/android/file_access_filesystem_jandroid.cpp
Normal 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();
|
||||
}
|
||||
}
|
97
platform/android/file_access_filesystem_jandroid.h
Normal file
97
platform/android/file_access_filesystem_jandroid.h
Normal 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
|
@ -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',
|
||||
|
@ -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'
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user