Android: Add specialized content provider implementation of DoFileSearch

This commit is contained in:
JosJuice 2020-12-28 13:25:24 +01:00
parent 01b964b01a
commit d78277c063
8 changed files with 177 additions and 42 deletions

View File

@ -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,

View File

@ -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<String> 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<String> resultOut)
@NonNull @Keep
public static String[] doFileSearch(@NonNull String directory, @NonNull String[] extensions,
boolean recursive)
{
ArrayList<String> result = new ArrayList<>();
try
{
Uri uri = unmangle(directory);
String documentId = DocumentsContract.getDocumentId(treeToDocument(uri));
boolean acceptAll = extensions.length == 0;
Predicate<String> 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<String> resultOut,
boolean acceptAll, @NonNull Predicate<String> 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)));
}
}
}

View File

@ -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<String> set)

View File

@ -44,6 +44,14 @@ std::vector<std::string> JStringArrayToVector(JNIEnv* env, jobjectArray array)
return result;
}
jobjectArray JStringArrayFromVector(JNIEnv* env, std::vector<std::string> 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<std::string> GetAndroidContentChildNames(const std::string& uri)
return JStringArrayToVector(env, reinterpret_cast<jobjectArray>(children));
}
std::vector<std::string> DoFileSearchAndroidContent(const std::string& directory,
const std::vector<std::string>& 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<jobjectArray>(result));
}
int GetNetworkIpAddress()
{
JNIEnv* env = IDCache::GetEnvForThread();

View File

@ -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<std::string> GetAndroidContentChildNames(const std::string& uri);
std::vector<std::string> DoFileSearchAndroidContent(const std::string& directory,
const std::vector<std::string>& extensions,
bool recursive);
int GetNetworkIpAddress();
int GetNetworkPrefixLength();
int GetNetworkGateway();

View File

@ -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<void**>(&env), JNI_VERSION) != JNI_OK)
return JNI_ERR;
const jclass string_class = env->FindClass("java/lang/String");
s_string_class = reinterpret_cast<jclass>(env->NewGlobalRef(string_class));
const jclass native_library_class = env->FindClass("org/dolphinemu/dolphinemu/NativeLibrary");
s_native_library_class = reinterpret_cast<jclass>(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");

View File

@ -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();

View File

@ -4,6 +4,7 @@
#include <algorithm>
#include <functional>
#include <iterator>
#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 <cstring>
#include "Common/CommonFuncs.h"
#include "Common/FileUtil.h"
@ -24,36 +29,30 @@ namespace Common
{
#ifndef HAS_STD_FILESYSTEM
static std::vector<std::string>
FileSearchWithTest(const std::vector<std::string>& directories, bool recursive,
std::function<bool(const File::FSTEntry&)> callback)
static void FileSearchWithTest(const std::string& directory, bool recursive,
std::vector<std::string>* result_out,
std::function<bool(const File::FSTEntry&)> callback)
{
std::vector<std::string> result;
for (const std::string& directory : directories)
{
File::FSTEntry top = File::ScanDirectoryTree(directory, recursive);
File::FSTEntry top = File::ScanDirectoryTree(directory, recursive);
std::function<void(File::FSTEntry&)> 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<void(File::FSTEntry&)> 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<std::string> DoFileSearch(const std::vector<std::string>& directories,
const std::vector<std::string>& exts, bool recursive)
{
std::vector<std::string> 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<std::string> DoFileSearch(const std::vector<std::string>& 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<std::string> 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