diff --git a/src/xenia/base/filesystem.h b/src/xenia/base/filesystem.h index c8cff150f..cca30dd3f 100644 --- a/src/xenia/base/filesystem.h +++ b/src/xenia/base/filesystem.h @@ -14,8 +14,10 @@ #include #include #include +#include #include +#include "xenia/base/platform.h" #include "xenia/base/string.h" namespace xe { @@ -122,6 +124,14 @@ struct FileInfo { bool GetInfo(const std::filesystem::path& path, FileInfo* out_info); std::vector ListFiles(const std::filesystem::path& path); +#if XE_PLATFORM_ANDROID +void AndroidInitialize(); +void AndroidShutdown(); +bool IsAndroidContentUri(const std::string_view source); +int OpenAndroidContentFileDescriptor(const std::string_view uri, + const char* mode); +#endif // XE_PLATFORM_ANDROID + } // namespace filesystem } // namespace xe diff --git a/src/xenia/base/filesystem_android.cc b/src/xenia/base/filesystem_android.cc new file mode 100644 index 000000000..12f5c2d7d --- /dev/null +++ b/src/xenia/base/filesystem_android.cc @@ -0,0 +1,278 @@ +/** + ****************************************************************************** + * Xenia : Xbox 360 Emulator Research Project * + ****************************************************************************** + * Copyright 2022 Ben Vanik. All rights reserved. * + * Released under the BSD license - see LICENSE in the root for more details. * + ****************************************************************************** + */ + +#include +#include +#include + +#include "xenia/base/filesystem.h" +#include "xenia/base/main_android.h" +#include "xenia/base/math.h" +#include "xenia/base/string.h" + +namespace xe { +namespace filesystem { + +// Using Android logging because the file system may need to be initialized +// before logging. + +static jobject android_content_resolver_; +static jclass android_content_resolver_class_; +static jmethodID android_content_resolver_open_file_descriptor_; + +static jclass android_parcel_file_descriptor_class_; +static jmethodID android_parcel_file_descriptor_detach_fd_; + +static jclass android_uri_class_; +static jmethodID android_uri_parse_; + +static bool android_content_resolver_initialized_; + +static void AndroidShutdownContentResolver() { + android_content_resolver_initialized_ = false; + android_uri_parse_ = nullptr; + android_parcel_file_descriptor_detach_fd_ = nullptr; + android_content_resolver_open_file_descriptor_ = nullptr; + JNIEnv* jni_env = GetAndroidThreadJniEnv(); + if (jni_env) { + if (android_uri_class_) { + jni_env->DeleteGlobalRef(android_uri_class_); + } + if (android_parcel_file_descriptor_class_) { + jni_env->DeleteGlobalRef( + reinterpret_cast(android_parcel_file_descriptor_class_)); + } + if (android_content_resolver_class_) { + jni_env->DeleteGlobalRef( + reinterpret_cast(android_content_resolver_class_)); + } + if (android_content_resolver_) { + jni_env->DeleteGlobalRef( + reinterpret_cast(android_content_resolver_)); + } + } + android_uri_class_ = nullptr; + android_parcel_file_descriptor_class_ = nullptr; + android_content_resolver_class_ = nullptr; + android_content_resolver_ = nullptr; +} + +static void AndroidInitializeContentResolver() { + JNIEnv* jni_env = GetAndroidThreadJniEnv(); + if (!jni_env) { + return; + } + jobject application_context = GetAndroidApplicationContext(); + if (!application_context) { + return; + } + { + jclass context_class = jni_env->GetObjectClass(application_context); + if (!context_class) { + __android_log_write(ANDROID_LOG_ERROR, "AndroidInitializeContentResolver", + "Failed to get the context class"); + AndroidShutdownContentResolver(); + return; + } + jmethodID context_get_content_resolver = + jni_env->GetMethodID(context_class, "getContentResolver", + "()Landroid/content/ContentResolver;"); + if (!context_get_content_resolver) { + __android_log_write( + ANDROID_LOG_ERROR, "AndroidInitializeContentResolver", + "Failed to get the getContentResolver method of the context"); + jni_env->DeleteLocalRef(reinterpret_cast(context_class)); + AndroidShutdownContentResolver(); + return; + } + jobject content_resolver = jni_env->CallObjectMethod( + application_context, context_get_content_resolver); + jni_env->DeleteLocalRef(reinterpret_cast(context_class)); + if (!content_resolver) { + __android_log_write(ANDROID_LOG_ERROR, "AndroidInitializeContentResolver", + "Failed to get the content resolver"); + AndroidShutdownContentResolver(); + return; + } + android_content_resolver_ = jni_env->NewGlobalRef(content_resolver); + jni_env->DeleteLocalRef(content_resolver); + } + if (!android_content_resolver_) { + __android_log_write( + ANDROID_LOG_ERROR, "AndroidInitializeContentResolver", + "Failed to create a global reference to the content resolver"); + AndroidShutdownContentResolver(); + return; + } + { + jobject content_resolver_class = + jni_env->GetObjectClass(android_content_resolver_); + if (!content_resolver_class) { + __android_log_write(ANDROID_LOG_ERROR, "AndroidInitializeContentResolver", + "Failed to get the content resolver class"); + AndroidShutdownContentResolver(); + return; + } + android_content_resolver_class_ = + reinterpret_cast(jni_env->NewGlobalRef( + reinterpret_cast(content_resolver_class))); + jni_env->DeleteLocalRef(reinterpret_cast(content_resolver_class)); + } + if (!android_content_resolver_class_) { + __android_log_write( + ANDROID_LOG_ERROR, "AndroidInitializeContentResolver", + "Failed to create a global reference to the content resolver class"); + AndroidShutdownContentResolver(); + return; + } + android_content_resolver_open_file_descriptor_ = jni_env->GetMethodID( + android_content_resolver_class_, "openFileDescriptor", + "(Landroid/net/Uri;Ljava/lang/String;)Landroid/os/ParcelFileDescriptor;"); + if (!android_content_resolver_open_file_descriptor_) { + __android_log_write( + ANDROID_LOG_ERROR, "AndroidInitializeContentResolver", + "Failed to get the openFileDescriptor method of the content resolver"); + AndroidShutdownContentResolver(); + return; + } + + { + jclass parcel_file_descriptor_class = + jni_env->FindClass("android/os/ParcelFileDescriptor"); + if (!parcel_file_descriptor_class) { + __android_log_write(ANDROID_LOG_ERROR, "AndroidInitializeContentResolver", + "Failed to get the parcel file descriptor class"); + AndroidShutdownContentResolver(); + return; + } + android_parcel_file_descriptor_class_ = + reinterpret_cast(jni_env->NewGlobalRef( + reinterpret_cast(parcel_file_descriptor_class))); + jni_env->DeleteLocalRef( + reinterpret_cast(parcel_file_descriptor_class)); + } + if (!android_parcel_file_descriptor_class_) { + __android_log_write( + ANDROID_LOG_ERROR, "AndroidInitializeContentResolver", + "Failed to create a global reference to the parcel file descriptor " + "class"); + AndroidShutdownContentResolver(); + return; + } + android_parcel_file_descriptor_detach_fd_ = jni_env->GetMethodID( + android_parcel_file_descriptor_class_, "detachFd", "()I"); + if (!android_parcel_file_descriptor_detach_fd_) { + __android_log_write( + ANDROID_LOG_ERROR, "AndroidInitializeContentResolver", + "Failed to get the detachFd method of the parcel file descriptor"); + AndroidShutdownContentResolver(); + return; + } + + { + jclass uri_class = jni_env->FindClass("android/net/Uri"); + if (!uri_class) { + __android_log_write(ANDROID_LOG_ERROR, "AndroidInitializeContentResolver", + "Failed to get the URI class"); + AndroidShutdownContentResolver(); + return; + } + android_uri_class_ = reinterpret_cast( + jni_env->NewGlobalRef(reinterpret_cast(uri_class))); + jni_env->DeleteLocalRef(reinterpret_cast(uri_class)); + } + if (!android_uri_class_) { + __android_log_write(ANDROID_LOG_ERROR, "AndroidInitializeContentResolver", + "Failed to create a global reference to the URI class"); + AndroidShutdownContentResolver(); + return; + } + android_uri_parse_ = jni_env->GetStaticMethodID( + android_uri_class_, "parse", "(Ljava/lang/String;)Landroid/net/Uri;"); + if (!android_uri_parse_) { + __android_log_write(ANDROID_LOG_ERROR, "AndroidInitializeContentResolver", + "Failed to get the parse method of the URI class"); + AndroidShutdownContentResolver(); + return; + } + + android_content_resolver_initialized_ = true; +} + +void AndroidInitialize() { AndroidInitializeContentResolver(); } + +void AndroidShutdown() { AndroidShutdownContentResolver(); } + +bool IsAndroidContentUri(const std::string_view source) { + // A URI schema is case-insensitive. Though just content: defines the schema, + // still including // in the comparison to distinguish from a file with a name + // starting from content: (as this is the main purpose of this code - + // separating URIs from file paths) more clearly. + static const char kContentSchema[] = "content://"; + constexpr size_t kContentSchemaLength = xe::countof(kContentSchema) - 1; + return source.size() >= kContentSchemaLength && + !xe_strncasecmp(source.data(), kContentSchema, kContentSchemaLength); +} + +int OpenAndroidContentFileDescriptor(const std::string_view uri, + const char* mode) { + if (!android_content_resolver_initialized_) { + return -1; + } + JNIEnv* jni_env = GetAndroidThreadJniEnv(); + if (!jni_env) { + return -1; + } + + jobject uri_object; + { + jstring uri_string; + { + std::u16string uri_u16 = xe::to_utf16(uri); + uri_string = jni_env->NewString( + reinterpret_cast(uri_u16.data()), uri_u16.size()); + } + if (!uri_string) { + return -1; + } + uri_object = jni_env->CallStaticObjectMethod( + android_uri_class_, android_uri_parse_, uri_string); + jni_env->DeleteLocalRef(uri_string); + } + if (!uri_object) { + return -1; + } + + jstring mode_string = jni_env->NewStringUTF(mode); + if (!mode_string) { + jni_env->DeleteLocalRef(uri_object); + return -1; + } + + jobject parcel_file_descriptor = jni_env->CallObjectMethod( + android_content_resolver_, android_content_resolver_open_file_descriptor_, + uri_object, mode_string); + jni_env->DeleteLocalRef(mode_string); + jni_env->DeleteLocalRef(uri_object); + if (jni_env->ExceptionCheck()) { + jni_env->ExceptionClear(); + return -1; + } + if (!parcel_file_descriptor) { + return -1; + } + int file_descriptor = jni_env->CallIntMethod( + parcel_file_descriptor, android_parcel_file_descriptor_detach_fd_); + jni_env->DeleteLocalRef(parcel_file_descriptor); + + return file_descriptor; +} + +} // namespace filesystem +} // namespace xe diff --git a/src/xenia/base/main_android.cc b/src/xenia/base/main_android.cc index eaa4642c3..5e43dbcad 100644 --- a/src/xenia/base/main_android.cc +++ b/src/xenia/base/main_android.cc @@ -16,6 +16,7 @@ #include "xenia/base/assert.h" #include "xenia/base/cvar.h" +#include "xenia/base/filesystem.h" #include "xenia/base/logging.h" #include "xenia/base/memory.h" #include "xenia/base/system.h" @@ -98,6 +99,8 @@ void InitializeAndroidAppFromMainThread(int32_t api_level, // Logging uses threading. xe::threading::AndroidInitialize(); + xe::filesystem::AndroidInitialize(); + // Initialize the cvars before logging. if (launch_arguments_bundle) { cvar::ParseLaunchArgumentsFromAndroidBundle(launch_arguments_bundle); @@ -135,6 +138,8 @@ void ShutdownAndroidAppFromMainThread() { xe::ShutdownLogging(); + xe::filesystem::AndroidShutdown(); + xe::threading::AndroidShutdown(); if (android_application_context_) {