Add full support for Android scoped storage.
This was done by refactoring directory and file access handling for the Android platform so that any general filesystem access type go through the Android layer. This allows us to validate whether the access is unrestricted, or whether it falls under scoped storage and thus act appropriately.
This commit is contained in:
parent
100d223736
commit
f9c19298ce
40 changed files with 2435 additions and 299 deletions
|
@ -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();
|
||||
jstring str = (jstring)env->CallObjectMethod(io, _dir_next, id);
|
||||
ERR_FAIL_COND_V(env == nullptr, "");
|
||||
jstring str = (jstring)env->CallObjectMethod(dir_access_handler, _dir_next, get_access_type(), id);
|
||||
if (!str) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String ret = jstring_to_string((jstring)str, env);
|
||||
env->DeleteLocalRef((jobject)str);
|
||||
return ret;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
bool DirAccessJAndroid::current_is_dir() const {
|
||||
if (_dir_is_dir) {
|
||||
JNIEnv *env = get_jni_env();
|
||||
|
||||
return env->CallBooleanMethod(io, _dir_is_dir, id);
|
||||
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() {
|
||||
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) {
|
||||
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 "";
|
||||
}
|
||||
|
||||
Error DirAccessJAndroid::change_dir(String p_dir) {
|
||||
JNIEnv *env = get_jni_env();
|
||||
String drive = jstring_to_string(j_drive, env);
|
||||
env->DeleteLocalRef(j_drive);
|
||||
return drive;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
if (p_dir.is_empty() || p_dir == "." || (p_dir == ".." && current_dir.is_empty())) {
|
||||
Error DirAccessJAndroid::change_dir(String p_dir) {
|
||||
String new_dir = get_absolute_path(p_dir);
|
||||
if (new_dir == current_dir) {
|
||||
return OK;
|
||||
}
|
||||
|
||||
String new_dir;
|
||||
|
||||
if (p_dir != "res://" && p_dir.length() > 1 && p_dir.ends_with("/")) {
|
||||
p_dir = p_dir.substr(0, p_dir.length() - 1);
|
||||
}
|
||||
|
||||
if (p_dir.begins_with("/")) {
|
||||
new_dir = p_dir.substr(1, p_dir.length());
|
||||
} else if (p_dir.begins_with("res://")) {
|
||||
new_dir = p_dir.substr(6, p_dir.length());
|
||||
} else if (current_dir.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) {
|
||||
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() {
|
||||
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://")) {
|
||||
|
|
|
@ -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 {
|
||||
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) {
|
||||
dangerousPermissions.add(manifestPermission);
|
||||
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 {
|
||||
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) {
|
||||
dangerousPermissions.add(manifestPermission);
|
||||
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 a new issue