From d78277c063d43e3234fe80a38d272500f3dfce3a Mon Sep 17 00:00:00 2001 From: JosJuice Date: Mon, 28 Dec 2020 13:25:24 +0100 Subject: [PATCH] Android: Add specialized content provider implementation of DoFileSearch --- .../dolphinemu/ui/main/MainPresenter.java | 4 +- .../dolphinemu/utils/ContentHandler.java | 90 ++++++++++++++++--- .../dolphinemu/utils/FileBrowserHelper.java | 10 ++- .../jni/AndroidCommon/AndroidCommon.cpp | 19 ++++ .../Android/jni/AndroidCommon/AndroidCommon.h | 4 + Source/Android/jni/AndroidCommon/IDCache.cpp | 19 ++++ Source/Android/jni/AndroidCommon/IDCache.h | 3 + Source/Core/Common/FileSearch.cpp | 70 ++++++++++----- 8 files changed, 177 insertions(+), 42 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java index ff34ea2f1b..1456bb6654 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java @@ -130,8 +130,8 @@ public final class MainPresenter boolean recursive = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.getBooleanGlobal(); String[] childNames = ContentHandler.getChildNames(uri, recursive); - if (Arrays.stream(childNames).noneMatch((name) -> - FileBrowserHelper.GAME_EXTENSIONS.contains(FileBrowserHelper.getExtension(name)))) + if (Arrays.stream(childNames).noneMatch((name) -> FileBrowserHelper.GAME_EXTENSIONS.contains( + FileBrowserHelper.getExtension(name, false)))) { AlertDialog.Builder builder = new AlertDialog.Builder(mContext, R.style.DolphinDialogBase); builder.setMessage(mContext.getString(R.string.wrong_file_extension_in_directory, diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java index ebedef4b60..52600fe488 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java @@ -15,7 +15,9 @@ import org.dolphinemu.dolphinemu.DolphinApplication; import java.io.FileNotFoundException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.function.Predicate; /* We use a lot of "catch (Exception e)" in this class. This is for two reasons: @@ -184,34 +186,94 @@ public class ContentHandler public static String[] getChildNames(@NonNull Uri uri, boolean recursive) { ArrayList result = new ArrayList<>(); - getChildNames(uri, DocumentsContract.getDocumentId(treeToDocument(uri)), recursive, result); + + ForEachChildCallback callback = new ForEachChildCallback() + { + @Override + public void run(String displayName, String documentId, boolean isDirectory) + { + if (recursive && isDirectory) + { + forEachChild(uri, documentId, this); + } + else + { + result.add(displayName); + } + } + }; + + forEachChild(uri, DocumentsContract.getDocumentId(treeToDocument(uri)), callback); + return result.toArray(new String[0]); } - private static void getChildNames(@NonNull Uri uri, @NonNull String documentId, boolean recursive, - List resultOut) + @NonNull @Keep + public static String[] doFileSearch(@NonNull String directory, @NonNull String[] extensions, + boolean recursive) + { + ArrayList result = new ArrayList<>(); + + try + { + Uri uri = unmangle(directory); + String documentId = DocumentsContract.getDocumentId(treeToDocument(uri)); + boolean acceptAll = extensions.length == 0; + Predicate extensionCheck = (displayName) -> + { + String extension = FileBrowserHelper.getExtension(displayName, true); + return extension != null && Arrays.stream(extensions).anyMatch(extension::equalsIgnoreCase); + }; + doFileSearch(uri, directory, documentId, recursive, result, acceptAll, extensionCheck); + } + catch (Exception ignored) + { + } + + return result.toArray(new String[0]); + } + + private static void doFileSearch(@NonNull Uri baseUri, @NonNull String path, + @NonNull String documentId, boolean recursive, @NonNull List resultOut, + boolean acceptAll, @NonNull Predicate extensionCheck) + { + forEachChild(baseUri, documentId, (displayName, childDocumentId, isDirectory) -> + { + String childPath = path + '/' + displayName; + if (acceptAll || (!isDirectory && extensionCheck.test(displayName))) + { + resultOut.add(childPath); + } + if (recursive && isDirectory) + { + doFileSearch(baseUri, childPath, childDocumentId, recursive, resultOut, acceptAll, + extensionCheck); + } + }); + } + + private interface ForEachChildCallback + { + void run(String displayName, String documentId, boolean isDirectory); + } + + private static void forEachChild(@NonNull Uri uri, @NonNull String documentId, + @NonNull ForEachChildCallback callback) { try { Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, documentId); - final String[] projection = recursive ? new String[]{Document.COLUMN_DISPLAY_NAME, - Document.COLUMN_MIME_TYPE, Document.COLUMN_DOCUMENT_ID} : - new String[]{Document.COLUMN_DISPLAY_NAME}; + final String[] projection = new String[]{Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_MIME_TYPE, Document.COLUMN_DOCUMENT_ID}; try (Cursor cursor = getContentResolver().query(childrenUri, projection, null, null, null)) { if (cursor != null) { while (cursor.moveToNext()) { - if (recursive && Document.MIME_TYPE_DIR.equals(cursor.getString(1))) - { - getChildNames(uri, cursor.getString(2), recursive, resultOut); - } - else - { - resultOut.add(cursor.getString(0)); - } + callback.run(cursor.getString(0), cursor.getString(2), + Document.MIME_TYPE_DIR.equals(cursor.getString(1))); } } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java index 82f16519fa..f68bc46857 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java @@ -88,10 +88,10 @@ public final class FileBrowserHelper String path = uri.getLastPathSegment(); if (path != null) - extension = getExtension(new File(path).getName()); + extension = getExtension(new File(path).getName(), false); if (extension == null) - extension = getExtension(ContentHandler.getDisplayName(uri)); + extension = getExtension(ContentHandler.getDisplayName(uri), false); if (extension != null && validExtensions.contains(extension)) { @@ -122,13 +122,15 @@ public final class FileBrowserHelper } @Nullable - public static String getExtension(@Nullable String fileName) + public static String getExtension(@Nullable String fileName, boolean includeDot) { if (fileName == null) return null; int dotIndex = fileName.lastIndexOf("."); - return dotIndex != -1 ? fileName.substring(dotIndex + 1) : null; + if (dotIndex == -1) + return null; + return fileName.substring(dotIndex + (includeDot ? 0 : 1)); } public static String setToSortedDelimitedString(Set set) diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp index 9e57d2764a..b5a5a7faaa 100644 --- a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp +++ b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp @@ -44,6 +44,14 @@ std::vector JStringArrayToVector(JNIEnv* env, jobjectArray array) return result; } +jobjectArray JStringArrayFromVector(JNIEnv* env, std::vector vector) +{ + jobjectArray result = env->NewObjectArray(vector.size(), IDCache::GetStringClass(), nullptr); + for (jsize i = 0; i < vector.size(); ++i) + env->SetObjectArrayElement(result, i, ToJString(env, vector[i])); + return result; +} + bool IsPathAndroidContent(const std::string& uri) { return StringBeginsWith(uri, "content://"); @@ -130,6 +138,17 @@ std::vector GetAndroidContentChildNames(const std::string& uri) return JStringArrayToVector(env, reinterpret_cast(children)); } +std::vector DoFileSearchAndroidContent(const std::string& directory, + const std::vector& extensions, + bool recursive) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + jobject result = env->CallStaticObjectMethod( + IDCache::GetContentHandlerClass(), IDCache::GetContentHandlerDoFileSearch(), + ToJString(env, directory), JStringArrayFromVector(env, extensions), recursive); + return JStringArrayToVector(env, reinterpret_cast(result)); +} + int GetNetworkIpAddress() { JNIEnv* env = IDCache::GetEnvForThread(); diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.h b/Source/Android/jni/AndroidCommon/AndroidCommon.h index 1f75864704..7d4f5d0fbc 100644 --- a/Source/Android/jni/AndroidCommon/AndroidCommon.h +++ b/Source/Android/jni/AndroidCommon/AndroidCommon.h @@ -38,6 +38,10 @@ std::string GetAndroidContentDisplayName(const std::string& uri); // Returns the display names of all children of a directory, non-recursively. std::vector GetAndroidContentChildNames(const std::string& uri); +std::vector DoFileSearchAndroidContent(const std::string& directory, + const std::vector& extensions, + bool recursive); + int GetNetworkIpAddress(); int GetNetworkPrefixLength(); int GetNetworkGateway(); diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index a4a7ef4ebb..4c6c4b2ce7 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -10,6 +10,8 @@ static constexpr jint JNI_VERSION = JNI_VERSION_1_6; static JavaVM* s_java_vm; +static jclass s_string_class; + static jclass s_native_library_class; static jmethodID s_display_alert_msg; static jmethodID s_do_rumble; @@ -47,6 +49,7 @@ static jmethodID s_content_handler_delete; static jmethodID s_content_handler_get_size_and_is_directory; static jmethodID s_content_handler_get_display_name; static jmethodID s_content_handler_get_child_names; +static jmethodID s_content_handler_do_file_search; static jclass s_network_helper_class; static jmethodID s_network_helper_get_network_ip_address; @@ -78,6 +81,11 @@ JNIEnv* GetEnvForThread() return owned.env; } +jclass GetStringClass() +{ + return s_string_class; +} + jclass GetNativeLibraryClass() { return s_native_library_class; @@ -228,6 +236,11 @@ jmethodID GetContentHandlerGetChildNames() return s_content_handler_get_child_names; } +jmethodID GetContentHandlerDoFileSearch() +{ + return s_content_handler_do_file_search; +} + jclass GetNetworkHelperClass() { return s_network_helper_class; @@ -262,6 +275,9 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION) != JNI_OK) return JNI_ERR; + const jclass string_class = env->FindClass("java/lang/String"); + s_string_class = reinterpret_cast(env->NewGlobalRef(string_class)); + const jclass native_library_class = env->FindClass("org/dolphinemu/dolphinemu/NativeLibrary"); s_native_library_class = reinterpret_cast(env->NewGlobalRef(native_library_class)); s_display_alert_msg = env->GetStaticMethodID(s_native_library_class, "displayAlertMsg", @@ -331,6 +347,9 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) s_content_handler_class, "getDisplayName", "(Ljava/lang/String;)Ljava/lang/String;"); s_content_handler_get_child_names = env->GetStaticMethodID( s_content_handler_class, "getChildNames", "(Ljava/lang/String;Z)[Ljava/lang/String;"); + s_content_handler_do_file_search = + env->GetStaticMethodID(s_content_handler_class, "doFileSearch", + "(Ljava/lang/String;[Ljava/lang/String;Z)[Ljava/lang/String;"); const jclass network_helper_class = env->FindClass("org/dolphinemu/dolphinemu/utils/NetworkHelper"); diff --git a/Source/Android/jni/AndroidCommon/IDCache.h b/Source/Android/jni/AndroidCommon/IDCache.h index 3f568e3f73..b633267f55 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -10,6 +10,8 @@ namespace IDCache { JNIEnv* GetEnvForThread(); +jclass GetStringClass(); + jclass GetNativeLibraryClass(); jmethodID GetDisplayAlertMsg(); jmethodID GetDoRumble(); @@ -47,6 +49,7 @@ jmethodID GetContentHandlerDelete(); jmethodID GetContentHandlerGetSizeAndIsDirectory(); jmethodID GetContentHandlerGetDisplayName(); jmethodID GetContentHandlerGetChildNames(); +jmethodID GetContentHandlerDoFileSearch(); jclass GetNetworkHelperClass(); jmethodID GetNetworkHelperGetNetworkIpAddress(); diff --git a/Source/Core/Common/FileSearch.cpp b/Source/Core/Common/FileSearch.cpp index bbcdc12026..b06ef9b3a1 100644 --- a/Source/Core/Common/FileSearch.cpp +++ b/Source/Core/Common/FileSearch.cpp @@ -4,6 +4,7 @@ #include #include +#include #include "Common/CommonPaths.h" #include "Common/FileSearch.h" @@ -15,6 +16,10 @@ namespace fs = std::filesystem; #define HAS_STD_FILESYSTEM #else +#ifdef ANDROID +#include "jni/AndroidCommon/AndroidCommon.h" +#endif + #include #include "Common/CommonFuncs.h" #include "Common/FileUtil.h" @@ -24,36 +29,30 @@ namespace Common { #ifndef HAS_STD_FILESYSTEM -static std::vector -FileSearchWithTest(const std::vector& directories, bool recursive, - std::function callback) +static void FileSearchWithTest(const std::string& directory, bool recursive, + std::vector* result_out, + std::function callback) { - std::vector result; - for (const std::string& directory : directories) - { - File::FSTEntry top = File::ScanDirectoryTree(directory, recursive); + File::FSTEntry top = File::ScanDirectoryTree(directory, recursive); - std::function DoEntry; - DoEntry = [&](File::FSTEntry& entry) { - if (callback(entry)) - result.push_back(entry.physicalName); - for (auto& child : entry.children) - DoEntry(child); - }; - for (auto& child : top.children) + const std::function DoEntry = [&](File::FSTEntry& entry) { + if (callback(entry)) + result_out->push_back(entry.physicalName); + for (auto& child : entry.children) DoEntry(child); - } - // remove duplicates - std::sort(result.begin(), result.end()); - result.erase(std::unique(result.begin(), result.end()), result.end()); - return result; + }; + + for (auto& child : top.children) + DoEntry(child); } std::vector DoFileSearch(const std::vector& directories, const std::vector& exts, bool recursive) { + std::vector result; + bool accept_all = exts.empty(); - return FileSearchWithTest(directories, recursive, [&](const File::FSTEntry& entry) { + const auto callback = [&exts, accept_all](const File::FSTEntry& entry) { if (accept_all) return true; if (entry.isDirectory) @@ -63,7 +62,34 @@ std::vector DoFileSearch(const std::vector& directorie return name.length() >= ext.length() && strcasecmp(name.c_str() + name.length() - ext.length(), ext.c_str()) == 0; }); - }); + }; + + for (const std::string& directory : directories) + { +#ifdef ANDROID + // While File::ScanDirectoryTree (which is called in FileSearchWithTest) does handle Android + // content correctly, having a specialized implementation of DoFileSearch for Android content + // provides a much needed performance boost. Also, this specialized implementation will be + // required if we in the future replace the use of File::ScanDirectoryTree with std::filesystem. + if (IsPathAndroidContent(directory)) + { + const std::vector partial_result = + DoFileSearchAndroidContent(directory, exts, recursive); + + result.insert(result.end(), std::make_move_iterator(partial_result.begin()), + std::make_move_iterator(partial_result.end())); + } + else +#endif + { + FileSearchWithTest(directory, recursive, &result, callback); + } + } + + // remove duplicates + std::sort(result.begin(), result.end()); + result.erase(std::unique(result.begin(), result.end()), result.end()); + return result; } #else