Merge pull request #62459 from m4gr3d/refactor_android_storage_handling_main

This commit is contained in:
Rémi Verschelde 2022-07-05 12:44:55 +02:00 committed by GitHub
commit 4e7223ce49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 2435 additions and 299 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -123,6 +123,7 @@ static const char *android_perms[] = {
"MANAGE_ACCOUNTS",
"MANAGE_APP_TOKENS",
"MANAGE_DOCUMENTS",
"MANAGE_EXTERNAL_STORAGE",
"MASTER_CLEAR",
"MEDIA_CONTENT_CONTROL",
"MODIFY_AUDIO_SETTINGS",
@ -245,7 +246,7 @@ static const char *APK_ASSETS_DIRECTORY = "res://android/build/assets";
static const char *AAB_ASSETS_DIRECTORY = "res://android/build/assetPacks/installTime/src/main/assets";
static const int DEFAULT_MIN_SDK_VERSION = 19; // Should match the value in 'platform/android/java/app/config.gradle#minSdk'
static const int DEFAULT_TARGET_SDK_VERSION = 31; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk'
static const int DEFAULT_TARGET_SDK_VERSION = 32; // Should match the value in 'platform/android/java/app/config.gradle#targetSdk'
void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
EditorExportPlatformAndroid *ea = static_cast<EditorExportPlatformAndroid *>(ud);
@ -276,6 +277,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
}
}
#ifndef ANDROID_ENABLED
// Check for devices updates
String adb = get_adb_path();
if (FileAccess::exists(adb)) {
@ -387,6 +389,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
ea->devices_changed.set();
}
}
#endif
uint64_t sleep = 200;
uint64_t wait = 3000000;
@ -399,6 +402,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
}
}
#ifndef ANDROID_ENABLED
if (EditorSettings::get_singleton()->get("export/android/shutdown_adb_on_exit")) {
String adb = get_adb_path();
if (!FileAccess::exists(adb)) {
@ -409,6 +413,7 @@ void EditorExportPlatformAndroid::_check_for_changes_poll_thread(void *ud) {
args.push_back("kill-server");
OS::get_singleton()->execute(adb, args);
}
#endif
}
String EditorExportPlatformAndroid::get_project_name(const String &p_name) const {
@ -747,10 +752,14 @@ Error EditorExportPlatformAndroid::copy_gradle_so(void *p_userdata, const Shared
return OK;
}
bool EditorExportPlatformAndroid::_has_storage_permission(const Vector<String> &p_permissions) {
bool EditorExportPlatformAndroid::_has_read_write_storage_permission(const Vector<String> &p_permissions) {
return p_permissions.find("android.permission.READ_EXTERNAL_STORAGE") != -1 || p_permissions.find("android.permission.WRITE_EXTERNAL_STORAGE") != -1;
}
bool EditorExportPlatformAndroid::_has_manage_external_storage_permission(const Vector<String> &p_permissions) {
return p_permissions.find("android.permission.MANAGE_EXTERNAL_STORAGE") != -1;
}
void EditorExportPlatformAndroid::_get_permissions(const Ref<EditorExportPreset> &p_preset, bool p_give_internet, Vector<String> &r_permissions) {
const char **aperms = android_perms;
while (*aperms) {
@ -798,7 +807,7 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPres
_get_permissions(p_preset, p_give_internet, perms);
for (int i = 0; i < perms.size(); i++) {
String permission = perms.get(i);
if (permission == "android.permission.WRITE_EXTERNAL_STORAGE" || permission == "android.permission.READ_EXTERNAL_STORAGE") {
if (permission == "android.permission.WRITE_EXTERNAL_STORAGE" || (permission == "android.permission.READ_EXTERNAL_STORAGE" && _has_manage_external_storage_permission(perms))) {
manifest_text += vformat(" <uses-permission android:name=\"%s\" android:maxSdkVersion=\"29\" />\n", permission);
} else {
manifest_text += vformat(" <uses-permission android:name=\"%s\" />\n", permission);
@ -806,7 +815,7 @@ void EditorExportPlatformAndroid::_write_tmp_manifest(const Ref<EditorExportPres
}
manifest_text += _get_xr_features_tag(p_preset);
manifest_text += _get_application_tag(p_preset, _has_storage_permission(perms));
manifest_text += _get_application_tag(p_preset, _has_read_write_storage_permission(perms));
manifest_text += "</manifest>\n";
String manifest_path = vformat("res://android/build/src/%s/AndroidManifest.xml", (p_debug ? "debug" : "release"));
@ -864,7 +873,7 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p
Vector<String> perms;
// Write permissions into the perms variable.
_get_permissions(p_preset, p_give_internet, perms);
bool has_storage_permission = _has_storage_permission(perms);
bool has_read_write_storage_permission = _has_read_write_storage_permission(perms);
while (ofs < (uint32_t)p_manifest.size()) {
uint32_t chunk = decode_uint32(&p_manifest[ofs]);
@ -948,7 +957,7 @@ void EditorExportPlatformAndroid::_fix_manifest(const Ref<EditorExportPreset> &p
}
if (tname == "application" && attrname == "requestLegacyExternalStorage") {
encode_uint32(has_storage_permission ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);
encode_uint32(has_read_write_storage_permission ? 0xFFFFFFFF : 0, &p_manifest.write[iofs + 16]);
}
if (tname == "application" && attrname == "allowBackup") {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
ext.versions = [
androidGradlePlugin: '7.0.3',
compileSdk : 31,
compileSdk : 32,
minSdk : 19, // Also update 'platform/android/java/lib/AndroidManifest.xml#minSdkVersion' & 'platform/android/export/export_plugin.cpp#DEFAULT_MIN_SDK_VERSION'
targetSdk : 31, // Also update 'platform/android/java/lib/AndroidManifest.xml#targetSdkVersion' & 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION'
buildTools : '30.0.3',
targetSdk : 32, // Also update 'platform/android/java/lib/AndroidManifest.xml#targetSdkVersion' & 'platform/android/export/export_plugin.cpp#DEFAULT_TARGET_SDK_VERSION'
buildTools : '32.0.0',
kotlinVersion : '1.6.21',
fragmentVersion : '1.3.6',
nexusPublishVersion: '1.1.0',

View File

@ -23,8 +23,7 @@ android {
versionCode getGodotLibraryVersionCode()
versionName getGodotLibraryVersionName()
minSdkVersion versions.minSdk
//noinspection ExpiredTargetSdkVersion - Restrict to version 29 until https://github.com/godotengine/godot/pull/51815 is submitted
targetSdkVersion 29 // versions.targetSdk
targetSdkVersion versions.targetSdk
missingDimensionStrategy 'products', 'editor'
}

View File

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

View File

@ -30,10 +30,14 @@
package org.godotengine.editor
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.Debug
import android.os.Environment
import android.widget.Toast
import androidx.window.layout.WindowMetricsCalculator
import org.godotengine.godot.FullScreenGodotApp
import org.godotengine.godot.utils.PermissionsUtil
@ -68,7 +72,7 @@ open class GodotEditor : FullScreenGodotApp() {
val params = intent.getStringArrayExtra(COMMAND_LINE_PARAMS)
updateCommandLineParams(params)
if (BuildConfig.BUILD_TYPE == "debug" && WAIT_FOR_DEBUGGER) {
if (BuildConfig.BUILD_TYPE == "dev" && WAIT_FOR_DEBUGGER) {
Debug.waitForDebugger()
}
@ -143,4 +147,50 @@ open class GodotEditor : FullScreenGodotApp() {
* The Godot Android Editor sets its own orientation via its AndroidManifest
*/
protected open fun overrideOrientationRequest() = true
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// Check if we got the MANAGE_EXTERNAL_STORAGE permission
if (requestCode == PermissionsUtil.REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
Toast.makeText(
this,
R.string.denied_storage_permission_error_msg,
Toast.LENGTH_LONG
).show()
}
}
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String?>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
// Check if we got access to the necessary storage permissions
if (requestCode == PermissionsUtil.REQUEST_ALL_PERMISSION_REQ_CODE) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
var hasReadAccess = false
var hasWriteAccess = false
for (i in permissions.indices) {
if (Manifest.permission.READ_EXTERNAL_STORAGE == permissions[i] && grantResults[i] == PackageManager.PERMISSION_GRANTED) {
hasReadAccess = true
}
if (Manifest.permission.WRITE_EXTERNAL_STORAGE == permissions[i] && grantResults[i] == PackageManager.PERMISSION_GRANTED) {
hasWriteAccess = true
}
}
if (!hasReadAccess || !hasWriteAccess) {
Toast.makeText(
this,
R.string.denied_storage_permission_error_msg,
Toast.LENGTH_LONG
).show()
}
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -36,7 +36,6 @@ import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.AssetManager;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
@ -46,12 +45,10 @@ import android.provider.Settings;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseArray;
import android.view.Display;
import android.view.DisplayCutout;
import android.view.WindowInsets;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
@ -60,7 +57,6 @@ import java.util.Locale;
public class GodotIO {
private static final String TAG = GodotIO.class.getSimpleName();
private final AssetManager am;
private final Activity activity;
private final String uniqueId;
GodotEditText edit;
@ -73,100 +69,8 @@ public class GodotIO {
final int SCREEN_SENSOR_PORTRAIT = 5;
final int SCREEN_SENSOR = 6;
/////////////////////////
/// DIRECTORIES
/////////////////////////
static class AssetDir {
public String[] files;
public int current;
public String path;
}
private int last_dir_id = 1;
private final SparseArray<AssetDir> dirs;
public int dir_open(String path) {
AssetDir ad = new AssetDir();
ad.current = 0;
ad.path = path;
try {
ad.files = am.list(path);
// no way to find path is directory or file exactly.
// but if ad.files.length==0, then it's an empty directory or file.
if (ad.files.length == 0) {
return -1;
}
} catch (IOException e) {
System.out.printf("Exception on dir_open: %s\n", e);
return -1;
}
++last_dir_id;
dirs.put(last_dir_id, ad);
return last_dir_id;
}
public boolean dir_is_dir(int id) {
if (dirs.get(id) == null) {
System.out.printf("dir_next: invalid dir id: %d\n", id);
return false;
}
AssetDir ad = dirs.get(id);
//System.out.printf("go next: %d,%d\n",ad.current,ad.files.length);
int idx = ad.current;
if (idx > 0)
idx--;
if (idx >= ad.files.length)
return false;
String fname = ad.files[idx];
try {
if (ad.path.equals(""))
am.open(fname);
else
am.open(ad.path + "/" + fname);
return false;
} catch (Exception e) {
return true;
}
}
public String dir_next(int id) {
if (dirs.get(id) == null) {
System.out.printf("dir_next: invalid dir id: %d\n", id);
return "";
}
AssetDir ad = dirs.get(id);
//System.out.printf("go next: %d,%d\n",ad.current,ad.files.length);
if (ad.current >= ad.files.length) {
ad.current++;
return "";
}
String r = ad.files[ad.current];
ad.current++;
return r;
}
public void dir_close(int id) {
if (dirs.get(id) == null) {
System.out.printf("dir_close: invalid dir id: %d\n", id);
return;
}
dirs.remove(id);
}
GodotIO(Activity p_activity) {
am = p_activity.getAssets();
activity = p_activity;
dirs = new SparseArray<>();
String androidId = Settings.Secure.getString(activity.getContentResolver(),
Settings.Secure.ANDROID_ID);
if (androidId == null) {

View File

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

View File

@ -0,0 +1,114 @@
/*************************************************************************/
/* StorageScope.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.io
import android.content.Context
import android.os.Build
import android.os.Environment
import java.io.File
/**
* Represents the different storage scopes.
*/
internal enum class StorageScope {
/**
* Covers internal and external directories accessible to the app without restrictions.
*/
APP,
/**
* Covers shared directories (from Android 10 and higher).
*/
SHARED,
/**
* Everything else..
*/
UNKNOWN;
companion object {
/**
* Determines which [StorageScope] the given path falls under.
*/
fun getStorageScope(context: Context, path: String?): StorageScope {
if (path == null) {
return UNKNOWN
}
val pathFile = File(path)
if (!pathFile.isAbsolute) {
return UNKNOWN
}
val canonicalPathFile = pathFile.canonicalPath
val internalAppDir = context.filesDir.canonicalPath ?: return UNKNOWN
if (canonicalPathFile.startsWith(internalAppDir)) {
return APP
}
val internalCacheDir = context.cacheDir.canonicalPath ?: return UNKNOWN
if (canonicalPathFile.startsWith(internalCacheDir)) {
return APP
}
val externalAppDir = context.getExternalFilesDir(null)?.canonicalPath ?: return UNKNOWN
if (canonicalPathFile.startsWith(externalAppDir)) {
return APP
}
val sharedDir = Environment.getExternalStorageDirectory().canonicalPath ?: return UNKNOWN
if (canonicalPathFile.startsWith(sharedDir)) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
// Before R, apps had access to shared storage so long as they have the right
// permissions (and flag on Q).
return APP
}
// Post R, access is limited based on the target destination
// 'Downloads' and 'Documents' are still accessible
val downloadsSharedDir =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).canonicalPath
?: return SHARED
val documentsSharedDir =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).canonicalPath
?: return SHARED
if (canonicalPathFile.startsWith(downloadsSharedDir) || canonicalPathFile.startsWith(documentsSharedDir)) {
return APP
}
return SHARED
}
return UNKNOWN
}
}
}

View File

@ -0,0 +1,177 @@
/*************************************************************************/
/* AssetsDirectoryAccess.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.io.directory
import android.content.Context
import android.util.Log
import android.util.SparseArray
import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.INVALID_DIR_ID
import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_DIR_ID
import java.io.File
import java.io.IOException
/**
* Handles directories access within the Android assets directory.
*/
internal class AssetsDirectoryAccess(context: Context) : DirectoryAccessHandler.DirectoryAccess {
companion object {
private val TAG = AssetsDirectoryAccess::class.java.simpleName
}
private data class AssetDir(val path: String, val files: Array<String>, var current: Int = 0)
private val assetManager = context.assets
private var lastDirId = STARTING_DIR_ID
private val dirs = SparseArray<AssetDir>()
private fun getAssetsPath(originalPath: String): String {
if (originalPath.startsWith(File.separatorChar)) {
return originalPath.substring(1)
}
return originalPath
}
override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0
override fun dirOpen(path: String): Int {
val assetsPath = getAssetsPath(path) ?: return INVALID_DIR_ID
try {
val files = assetManager.list(assetsPath) ?: return INVALID_DIR_ID
// Empty directories don't get added to the 'assets' directory, so
// if ad.files.length > 0 ==> path is directory
// if ad.files.length == 0 ==> path is file
if (files.isEmpty()) {
return INVALID_DIR_ID
}
val ad = AssetDir(assetsPath, files)
dirs.put(++lastDirId, ad)
return lastDirId
} catch (e: IOException) {
Log.e(TAG, "Exception on dirOpen", e)
return INVALID_DIR_ID
}
}
override fun dirExists(path: String): Boolean {
val assetsPath = getAssetsPath(path)
try {
val files = assetManager.list(assetsPath) ?: return false
// Empty directories don't get added to the 'assets' directory, so
// if ad.files.length > 0 ==> path is directory
// if ad.files.length == 0 ==> path is file
return files.isNotEmpty()
} catch (e: IOException) {
Log.e(TAG, "Exception on dirExists", e)
return false
}
}
override fun fileExists(path: String): Boolean {
val assetsPath = getAssetsPath(path) ?: return false
try {
val files = assetManager.list(assetsPath) ?: return false
// Empty directories don't get added to the 'assets' directory, so
// if ad.files.length > 0 ==> path is directory
// if ad.files.length == 0 ==> path is file
return files.isEmpty()
} catch (e: IOException) {
Log.e(TAG, "Exception on fileExists", e)
return false
}
}
override fun dirIsDir(dirId: Int): Boolean {
val ad: AssetDir = dirs[dirId]
var idx = ad.current
if (idx > 0) {
idx--
}
if (idx >= ad.files.size) {
return false
}
val fileName = ad.files[idx]
// List the contents of $fileName. If it's a file, it will be empty, otherwise it'll be a
// directory
val filePath = if (ad.path == "") fileName else "${ad.path}/${fileName}"
val fileContents = assetManager.list(filePath)
return (fileContents?.size?: 0) > 0
}
override fun isCurrentHidden(dirId: Int): Boolean {
val ad = dirs[dirId]
var idx = ad.current
if (idx > 0) {
idx--
}
if (idx >= ad.files.size) {
return false
}
val fileName = ad.files[idx]
return fileName.startsWith('.')
}
override fun dirNext(dirId: Int): String {
val ad: AssetDir = dirs[dirId]
if (ad.current >= ad.files.size) {
ad.current++
return ""
}
return ad.files[ad.current++]
}
override fun dirClose(dirId: Int) {
dirs.remove(dirId)
}
override fun getDriveCount() = 0
override fun getDrive(drive: Int) = ""
override fun makeDir(dir: String) = false
override fun getSpaceLeft() = 0L
override fun rename(from: String, to: String) = false
override fun remove(filename: String) = false
}

View File

@ -0,0 +1,224 @@
/*************************************************************************/
/* DirectoryAccessHandler.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.io.directory
import android.content.Context
import android.util.Log
import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_FILESYSTEM
import org.godotengine.godot.io.directory.DirectoryAccessHandler.AccessType.ACCESS_RESOURCES
/**
* Handles files and directories access and manipulation for the Android platform
*/
class DirectoryAccessHandler(context: Context) {
companion object {
private val TAG = DirectoryAccessHandler::class.java.simpleName
internal const val INVALID_DIR_ID = -1
internal const val STARTING_DIR_ID = 1
private fun getAccessTypeFromNative(accessType: Int): AccessType? {
return when (accessType) {
ACCESS_RESOURCES.nativeValue -> ACCESS_RESOURCES
ACCESS_FILESYSTEM.nativeValue -> ACCESS_FILESYSTEM
else -> null
}
}
}
private enum class AccessType(val nativeValue: Int) {
ACCESS_RESOURCES(0), ACCESS_FILESYSTEM(2)
}
internal interface DirectoryAccess {
fun dirOpen(path: String): Int
fun dirNext(dirId: Int): String
fun dirClose(dirId: Int)
fun dirIsDir(dirId: Int): Boolean
fun dirExists(path: String): Boolean
fun fileExists(path: String): Boolean
fun hasDirId(dirId: Int): Boolean
fun isCurrentHidden(dirId: Int): Boolean
fun getDriveCount() : Int
fun getDrive(drive: Int): String
fun makeDir(dir: String): Boolean
fun getSpaceLeft(): Long
fun rename(from: String, to: String): Boolean
fun remove(filename: String): Boolean
}
private val assetsDirAccess = AssetsDirectoryAccess(context)
private val fileSystemDirAccess = FilesystemDirectoryAccess(context)
private fun hasDirId(accessType: AccessType, dirId: Int): Boolean {
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.hasDirId(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.hasDirId(dirId)
}
}
fun dirOpen(nativeAccessType: Int, path: String?): Int {
val accessType = getAccessTypeFromNative(nativeAccessType)
if (path == null || accessType == null) {
return INVALID_DIR_ID
}
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirOpen(path)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirOpen(path)
}
}
fun dirNext(nativeAccessType: Int, dirId: Int): String {
val accessType = getAccessTypeFromNative(nativeAccessType)
if (accessType == null || !hasDirId(accessType, dirId)) {
Log.w(TAG, "dirNext: Invalid dir id: $dirId")
return ""
}
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirNext(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirNext(dirId)
}
}
fun dirClose(nativeAccessType: Int, dirId: Int) {
val accessType = getAccessTypeFromNative(nativeAccessType)
if (accessType == null || !hasDirId(accessType, dirId)) {
Log.w(TAG, "dirClose: Invalid dir id: $dirId")
return
}
when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirClose(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirClose(dirId)
}
}
fun dirIsDir(nativeAccessType: Int, dirId: Int): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType)
if (accessType == null || !hasDirId(accessType, dirId)) {
Log.w(TAG, "dirIsDir: Invalid dir id: $dirId")
return false
}
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirIsDir(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirIsDir(dirId)
}
}
fun isCurrentHidden(nativeAccessType: Int, dirId: Int): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType)
if (accessType == null || !hasDirId(accessType, dirId)) {
return false
}
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.isCurrentHidden(dirId)
ACCESS_FILESYSTEM -> fileSystemDirAccess.isCurrentHidden(dirId)
}
}
fun dirExists(nativeAccessType: Int, path: String?): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType)
if (path == null || accessType == null) {
return false
}
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.dirExists(path)
ACCESS_FILESYSTEM -> fileSystemDirAccess.dirExists(path)
}
}
fun fileExists(nativeAccessType: Int, path: String?): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType)
if (path == null || accessType == null) {
return false
}
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.fileExists(path)
ACCESS_FILESYSTEM -> fileSystemDirAccess.fileExists(path)
}
}
fun getDriveCount(nativeAccessType: Int): Int {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0
return when(accessType) {
ACCESS_RESOURCES -> assetsDirAccess.getDriveCount()
ACCESS_FILESYSTEM -> fileSystemDirAccess.getDriveCount()
}
}
fun getDrive(nativeAccessType: Int, drive: Int): String {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return ""
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.getDrive(drive)
ACCESS_FILESYSTEM -> fileSystemDirAccess.getDrive(drive)
}
}
fun makeDir(nativeAccessType: Int, dir: String): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.makeDir(dir)
ACCESS_FILESYSTEM -> fileSystemDirAccess.makeDir(dir)
}
}
fun getSpaceLeft(nativeAccessType: Int): Long {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return 0L
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.getSpaceLeft()
ACCESS_FILESYSTEM -> fileSystemDirAccess.getSpaceLeft()
}
}
fun rename(nativeAccessType: Int, from: String, to: String): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.rename(from, to)
ACCESS_FILESYSTEM -> fileSystemDirAccess.rename(from, to)
}
}
fun remove(nativeAccessType: Int, filename: String): Boolean {
val accessType = getAccessTypeFromNative(nativeAccessType) ?: return false
return when (accessType) {
ACCESS_RESOURCES -> assetsDirAccess.remove(filename)
ACCESS_FILESYSTEM -> fileSystemDirAccess.remove(filename)
}
}
}

View File

@ -0,0 +1,230 @@
/*************************************************************************/
/* FileSystemDirectoryAccess.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.io.directory
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.os.storage.StorageManager
import android.util.Log
import android.util.SparseArray
import org.godotengine.godot.io.StorageScope
import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.INVALID_DIR_ID
import org.godotengine.godot.io.directory.DirectoryAccessHandler.Companion.STARTING_DIR_ID
import org.godotengine.godot.io.file.FileAccessHandler
import java.io.File
/**
* Handles directories access with the internal and external filesystem.
*/
internal class FilesystemDirectoryAccess(private val context: Context):
DirectoryAccessHandler.DirectoryAccess {
companion object {
private val TAG = FilesystemDirectoryAccess::class.java.simpleName
}
private data class DirData(val dirFile: File, val files: Array<File>, var current: Int = 0)
private val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
private var lastDirId = STARTING_DIR_ID
private val dirs = SparseArray<DirData>()
private fun inScope(path: String): Boolean {
// Directory access is available for shared storage on Android 11+
// On Android 10, access is also available as long as the `requestLegacyExternalStorage`
// tag is available.
return StorageScope.getStorageScope(context, path) != StorageScope.UNKNOWN
}
override fun hasDirId(dirId: Int) = dirs.indexOfKey(dirId) >= 0
override fun dirOpen(path: String): Int {
if (!inScope(path)) {
Log.w(TAG, "Path $path is not accessible.")
return INVALID_DIR_ID
}
// Check this is a directory.
val dirFile = File(path)
if (!dirFile.isDirectory) {
return INVALID_DIR_ID
}
// Get the files in the directory
val files = dirFile.listFiles()?: return INVALID_DIR_ID
// Create the data representing this directory
val dirData = DirData(dirFile, files)
dirs.put(++lastDirId, dirData)
return lastDirId
}
override fun dirExists(path: String): Boolean {
if (!inScope(path)) {
Log.w(TAG, "Path $path is not accessible.")
return false
}
try {
return File(path).isDirectory
} catch (e: SecurityException) {
return false
}
}
override fun fileExists(path: String) = FileAccessHandler.fileExists(context, path)
override fun dirNext(dirId: Int): String {
val dirData = dirs[dirId]
if (dirData.current >= dirData.files.size) {
dirData.current++
return ""
}
return dirData.files[dirData.current++].name
}
override fun dirClose(dirId: Int) {
dirs.remove(dirId)
}
override fun dirIsDir(dirId: Int): Boolean {
val dirData = dirs[dirId]
var index = dirData.current
if (index > 0) {
index--
}
if (index >= dirData.files.size) {
return false
}
return dirData.files[index].isDirectory
}
override fun isCurrentHidden(dirId: Int): Boolean {
val dirData = dirs[dirId]
var index = dirData.current
if (index > 0) {
index--
}
if (index >= dirData.files.size) {
return false
}
return dirData.files[index].isHidden
}
override fun getDriveCount(): Int {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
storageManager.storageVolumes.size
} else {
0
}
}
override fun getDrive(drive: Int): String {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return ""
}
if (drive < 0 || drive >= storageManager.storageVolumes.size) {
return ""
}
val storageVolume = storageManager.storageVolumes[drive]
return storageVolume.getDescription(context)
}
override fun makeDir(dir: String): Boolean {
if (!inScope(dir)) {
Log.w(TAG, "Directory $dir is not accessible.")
return false
}
try {
val dirFile = File(dir)
return dirFile.isDirectory || dirFile.mkdirs()
} catch (e: SecurityException) {
return false
}
}
@SuppressLint("UsableSpace")
override fun getSpaceLeft() = context.getExternalFilesDir(null)?.usableSpace ?: 0L
override fun rename(from: String, to: String): Boolean {
if (!inScope(from) || !inScope(to)) {
Log.w(TAG, "Argument filenames are not accessible:\n" +
"from: $from\n" +
"to: $to")
return false
}
return try {
val fromFile = File(from)
if (fromFile.isDirectory) {
fromFile.renameTo(File(to))
} else {
FileAccessHandler.renameFile(context, from, to)
}
} catch (e: SecurityException) {
false
}
}
override fun remove(filename: String): Boolean {
if (!inScope(filename)) {
Log.w(TAG, "Filename $filename is not accessible.")
return false
}
return try {
val deleteFile = File(filename)
if (deleteFile.exists()) {
if (deleteFile.isDirectory) {
deleteFile.delete()
} else {
FileAccessHandler.removeFile(context, filename)
}
} else {
true
}
} catch (e: SecurityException) {
false
}
}
}

View File

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

View File

@ -0,0 +1,87 @@
/*************************************************************************/
/* FileAccessFlags.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.io.file
/**
* Android representation of Godot native access flags.
*/
internal enum class FileAccessFlags(val nativeValue: Int) {
/**
* Opens the file for read operations.
* The cursor is positioned at the beginning of the file.
*/
READ(1),
/**
* Opens the file for write operations.
* The file is created if it does not exist, and truncated if it does.
*/
WRITE(2),
/**
* Opens the file for read and write operations.
* Does not truncate the file. The cursor is positioned at the beginning of the file.
*/
READ_WRITE(3),
/**
* Opens the file for read and write operations.
* The file is created if it does not exist, and truncated if it does.
* The cursor is positioned at the beginning of the file.
*/
WRITE_READ(7);
fun getMode(): String {
return when (this) {
READ -> "r"
WRITE -> "w"
READ_WRITE, WRITE_READ -> "rw"
}
}
fun shouldTruncate(): Boolean {
return when (this) {
READ, READ_WRITE -> false
WRITE, WRITE_READ -> true
}
}
companion object {
fun fromNativeModeFlags(modeFlag: Int): FileAccessFlags? {
for (flag in values()) {
if (flag.nativeValue == modeFlag) {
return flag
}
}
return null
}
}
}

View File

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

View File

@ -0,0 +1,93 @@
/*************************************************************************/
/* FileData.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.io.file
import java.io.File
import java.io.FileOutputStream
import java.io.RandomAccessFile
import java.nio.channels.FileChannel
/**
* Implementation of [DataAccess] which handles regular (not scoped) file access and interactions.
*/
internal class FileData(filePath: String, accessFlag: FileAccessFlags) : DataAccess(filePath) {
companion object {
private val TAG = FileData::class.java.simpleName
fun fileExists(path: String): Boolean {
return try {
File(path).isFile
} catch (e: SecurityException) {
false
}
}
fun fileLastModified(filepath: String): Long {
return try {
File(filepath).lastModified()
} catch (e: SecurityException) {
0L
}
}
fun delete(filepath: String): Boolean {
return try {
File(filepath).delete()
} catch (e: Exception) {
false
}
}
fun rename(from: String, to: String): Boolean {
return try {
val fromFile = File(from)
fromFile.renameTo(File(to))
} catch (e: Exception) {
false
}
}
}
override val fileChannel: FileChannel
init {
if (accessFlag == FileAccessFlags.WRITE) {
fileChannel = FileOutputStream(filePath, !accessFlag.shouldTruncate()).channel
} else {
fileChannel = RandomAccessFile(filePath, accessFlag.getMode()).channel
}
if (accessFlag.shouldTruncate()) {
fileChannel.truncate(0)
}
}
}

View File

@ -0,0 +1,284 @@
/*************************************************************************/
/* MediaStoreData.kt */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
package org.godotengine.godot.io.file
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.nio.channels.FileChannel
/**
* Implementation of [DataAccess] which handles access and interactions with file and data
* under scoped storage via the MediaStore API.
*/
@RequiresApi(Build.VERSION_CODES.Q)
internal class MediaStoreData(context: Context, filePath: String, accessFlag: FileAccessFlags) :
DataAccess(filePath) {
private data class DataItem(
val id: Long,
val uri: Uri,
val displayName: String,
val relativePath: String,
val size: Int,
val dateModified: Int,
val mediaType: Int
)
companion object {
private val TAG = MediaStoreData::class.java.simpleName
private val COLLECTION = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
private val PROJECTION = arrayOf(
MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.DISPLAY_NAME,
MediaStore.Files.FileColumns.RELATIVE_PATH,
MediaStore.Files.FileColumns.SIZE,
MediaStore.Files.FileColumns.DATE_MODIFIED,
MediaStore.Files.FileColumns.MEDIA_TYPE,
)
private const val SELECTION_BY_PATH = "${MediaStore.Files.FileColumns.DISPLAY_NAME} = ? " +
" AND ${MediaStore.Files.FileColumns.RELATIVE_PATH} = ?"
private fun getSelectionByPathArguments(path: String): Array<String> {
return arrayOf(getMediaStoreDisplayName(path), getMediaStoreRelativePath(path))
}
private const val SELECTION_BY_ID = "${MediaStore.Files.FileColumns._ID} = ? "
private fun getSelectionByIdArgument(id: Long) = arrayOf(id.toString())
private fun getMediaStoreDisplayName(path: String) = File(path).name
private fun getMediaStoreRelativePath(path: String): String {
val pathFile = File(path)
val environmentDir = Environment.getExternalStorageDirectory()
var relativePath = (pathFile.parent?.replace(environmentDir.absolutePath, "") ?: "").trim('/')
if (relativePath.isNotBlank()) {
relativePath += "/"
}
return relativePath
}
private fun queryById(context: Context, id: Long): List<DataItem> {
val query = context.contentResolver.query(
COLLECTION,
PROJECTION,
SELECTION_BY_ID,
getSelectionByIdArgument(id),
null
)
return dataItemFromCursor(query)
}
private fun queryByPath(context: Context, path: String): List<DataItem> {
val query = context.contentResolver.query(
COLLECTION,
PROJECTION,
SELECTION_BY_PATH,
getSelectionByPathArguments(path),
null
)
return dataItemFromCursor(query)
}
private fun dataItemFromCursor(query: Cursor?): List<DataItem> {
query?.use { cursor ->
cursor.count
if (cursor.count == 0) {
return emptyList()
}
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)
val displayNameColumn =
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME)
val relativePathColumn =
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.RELATIVE_PATH)
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE)
val dateModifiedColumn =
cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATE_MODIFIED)
val mediaTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE)
val result = ArrayList<DataItem>()
while (cursor.moveToNext()) {
val id = cursor.getLong(idColumn)
result.add(
DataItem(
id,
ContentUris.withAppendedId(COLLECTION, id),
cursor.getString(displayNameColumn),
cursor.getString(relativePathColumn),
cursor.getInt(sizeColumn),
cursor.getInt(dateModifiedColumn),
cursor.getInt(mediaTypeColumn)
)
)
}
return result
}
return emptyList()
}
private fun addFile(context: Context, path: String): DataItem? {
val fileDetails = ContentValues().apply {
put(MediaStore.Files.FileColumns._ID, 0)
put(MediaStore.Files.FileColumns.DISPLAY_NAME, getMediaStoreDisplayName(path))
put(MediaStore.Files.FileColumns.RELATIVE_PATH, getMediaStoreRelativePath(path))
}
context.contentResolver.insert(COLLECTION, fileDetails) ?: return null
// File was successfully added, let's retrieve its info
val infos = queryByPath(context, path)
if (infos.isEmpty()) {
return null
}
return infos[0]
}
fun delete(context: Context, path: String): Boolean {
val itemsToDelete = queryByPath(context, path)
if (itemsToDelete.isEmpty()) {
return false
}
val resolver = context.contentResolver
var itemsDeleted = 0
for (item in itemsToDelete) {
itemsDeleted += resolver.delete(item.uri, null, null)
}
return itemsDeleted > 0
}
fun fileExists(context: Context, path: String): Boolean {
return queryByPath(context, path).isNotEmpty()
}
fun fileLastModified(context: Context, path: String): Long {
val result = queryByPath(context, path)
if (result.isEmpty()) {
return 0L
}
val dataItem = result[0]
return dataItem.dateModified.toLong()
}
fun rename(context: Context, from: String, to: String): Boolean {
// Ensure the source exists.
val sources = queryByPath(context, from)
if (sources.isEmpty()) {
return false
}
// Take the first source
val source = sources[0]
// Set up the updated values
val updatedDetails = ContentValues().apply {
put(MediaStore.Files.FileColumns.DISPLAY_NAME, getMediaStoreDisplayName(to))
put(MediaStore.Files.FileColumns.RELATIVE_PATH, getMediaStoreRelativePath(to))
}
val updated = context.contentResolver.update(
source.uri,
updatedDetails,
SELECTION_BY_ID,
getSelectionByIdArgument(source.id)
)
return updated > 0
}
}
private val id: Long
private val uri: Uri
override val fileChannel: FileChannel
init {
val contentResolver = context.contentResolver
val dataItems = queryByPath(context, filePath)
val dataItem = when (accessFlag) {
FileAccessFlags.READ -> {
// The file should already exist
if (dataItems.isEmpty()) {
throw FileNotFoundException("Unable to access file $filePath")
}
val dataItem = dataItems[0]
dataItem
}
FileAccessFlags.WRITE, FileAccessFlags.READ_WRITE, FileAccessFlags.WRITE_READ -> {
// Create the file if it doesn't exist
val dataItem = if (dataItems.isEmpty()) {
addFile(context, filePath)
} else {
dataItems[0]
}
if (dataItem == null) {
throw FileNotFoundException("Unable to access file $filePath")
}
dataItem
}
}
id = dataItem.id
uri = dataItem.uri
val parcelFileDescriptor = contentResolver.openFileDescriptor(uri, accessFlag.getMode())
?: throw IllegalStateException("Unable to access file descriptor")
fileChannel = if (accessFlag == FileAccessFlags.READ) {
FileInputStream(parcelFileDescriptor.fileDescriptor).channel
} else {
FileOutputStream(parcelFileDescriptor.fileDescriptor).channel
}
if (accessFlag.shouldTruncate()) {
fileChannel.truncate(0)
}
}
}

View File

@ -32,10 +32,14 @@ package org.godotengine.godot.utils;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.Settings;
import android.util.Log;
import androidx.core.content.ContextCompat;
@ -53,7 +57,8 @@ public final class PermissionsUtil {
static final int REQUEST_RECORD_AUDIO_PERMISSION = 1;
static final int REQUEST_CAMERA_PERMISSION = 2;
static final int REQUEST_VIBRATE_PERMISSION = 3;
static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001;
public static final int REQUEST_ALL_PERMISSION_REQ_CODE = 1001;
public static final int REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE = 2002;
private PermissionsUtil() {
}
@ -108,13 +113,26 @@ public final class PermissionsUtil {
if (manifestPermissions.length == 0)
return true;
List<String> dangerousPermissions = new ArrayList<>();
List<String> requestedPermissions = new ArrayList<>();
for (String manifestPermission : manifestPermissions) {
try {
PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) {
dangerousPermissions.add(manifestPermission);
if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
try {
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
intent.setData(Uri.parse(String.format("package:%s", activity.getPackageName())));
activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
} catch (Exception ignored) {
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
activity.startActivityForResult(intent, REQUEST_MANAGE_EXTERNAL_STORAGE_REQ_CODE);
}
}
} else {
PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) != PackageManager.PERMISSION_GRANTED) {
requestedPermissions.add(manifestPermission);
}
}
} catch (PackageManager.NameNotFoundException e) {
// Skip this permission and continue.
@ -122,13 +140,12 @@ public final class PermissionsUtil {
}
}
if (dangerousPermissions.isEmpty()) {
if (requestedPermissions.isEmpty()) {
// If list is empty, all of dangerous permissions were granted.
return true;
}
String[] requestedPermissions = dangerousPermissions.toArray(new String[0]);
activity.requestPermissions(requestedPermissions, REQUEST_ALL_PERMISSION_REQ_CODE);
activity.requestPermissions(requestedPermissions.toArray(new String[0]), REQUEST_ALL_PERMISSION_REQ_CODE);
return false;
}
@ -148,13 +165,19 @@ public final class PermissionsUtil {
if (manifestPermissions.length == 0)
return manifestPermissions;
List<String> dangerousPermissions = new ArrayList<>();
List<String> grantedPermissions = new ArrayList<>();
for (String manifestPermission : manifestPermissions) {
try {
PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
dangerousPermissions.add(manifestPermission);
if (manifestPermission.equals(Manifest.permission.MANAGE_EXTERNAL_STORAGE)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
grantedPermissions.add(manifestPermission);
}
} else {
PermissionInfo permissionInfo = getPermissionInfo(activity, manifestPermission);
int protectionLevel = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? permissionInfo.getProtection() : permissionInfo.protectionLevel;
if (protectionLevel == PermissionInfo.PROTECTION_DANGEROUS && ContextCompat.checkSelfPermission(activity, manifestPermission) == PackageManager.PERMISSION_GRANTED) {
grantedPermissions.add(manifestPermission);
}
}
} catch (PackageManager.NameNotFoundException e) {
// Skip this permission and continue.
@ -162,7 +185,7 @@ public final class PermissionsUtil {
}
}
return dangerousPermissions.toArray(new String[0]);
return grantedPermissions.toArray(new String[0]);
}
/**
@ -177,7 +200,7 @@ public final class PermissionsUtil {
if (permission.equals(p))
return true;
}
} catch (PackageManager.NameNotFoundException e) {
} catch (PackageManager.NameNotFoundException ignored) {
}
return false;

View File

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

View File

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

View File

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

View File

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