diff --git a/android/app/src/cpp/android_host_interface.cpp b/android/app/src/cpp/android_host_interface.cpp index d722ad278..b66c3bc67 100644 --- a/android/app/src/cpp/android_host_interface.cpp +++ b/android/app/src/cpp/android_host_interface.cpp @@ -970,7 +970,8 @@ extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved) jclass emulation_activity_class; if ((s_AndroidHostInterface_constructor = - env->GetMethodID(s_AndroidHostInterface_class, "", "(Landroid/content/Context;)V")) == nullptr || + env->GetMethodID(s_AndroidHostInterface_class, "", + "(Landroid/content/Context;Lcom/github/stenzek/duckstation/FileHelper;)V")) == nullptr || (s_AndroidHostInterface_field_mNativePointer = env->GetFieldID(s_AndroidHostInterface_class, "mNativePointer", "J")) == nullptr || (s_AndroidHostInterface_method_reportError = @@ -1067,12 +1068,13 @@ DEFINE_JNI_ARGS_METHOD(void, AndroidHostInterface_setThreadAffinity, jobject unu } DEFINE_JNI_ARGS_METHOD(jobject, AndroidHostInterface_create, jobject unused, jobject context_object, - jstring user_directory) + jobject file_helper_object, jstring user_directory) { Log::SetDebugOutputParams(true, nullptr, LOGLEVEL_DEBUG); // initialize the java side - jobject java_obj = env->NewObject(s_AndroidHostInterface_class, s_AndroidHostInterface_constructor, context_object); + jobject java_obj = env->NewObject(s_AndroidHostInterface_class, s_AndroidHostInterface_constructor, context_object, + file_helper_object); if (!java_obj) { Log_ErrorPrint("Failed to create Java AndroidHostInterface"); @@ -1096,6 +1098,7 @@ DEFINE_JNI_ARGS_METHOD(jobject, AndroidHostInterface_create, jobject unused, job env->SetLongField(java_obj, s_AndroidHostInterface_field_mNativePointer, static_cast(reinterpret_cast(cpp_obj))); + FileSystem::SetAndroidFileHelper(s_jvm, env, file_helper_object); return java_obj; } @@ -1195,7 +1198,7 @@ DEFINE_JNI_ARGS_METHOD(jint, AndroidHostInterface_getControllerAxisType, jobject jstring axis_name) { std::optional type = - Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str()); + Settings::ParseControllerTypeName(AndroidHelpers::JStringToString(env, controller_type).c_str()); if (!type) return -1; @@ -1342,7 +1345,7 @@ static jobject CreateGameListEntry(JNIEnv* env, AndroidHostInterface* hi, const { const Timestamp modified_ts( Timestamp::FromUnixTimestamp(static_cast(entry.last_modified_time))); - const std::string file_title_str(System::GetTitleForPath(entry.path.c_str())); + const std::string file_title_str(FileSystem::GetFileTitleFromPath(entry.path)); const std::string cover_path_str(hi->GetGameList()->GetCoverImagePathForEntry(&entry)); jstring path = env->NewStringUTF(entry.path.c_str()); diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java index a64d13009..9c62dc993 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/AndroidHostInterface.java @@ -23,9 +23,11 @@ public class AndroidHostInterface { private long mNativePointer; private Context mContext; + private FileHelper mFileHelper; - public AndroidHostInterface(Context context) { + public AndroidHostInterface(Context context, FileHelper fileHelper) { this.mContext = context; + this.mFileHelper = fileHelper; } public void reportError(String message) { @@ -54,7 +56,7 @@ public class AndroidHostInterface { static public native boolean setThreadAffinity(int[] cpus); - static public native AndroidHostInterface create(Context context, String userDirectory); + static public native AndroidHostInterface create(Context context, FileHelper fileHelper, String userDirectory); public native boolean isEmulationThreadRunning(); @@ -184,7 +186,7 @@ public class AndroidHostInterface { mUserDirectory += "/duckstation"; Log.i("AndroidHostInterface", "User directory: " + mUserDirectory); - mInstance = create(context, mUserDirectory); + mInstance = create(context, new FileHelper(context), mUserDirectory); return mInstance != null; } diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/FileHelper.java b/android/app/src/main/java/com/github/stenzek/duckstation/FileHelper.java new file mode 100644 index 000000000..dae26fcb4 --- /dev/null +++ b/android/app/src/main/java/com/github/stenzek/duckstation/FileHelper.java @@ -0,0 +1,159 @@ +package com.github.stenzek.duckstation; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; + +import java.util.ArrayList; + +/** + * File helper class - used to bridge native code to Java storage access framework APIs. + */ +public class FileHelper { + /** + * Native filesystem flags. + */ + public static final int FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY = 1; + public static final int FILESYSTEM_FILE_ATTRIBUTE_READ_ONLY = 2; + public static final int FILESYSTEM_FILE_ATTRIBUTE_COMPRESSED = 4; + + /** + * Native filesystem find result flags. + */ + public static final int FILESYSTEM_FIND_RECURSIVE = (1 << 0); + public static final int FILESYSTEM_FIND_RELATIVE_PATHS = (1 << 1); + public static final int FILESYSTEM_FIND_HIDDEN_FILES = (1 << 2); + public static final int FILESYSTEM_FIND_FOLDERS = (1 << 3); + public static final int FILESYSTEM_FIND_FILES = (1 << 4); + public static final int FILESYSTEM_FIND_KEEP_ARRAY = (1 << 5); + + /** + * Projection used when searching for files. + */ + private static final String[] findProjection = new String[]{ + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_MIME_TYPE, + DocumentsContract.Document.COLUMN_SIZE, + DocumentsContract.Document.COLUMN_LAST_MODIFIED + }; + + private final Context context; + private final ContentResolver contentResolver; + + /** + * File helper class - used to bridge native code to Java storage access framework APIs. + * @param context Context in which to perform file actions as. + */ + public FileHelper(Context context) { + this.context = context; + this.contentResolver = context.getContentResolver(); + } + + /** + * Retrieves a file descriptor for a content URI string. Called by native code. + * @param uriString string of the URI to open + * @param mode Java open mode + * @return file descriptor for URI, or -1 + */ + public int openURIAsFileDescriptor(String uriString, String mode) { + try { + final Uri uri = Uri.parse(uriString); + final ParcelFileDescriptor fd = contentResolver.openFileDescriptor(uri, mode); + if (fd == null) + return -1; + return fd.detachFd(); + } catch (Exception e) { + return -1; + } + } + + /** + * Recursively iterates documents in the specified tree, searching for files. + * @param treeUri Root tree in which to search for documents. + * @param documentId Document ID representing the directory to start searching. + * @param flags Native search flags. + * @param results Cumulative result array. + */ + private void doFindFiles(Uri treeUri, String documentId, int flags, ArrayList results) { + try { + final Uri queryUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId); + final Cursor cursor = contentResolver.query(queryUri, findProjection, null, null, null); + final int count = cursor.getCount(); + + while (cursor.moveToNext()) { + try { + final String mimeType = cursor.getString(2); + final String childDocumentId = cursor.getString(0); + final Uri uri = DocumentsContract.buildDocumentUriUsingTree(treeUri, childDocumentId); + final long size = cursor.getLong(3); + final long lastModified = cursor.getLong(4); + + if (DocumentsContract.Document.MIME_TYPE_DIR.equals(mimeType)) { + if ((flags & FILESYSTEM_FIND_FOLDERS) != 0) { + results.add(new FindResult(childDocumentId, uri.toString(), size, lastModified, FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY)); + } + + if ((flags & FILESYSTEM_FIND_RECURSIVE) != 0) + doFindFiles(treeUri, childDocumentId, flags, results); + } else { + if ((flags & FILESYSTEM_FIND_FILES) != 0) { + results.add(new FindResult(childDocumentId, uri.toString(), size, lastModified, 0)); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + cursor.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Recursively iterates documents in the specified URI, searching for files. + * @param uriString URI containing directory to search. + * @param flags Native filter flags. + * @return Array of find results. + */ + public FindResult[] findFiles(String uriString, int flags) { + try { + final Uri fullUri = Uri.parse(uriString); + final String documentId = DocumentsContract.getTreeDocumentId(fullUri); + final ArrayList results = new ArrayList<>(); + doFindFiles(fullUri, documentId, flags, results); + if (results.isEmpty()) + return null; + + final FindResult[] resultsArray = new FindResult[results.size()]; + results.toArray(resultsArray); + return resultsArray; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * Java class containing the data for a file in a find operation. + */ + public static class FindResult { + public String name; + public String relativeName; + public long size; + public long modifiedTime; + public int flags; + + public FindResult(String relativeName, String name, long size, long modifiedTime, int flags) { + this.relativeName = relativeName; + this.name = name; + this.size = size; + this.modifiedTime = modifiedTime; + this.flags = flags; + } + } +} diff --git a/src/common/file_system.cpp b/src/common/file_system.cpp index 5f8a1a3c3..3a0072257 100644 --- a/src/common/file_system.cpp +++ b/src/common/file_system.cpp @@ -30,10 +30,257 @@ #include #endif +#ifdef __ANDROID__ +#include +#endif + Log_SetChannel(FileSystem); namespace FileSystem { +#ifdef __ANDROID__ + +static JavaVM* s_android_jvm; +static jobject s_android_FileHelper_object; +static jclass s_android_FileHelper_class; +static jmethodID s_android_FileHelper_openURIAsFileDescriptor; +static jmethodID s_android_FileHelper_FindFiles; +static jclass s_android_FileHelper_FindResult_class; +static jfieldID s_android_FileHelper_FindResult_name; +static jfieldID s_android_FileHelper_FindResult_relativeName; +static jfieldID s_android_FileHelper_FindResult_size; +static jfieldID s_android_FileHelper_FindResult_modifiedTime; +static jfieldID s_android_FileHelper_FindResult_flags; + +// helper for retrieving the current per-thread jni environment +static JNIEnv* GetJNIEnv() +{ + JNIEnv* env; + if (s_android_jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) + return nullptr; + else + return env; +} + +static bool IsUriPath(const std::string_view& path) +{ + return StringUtil::StartsWith(path, "content:/") || StringUtil::StartsWith(path, "file:/"); +} + +static bool UriHelpersAreAvailable() +{ + return (s_android_FileHelper_object != nullptr); +} + +void SetAndroidFileHelper(void* jvm, void* env, void* object) +{ + Assert(!jvm || !s_android_jvm || s_android_jvm == jvm); + + if (s_android_FileHelper_object) + { + JNIEnv* jenv = GetJNIEnv(); + jenv->DeleteGlobalRef(s_android_FileHelper_FindResult_class); + s_android_FileHelper_FindResult_name = {}; + s_android_FileHelper_FindResult_relativeName = {}; + s_android_FileHelper_FindResult_size = {}; + s_android_FileHelper_FindResult_modifiedTime = {}; + s_android_FileHelper_FindResult_flags = {}; + s_android_FileHelper_FindResult_class = {}; + + jenv->DeleteGlobalRef(s_android_FileHelper_object); + jenv->DeleteGlobalRef(s_android_FileHelper_class); + s_android_FileHelper_openURIAsFileDescriptor = {}; + s_android_FileHelper_FindFiles = {}; + s_android_FileHelper_object = {}; + s_android_FileHelper_class = {}; + s_android_jvm = {}; + } + + if (!object) + return; + + JNIEnv* jenv = static_cast(env); + s_android_jvm = static_cast(jvm); + s_android_FileHelper_object = jenv->NewGlobalRef(static_cast(object)); + Assert(s_android_FileHelper_object); + + jclass fh_class = jenv->GetObjectClass(static_cast(object)); + s_android_FileHelper_class = static_cast(jenv->NewGlobalRef(fh_class)); + Assert(s_android_FileHelper_class); + jenv->DeleteLocalRef(fh_class); + + s_android_FileHelper_openURIAsFileDescriptor = + jenv->GetMethodID(s_android_FileHelper_class, "openURIAsFileDescriptor", "(Ljava/lang/String;Ljava/lang/String;)I"); + s_android_FileHelper_FindFiles = + jenv->GetMethodID(s_android_FileHelper_class, "findFiles", + "(Ljava/lang/String;I)[Lcom/github/stenzek/duckstation/FileHelper$FindResult;"); + Assert(s_android_FileHelper_openURIAsFileDescriptor && s_android_FileHelper_FindFiles); + + jclass fr_class = jenv->FindClass("com/github/stenzek/duckstation/FileHelper$FindResult"); + Assert(fr_class); + s_android_FileHelper_FindResult_class = static_cast(jenv->NewGlobalRef(fr_class)); + Assert(s_android_FileHelper_FindResult_class); + jenv->DeleteLocalRef(fr_class); + + s_android_FileHelper_FindResult_relativeName = + jenv->GetFieldID(s_android_FileHelper_FindResult_class, "relativeName", "Ljava/lang/String;"); + s_android_FileHelper_FindResult_name = + jenv->GetFieldID(s_android_FileHelper_FindResult_class, "name", "Ljava/lang/String;"); + s_android_FileHelper_FindResult_size = jenv->GetFieldID(s_android_FileHelper_FindResult_class, "size", "J"); + s_android_FileHelper_FindResult_modifiedTime = + jenv->GetFieldID(s_android_FileHelper_FindResult_class, "modifiedTime", "J"); + s_android_FileHelper_FindResult_flags = jenv->GetFieldID(s_android_FileHelper_FindResult_class, "flags", "I"); + Assert(s_android_FileHelper_FindResult_relativeName && s_android_FileHelper_FindResult_name && + s_android_FileHelper_FindResult_size && s_android_FileHelper_FindResult_modifiedTime && + s_android_FileHelper_FindResult_flags); +} + +static std::FILE* OpenUriFile(const char* path, const char* mode) +{ + // translate C modes to Java modes + TinyString mode_trimmed; + std::size_t mode_len = std::strlen(mode); + for (size_t i = 0; i < mode_len; i++) + { + if (mode[i] == 'r' || mode[i] == 'w' || mode[i] == '+') + mode_trimmed.AppendCharacter(mode[i]); + } + + // TODO: Handle append mode by seeking to end. + const char* java_mode = nullptr; + if (mode_trimmed == "r") + java_mode = "r"; + else if (mode_trimmed == "r+") + java_mode = "rw"; + else if (mode_trimmed == "w") + java_mode = "w"; + else if (mode_trimmed == "w+") + java_mode = "rwt"; + + if (!java_mode) + { + Log_ErrorPrintf("Could not translate file mode '%s' ('%s')", mode, mode_trimmed.GetCharArray()); + return nullptr; + } + + // Hand off to Java... + JNIEnv* env = GetJNIEnv(); + jstring path_jstr = env->NewStringUTF(path); + jstring mode_jstr = env->NewStringUTF(java_mode); + int fd = + env->CallIntMethod(s_android_FileHelper_object, s_android_FileHelper_openURIAsFileDescriptor, path_jstr, mode_jstr); + env->DeleteLocalRef(mode_jstr); + env->DeleteLocalRef(path_jstr); + + // Just in case... + if (env->ExceptionCheck()) + { + env->ExceptionClear(); + return nullptr; + } + + if (fd < 0) + return nullptr; + + // Convert to a C file object. + std::FILE* fp = fdopen(fd, mode); + if (!fp) + { + Log_ErrorPrintf("Failed to convert FD %d to C FILE for '%s'.", fd, path); + close(fd); + return nullptr; + } + + return fp; +} + +static bool FindUriFiles(const char* path, const char* pattern, u32 flags, FindResultsArray* pVector) +{ + if (!s_android_FileHelper_object) + return false; + + JNIEnv* env = GetJNIEnv(); + + jstring path_jstr = env->NewStringUTF(path); + jobjectArray arr = static_cast(env->CallObjectMethod( + s_android_FileHelper_object, s_android_FileHelper_FindFiles, path_jstr, static_cast(flags))); + env->DeleteLocalRef(path_jstr); + if (!arr) + return false; + + // small speed optimization for '*' case + bool hasWildCards = false; + bool wildCardMatchAll = false; + u32 nFiles = 0; + if (std::strpbrk(pattern, "*?") != nullptr) + { + hasWildCards = true; + wildCardMatchAll = !(std::strcmp(pattern, "*")); + } + + jsize count = env->GetArrayLength(arr); + for (jsize i = 0; i < count; i++) + { + jobject result = env->GetObjectArrayElement(arr, i); + if (!result) + continue; + + jstring result_name_obj = static_cast(env->GetObjectField(result, s_android_FileHelper_FindResult_name)); + jstring result_relative_name_obj = + static_cast(env->GetObjectField(result, s_android_FileHelper_FindResult_relativeName)); + const u64 result_size = static_cast(env->GetLongField(result, s_android_FileHelper_FindResult_size)); + const u64 result_modified_time = + static_cast(env->GetLongField(result, s_android_FileHelper_FindResult_modifiedTime)); + const u32 result_flags = static_cast(env->GetIntField(result, s_android_FileHelper_FindResult_flags)); + + if (result_name_obj && result_relative_name_obj) + { + const char* result_name = env->GetStringUTFChars(result_name_obj, nullptr); + const char* result_relative_name = env->GetStringUTFChars(result_relative_name_obj, nullptr); + if (result_relative_name) + { + // match the filename + bool matched; + if (hasWildCards) + { + matched = wildCardMatchAll || StringUtil::WildcardMatch(result_relative_name, pattern); + } + else + { + matched = std::strcmp(result_relative_name, pattern) == 0; + } + + if (matched) + { + FILESYSTEM_FIND_DATA ffd; + ffd.FileName = ((flags & FILESYSTEM_FIND_RELATIVE_PATHS) != 0) ? result_relative_name : result_name; + ffd.Attributes = result_flags; + ffd.ModificationTime.SetUnixTimestamp(result_modified_time); + ffd.Size = result_size; + pVector->push_back(std::move(ffd)); + } + } + + if (result_name) + env->ReleaseStringUTFChars(result_name_obj, result_name); + if (result_relative_name) + env->ReleaseStringUTFChars(result_relative_name_obj, result_relative_name); + } + + if (result_name_obj) + env->DeleteLocalRef(result_name_obj); + if (result_relative_name_obj) + env->DeleteLocalRef(result_relative_name_obj); + + env->DeleteLocalRef(result); + } + + env->DeleteLocalRef(arr); + return true; +} + +#endif // __ANDROID__ + ChangeNotifier::ChangeNotifier(const String& directoryPath, bool recursiveWatch) : m_directoryPath(directoryPath), m_recursiveWatch(recursiveWatch) { @@ -298,6 +545,28 @@ static std::string_view::size_type GetLastSeperatorPosition(const std::string_vi if (last_separator == std::string_view::npos || other_last_separator > last_separator) last_separator = other_last_separator; } + +#elif defined(__ANDROID__) + if (IsUriPath(filename)) + { + // scoped storage rubbish + std::string_view::size_type other_last_separator = filename.rfind("%2F"); + if (other_last_separator != std::string_view::npos) + { + if (include_separator) + other_last_separator += 3; + if (last_separator == std::string_view::npos || other_last_separator > last_separator) + last_separator = other_last_separator; + } + std::string_view::size_type lower_other_last_separator = filename.rfind("%2f"); + if (lower_other_last_separator != std::string_view::npos) + { + if (include_separator) + lower_other_last_separator += 3; + if (last_separator == std::string_view::npos || lower_other_last_separator > last_separator) + last_separator = lower_other_last_separator; + } + } #endif return last_separator; @@ -374,6 +643,8 @@ std::unique_ptr OpenFile(const char* FileName, u32 Flags) if (FileName[0] == '\0') return nullptr; + // TODO: Handle Android content URIs here. + // forward to local file wrapper return ByteStream_OpenFileStream(FileName, Flags); } @@ -414,6 +685,11 @@ std::FILE* OpenCFile(const char* filename, const char* mode) return fp; #else +#ifdef __ANDROID__ + if (IsUriPath(filename) && UriHelpersAreAvailable()) + return OpenUriFile(filename, mode); +#endif + return std::fopen(filename, mode); #endif } @@ -1450,6 +1726,11 @@ bool FindFiles(const char* Path, const char* Pattern, u32 Flags, FindResultsArra if (!(Flags & FILESYSTEM_FIND_KEEP_ARRAY)) pResults->clear(); +#ifdef __ANDROID__ + if (IsUriPath(Path) && UriHelpersAreAvailable()) + return FindUriFiles(Path, Pattern, Flags, pResults); +#endif + // enter the recursive function return (RecursiveFindFiles(Path, nullptr, nullptr, Pattern, Flags, pResults) > 0); } diff --git a/src/common/file_system.h b/src/common/file_system.h index 60a4e2d92..b7217dbf8 100644 --- a/src/common/file_system.h +++ b/src/common/file_system.h @@ -61,6 +61,12 @@ namespace FileSystem { using FindResultsArray = std::vector; +#ifdef __ANDROID__ +/// Sets the instance for the FileHelpers Java class, used for storage access framework +/// file access on Android. +void SetAndroidFileHelper(void* jvm, void* env, void* object); +#endif + class ChangeNotifier { public: