From 64afe97491c1b713c18a79e4cd18a0cca732ade1 Mon Sep 17 00:00:00 2001 From: Ryan Meredith Date: Thu, 29 Oct 2020 15:06:55 -0400 Subject: [PATCH 01/11] Android: Convert ISOPaths to INI settings --- .../features/settings/utils/SettingsFile.java | 3 + .../dolphinemu/model/GameFileCache.java | 102 ++++++++++-------- .../services/GameFileCacheService.java | 9 +- .../dolphinemu/ui/main/MainActivity.java | 4 +- .../dolphinemu/ui/main/MainPresenter.java | 4 +- .../dolphinemu/ui/main/TvMainActivity.java | 6 +- 6 files changed, 71 insertions(+), 57 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/utils/SettingsFile.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/utils/SettingsFile.java index e65805fd95..82ea3a84f1 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/utils/SettingsFile.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/utils/SettingsFile.java @@ -16,6 +16,9 @@ import java.io.File; */ public final class SettingsFile { + public static final String KEY_ISO_PATH_BASE = "ISOPath"; + public static final String KEY_ISO_PATHS = "ISOPaths"; + public static final String KEY_GCPAD_TYPE = "SIDevice"; public static final String KEY_GCPAD_PLAYER_1 = "SIDevice0"; public static final String KEY_GCPAD_G_TYPE = "PadType"; diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFileCache.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFileCache.java index 08a999bd32..2fdc0cb179 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFileCache.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFileCache.java @@ -1,22 +1,18 @@ package org.dolphinemu.dolphinemu.model; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; - import androidx.annotation.Keep; +import org.dolphinemu.dolphinemu.NativeLibrary; import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; +import org.dolphinemu.dolphinemu.features.settings.model.Settings; +import org.dolphinemu.dolphinemu.features.settings.utils.SettingsFile; +import org.dolphinemu.dolphinemu.utils.IniFile; import java.io.File; -import java.util.HashSet; -import java.util.Set; +import java.util.LinkedHashSet; public class GameFileCache { - private static final String GAME_FOLDER_PATHS_PREFERENCE = "gameFolderPaths"; - private static final Set EMPTY_SET = new HashSet<>(); - @Keep private long mPointer; @@ -30,50 +26,72 @@ public class GameFileCache @Override public native void finalize(); - public static void addGameFolder(String path, Context context) + public static void addGameFolder(String path) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - Set folderPaths = preferences.getStringSet(GAME_FOLDER_PATHS_PREFERENCE, EMPTY_SET); + File dolphinFile = SettingsFile.getSettingsFile(Settings.FILE_DOLPHIN); + IniFile dolphinIni = new IniFile(dolphinFile); + LinkedHashSet pathSet = getPathSet(false); + int totalISOPaths = + dolphinIni.getInt(Settings.SECTION_INI_GENERAL, SettingsFile.KEY_ISO_PATHS, 0); - if (folderPaths == null) + if (!pathSet.contains(path)) { - return; + dolphinIni.setInt(Settings.SECTION_INI_GENERAL, SettingsFile.KEY_ISO_PATHS, + totalISOPaths + 1); + dolphinIni.setString(Settings.SECTION_INI_GENERAL, SettingsFile.KEY_ISO_PATH_BASE + + totalISOPaths, path); + dolphinIni.save(dolphinFile); + NativeLibrary.ReloadConfig(); } - - Set newFolderPaths = new HashSet<>(folderPaths); - newFolderPaths.add(path); - SharedPreferences.Editor editor = preferences.edit(); - editor.putStringSet(GAME_FOLDER_PATHS_PREFERENCE, newFolderPaths); - editor.apply(); } - private void removeNonExistentGameFolders(Context context) + private static LinkedHashSet getPathSet(boolean removeNonExistentFolders) { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - Set folderPaths = preferences.getStringSet(GAME_FOLDER_PATHS_PREFERENCE, EMPTY_SET); + File dolphinFile = SettingsFile.getSettingsFile(Settings.FILE_DOLPHIN); + IniFile dolphinIni = new IniFile(dolphinFile); + LinkedHashSet pathSet = new LinkedHashSet<>(); + int totalISOPaths = + dolphinIni.getInt(Settings.SECTION_INI_GENERAL, SettingsFile.KEY_ISO_PATHS, 0); - if (folderPaths == null) + for (int i = 0; i < totalISOPaths; i++) { - return; - } + String path = dolphinIni.getString(Settings.SECTION_INI_GENERAL, + SettingsFile.KEY_ISO_PATH_BASE + i, ""); - Set newFolderPaths = new HashSet<>(); - for (String folderPath : folderPaths) - { - File folder = new File(folderPath); + File folder = new File(path); if (folder.exists()) { - newFolderPaths.add(folderPath); + pathSet.add(path); } } - if (folderPaths.size() != newFolderPaths.size()) + if (removeNonExistentFolders && totalISOPaths > pathSet.size()) { - // One or more folders are being deleted - SharedPreferences.Editor editor = preferences.edit(); - editor.putStringSet(GAME_FOLDER_PATHS_PREFERENCE, newFolderPaths); - editor.apply(); + int setIndex = 0; + + dolphinIni.setInt(Settings.SECTION_INI_GENERAL, SettingsFile.KEY_ISO_PATHS, + pathSet.size()); + + // One or more folders have been removed. + for (String entry : pathSet) + { + dolphinIni.setString(Settings.SECTION_INI_GENERAL, SettingsFile.KEY_ISO_PATH_BASE + + setIndex, entry); + + setIndex++; + } + + // Delete known unnecessary keys. Ignore i values beyond totalISOPaths. + for (int i = setIndex; i < totalISOPaths; i++) + { + dolphinIni.deleteKey(Settings.SECTION_INI_GENERAL, SettingsFile.KEY_ISO_PATH_BASE + i); + } + + dolphinIni.save(dolphinFile); + NativeLibrary.ReloadConfig(); } + + return pathSet; } /** @@ -81,19 +99,11 @@ public class GameFileCache * * @return true if the cache was modified */ - public boolean scanLibrary(Context context) + public boolean scanLibrary() { boolean recursiveScan = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.getBooleanGlobal(); - removeNonExistentGameFolders(context); - - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - Set folderPathsSet = preferences.getStringSet(GAME_FOLDER_PATHS_PREFERENCE, EMPTY_SET); - - if (folderPathsSet == null) - { - return false; - } + LinkedHashSet folderPathsSet = getPathSet(true); String[] folderPaths = folderPathsSet.toArray(new String[0]); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheService.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheService.java index ff37ccac4b..ab295c5550 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheService.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheService.java @@ -29,9 +29,10 @@ public final class GameFileCacheService extends IntentService private static final String ACTION_RESCAN = "org.dolphinemu.dolphinemu.RESCAN_GAME_FILE_CACHE"; private static GameFileCache gameFileCache = null; - private static AtomicReference gameFiles = new AtomicReference<>(new GameFile[]{}); - private static AtomicBoolean hasLoadedCache = new AtomicBoolean(false); - private static AtomicBoolean hasScannedLibrary = new AtomicBoolean(false); + private static final AtomicReference gameFiles = + new AtomicReference<>(new GameFile[]{}); + private static final AtomicBoolean hasLoadedCache = new AtomicBoolean(false); + private static final AtomicBoolean hasScannedLibrary = new AtomicBoolean(false); public GameFileCacheService() { @@ -166,7 +167,7 @@ public final class GameFileCacheService extends IntentService { synchronized (gameFileCache) { - boolean changed = gameFileCache.scanLibrary(this); + boolean changed = gameFileCache.scanLibrary(); if (changed) updateGameFileArray(); hasScannedLibrary.set(true); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java index cd5dd80c75..d5525bcf61 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java @@ -45,7 +45,7 @@ public final class MainActivity extends AppCompatActivity implements MainView private FloatingActionButton mFab; private static boolean sShouldRescanLibrary = true; - private MainPresenter mPresenter = new MainPresenter(this, this); + private final MainPresenter mPresenter = new MainPresenter(this, this); @Override protected void onCreate(Bundle savedInstanceState) @@ -85,7 +85,7 @@ public final class MainActivity extends AppCompatActivity implements MainView .run(this, false, this::setPlatformTabsAndStartGameFileCacheService); } - mPresenter.addDirIfNeeded(this); + mPresenter.addDirIfNeeded(); // In case the user changed a setting that affects how games are displayed, // such as system language, cover downloading... 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 75f2002ba3..a161ac782b 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 @@ -107,11 +107,11 @@ public final class MainPresenter return false; } - public void addDirIfNeeded(Context context) + public void addDirIfNeeded() { if (mDirToAdd != null) { - GameFileCache.addGameFolder(mDirToAdd, context); + GameFileCache.addGameFolder(mDirToAdd); mDirToAdd = null; } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java index 23d1050d04..8d9ab73cd9 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java @@ -39,11 +39,11 @@ public final class TvMainActivity extends FragmentActivity implements MainView { private static boolean sShouldRescanLibrary = true; - private MainPresenter mPresenter = new MainPresenter(this, this); + private final MainPresenter mPresenter = new MainPresenter(this, this); private BrowseSupportFragment mBrowseFragment; - private ArrayList mGameRows = new ArrayList<>(); + private final ArrayList mGameRows = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) @@ -73,7 +73,7 @@ public final class TvMainActivity extends FragmentActivity implements MainView GameFileCacheService.startLoad(this); } - mPresenter.addDirIfNeeded(this); + mPresenter.addDirIfNeeded(); // In case the user changed a setting that affects how games are displayed, // such as system language, cover downloading... From 99ffee9a0acc3200f8c8af7b3fc58af2d38fb614 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Wed, 4 Nov 2020 20:59:39 +0100 Subject: [PATCH 02/11] Android: Add content provider support to File::OpenFStream --- .../jni/AndroidCommon/AndroidCommon.cpp | 23 +++++++++++++++++++ .../Android/jni/AndroidCommon/AndroidCommon.h | 2 ++ Source/Core/Common/FileUtil.h | 15 ++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp index 310db6b1f7..2870044ac9 100644 --- a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp +++ b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp @@ -4,6 +4,7 @@ #include "jni/AndroidCommon/AndroidCommon.h" +#include #include #include #include @@ -66,6 +67,28 @@ std::string OpenModeToAndroid(std::string mode) return mode; } +std::string OpenModeToAndroid(std::ios_base::openmode mode) +{ + std::string result; + + if (mode & std::ios_base::in) + result += 'r'; + + if (mode & (std::ios_base::out | std::ios_base::app)) + result += 'w'; + + if (mode & std::ios_base::app) + result += 'a'; + + constexpr std::ios_base::openmode t = std::ios_base::in | std::ios_base::trunc; + if ((mode & t) == t) + result += 't'; + + // The 'b' specifier is not supported. Since we're on POSIX, it's fine to just skip it. + + return result; +} + int OpenAndroidContent(const std::string& uri, const std::string& mode) { JNIEnv* env = IDCache::GetEnvForThread(); diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.h b/Source/Android/jni/AndroidCommon/AndroidCommon.h index 7ff745f047..a196108cfe 100644 --- a/Source/Android/jni/AndroidCommon/AndroidCommon.h +++ b/Source/Android/jni/AndroidCommon/AndroidCommon.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include @@ -17,6 +18,7 @@ bool IsPathAndroidContent(const std::string& uri); // Turns a C/C++ style mode (e.g. "rb") into one which can be used with OpenAndroidContent. std::string OpenModeToAndroid(std::string mode); +std::string OpenModeToAndroid(std::ios_base::openmode mode); // Opens a given file and returns a file descriptor. int OpenAndroidContent(const std::string& uri, const std::string& mode); diff --git a/Source/Core/Common/FileUtil.h b/Source/Core/Common/FileUtil.h index 7e0b2e6990..73ba41d2da 100644 --- a/Source/Core/Common/FileUtil.h +++ b/Source/Core/Common/FileUtil.h @@ -18,6 +18,11 @@ #include "Common/StringUtil.h" #endif +#ifdef ANDROID +#include "Common/StringUtil.h" +#include "jni/AndroidCommon/AndroidCommon.h" +#endif + // User directory indices for GetUserPath enum { @@ -213,14 +218,20 @@ std::string GetExeDirectory(); bool WriteStringToFile(const std::string& filename, std::string_view str); bool ReadFileToString(const std::string& filename, std::string& str); -// To deal with Windows being dumb at unicode: +// To deal with Windows not fully supporting UTF-8 and Android not fully supporting paths. template void OpenFStream(T& fstream, const std::string& filename, std::ios_base::openmode openmode) { #ifdef _WIN32 fstream.open(UTF8ToTStr(filename).c_str(), openmode); #else - fstream.open(filename.c_str(), openmode); +#ifdef ANDROID + // Unfortunately it seems like the non-standard __open is the only way to use a file descriptor + if (IsPathAndroidContent(filename)) + fstream.__open(OpenAndroidContent(filename, OpenModeToAndroid(openmode)), openmode); + else +#endif + fstream.open(filename.c_str(), openmode); #endif } From a7c05d7e84df5949e8edf42fa6332d6aa58282f6 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Thu, 5 Nov 2020 19:47:23 +0100 Subject: [PATCH 03/11] Android: Add content provider support to File::FileInfo --- .../dolphinemu/utils/ContentHandler.java | 25 +++++++++++++++++++ .../jni/AndroidCommon/AndroidCommon.cpp | 8 ++++++ .../Android/jni/AndroidCommon/AndroidCommon.h | 3 +++ Source/Android/jni/AndroidCommon/IDCache.cpp | 9 +++++++ Source/Android/jni/AndroidCommon/IDCache.h | 1 + Source/Core/Common/FileUtil.cpp | 25 +++++++++++++++++-- Source/Core/Common/FileUtil.h | 4 +++ 7 files changed, 73 insertions(+), 2 deletions(-) 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 dbeb410079..d3fbcce9c0 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 @@ -74,6 +74,31 @@ public class ContentHandler return false; } + /** + * @return -1 if not found, -2 if directory, file size otherwise + */ + @Keep + public static long getSizeAndIsDirectory(String uri) + { + final String[] projection = new String[]{Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE}; + try (Cursor cursor = getContentResolver().query(Uri.parse(uri), projection, null, null, null)) + { + if (cursor != null && cursor.moveToFirst()) + { + if (Document.MIME_TYPE_DIR.equals(cursor.getString(0))) + return -2; + else + return cursor.isNull(1) ? 0 : cursor.getLong(1); + } + } + catch (SecurityException e) + { + Log.error("Tried to get metadata for " + uri + " without permission"); + } + + return -1; + } + @Nullable public static String getDisplayName(@NonNull Uri uri) { diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp index 2870044ac9..066b8edf27 100644 --- a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp +++ b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp @@ -104,6 +104,14 @@ bool DeleteAndroidContent(const std::string& uri) IDCache::GetContentHandlerDelete(), ToJString(env, uri)); } +jlong GetAndroidContentSizeAndIsDirectory(const std::string& uri) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + return env->CallStaticLongMethod(IDCache::GetContentHandlerClass(), + IDCache::GetContentHandlerGetSizeAndIsDirectory(), + ToJString(env, uri)); +} + int GetNetworkIpAddress() { JNIEnv* env = IDCache::GetEnvForThread(); diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.h b/Source/Android/jni/AndroidCommon/AndroidCommon.h index a196108cfe..5502363c9c 100644 --- a/Source/Android/jni/AndroidCommon/AndroidCommon.h +++ b/Source/Android/jni/AndroidCommon/AndroidCommon.h @@ -25,6 +25,9 @@ int OpenAndroidContent(const std::string& uri, const std::string& mode); // Deletes a given file. bool DeleteAndroidContent(const std::string& uri); +// Returns -1 if not found, -2 if directory, file size otherwise. +jlong GetAndroidContentSizeAndIsDirectory(const std::string& uri); + int GetNetworkIpAddress(); int GetNetworkPrefixLength(); int GetNetworkGateway(); diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 674233d79f..5f5503eb87 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -44,6 +44,7 @@ static jmethodID s_compress_cb_run; static jclass s_content_handler_class; static jmethodID s_content_handler_open_fd; static jmethodID s_content_handler_delete; +static jmethodID s_content_handler_get_size_and_is_directory; static jclass s_network_helper_class; static jmethodID s_network_helper_get_network_ip_address; @@ -210,6 +211,11 @@ jmethodID GetContentHandlerDelete() return s_content_handler_delete; } +jmethodID GetContentHandlerGetSizeAndIsDirectory() +{ + return s_content_handler_get_size_and_is_directory; +} + jclass GetNetworkHelperClass() { return s_network_helper_class; @@ -229,6 +235,7 @@ jmethodID GetNetworkHelperGetNetworkGateway() { return s_network_helper_get_network_gateway; } + } // namespace IDCache #ifdef __cplusplus @@ -306,6 +313,8 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) "(Ljava/lang/String;Ljava/lang/String;)I"); s_content_handler_delete = env->GetStaticMethodID(s_content_handler_class, "delete", "(Ljava/lang/String;)Z"); + s_content_handler_get_size_and_is_directory = env->GetStaticMethodID( + s_content_handler_class, "getSizeAndIsDirectory", "(Ljava/lang/String;)J"); 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 c89e0c9fcc..2a6c9ccbb8 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -44,6 +44,7 @@ jmethodID GetCompressCallbackRun(); jclass GetContentHandlerClass(); jmethodID GetContentHandlerOpenFd(); jmethodID GetContentHandlerDelete(); +jmethodID GetContentHandlerGetSizeAndIsDirectory(); jclass GetNetworkHelperClass(); jmethodID GetNetworkHelperGetNetworkIpAddress(); diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp index 0d41395209..450eeea0a5 100644 --- a/Source/Core/Common/FileUtil.cpp +++ b/Source/Core/Common/FileUtil.cpp @@ -78,19 +78,40 @@ FileInfo::FileInfo(const char* path) : FileInfo(std::string(path)) #else FileInfo::FileInfo(const std::string& path) : FileInfo(path.c_str()) { +#ifdef ANDROID + if (IsPathAndroidContent(path)) + AndroidContentInit(path); + else +#endif + m_exists = stat(path.c_str(), &m_stat) == 0; } FileInfo::FileInfo(const char* path) { - m_exists = stat(path, &m_stat) == 0; +#ifdef ANDROID + if (IsPathAndroidContent(path)) + AndroidContentInit(path); + else +#endif + m_exists = stat(path, &m_stat) == 0; } #endif FileInfo::FileInfo(int fd) { - m_exists = fstat(fd, &m_stat); + m_exists = fstat(fd, &m_stat) == 0; } +#ifdef ANDROID +void FileInfo::AndroidContentInit(const std::string& path) +{ + const jlong result = GetAndroidContentSizeAndIsDirectory(path); + m_exists = result != -1; + m_stat.st_mode = result == -2 ? S_IFDIR : S_IFREG; + m_stat.st_size = result >= 0 ? result : 0; +} +#endif + bool FileInfo::Exists() const { return m_exists; diff --git a/Source/Core/Common/FileUtil.h b/Source/Core/Common/FileUtil.h index 73ba41d2da..0d57e6ba9b 100644 --- a/Source/Core/Common/FileUtil.h +++ b/Source/Core/Common/FileUtil.h @@ -113,6 +113,10 @@ public: u64 GetSize() const; private: +#ifdef ANDROID + void AndroidContentInit(const std::string& path); +#endif + struct stat m_stat; bool m_exists; }; From e60665da94917812313f1ca4b96b7a03fd5b4a1c Mon Sep 17 00:00:00 2001 From: JosJuice Date: Thu, 5 Nov 2020 19:29:13 +0100 Subject: [PATCH 04/11] Android: Use storage access framework for picking single games --- .../activities/EmulationActivity.java | 18 ++++++----- .../settings/ui/SettingsActivity.java | 16 +++++++--- .../features/settings/ui/SettingsAdapter.java | 29 ++++++----------- .../dolphinemu/ui/main/MainActivity.java | 14 ++++++--- .../dolphinemu/ui/main/TvMainActivity.java | 14 ++++++--- .../dolphinemu/utils/FileBrowserHelper.java | 31 ------------------- 6 files changed, 50 insertions(+), 72 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java index 9bb3be3878..8173d13ff0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java @@ -45,6 +45,7 @@ import org.dolphinemu.dolphinemu.fragments.SaveLoadStateFragment; import org.dolphinemu.dolphinemu.overlay.InputOverlay; import org.dolphinemu.dolphinemu.overlay.InputOverlayPointer; import org.dolphinemu.dolphinemu.ui.main.MainActivity; +import org.dolphinemu.dolphinemu.ui.main.MainPresenter; import org.dolphinemu.dolphinemu.ui.main.TvMainActivity; import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner; import org.dolphinemu.dolphinemu.utils.ControllerMappingHelper; @@ -165,6 +166,11 @@ public final class EmulationActivity extends AppCompatActivity EmulationActivity.MENU_ACTION_MOTION_CONTROLS); } + public static void launch(FragmentActivity activity, String filePath) + { + launch(activity, new String[]{filePath}); + } + public static void launch(FragmentActivity activity, String[] filePaths) { if (sIgnoreLaunchRequests) @@ -411,11 +417,7 @@ public final class EmulationActivity extends AppCompatActivity // If the user picked a file, as opposed to just backing out. if (resultCode == MainActivity.RESULT_OK) { - String newDiscPath = FileBrowserHelper.getSelectedPath(result); - if (!TextUtils.isEmpty(newDiscPath)) - { - NativeLibrary.ChangeDisc(newDiscPath); - } + NativeLibrary.ChangeDisc(result.getData().toString()); } } } @@ -640,8 +642,10 @@ public final class EmulationActivity extends AppCompatActivity break; case MENU_ACTION_CHANGE_DISC: - FileBrowserHelper.openFilePicker(this, REQUEST_CHANGE_DISC, false, - FileBrowserHelper.GAME_EXTENSIONS); + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + startActivityForResult(intent, REQUEST_CHANGE_DISC); break; case MENU_SET_IR_SENSITIVITY: diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.java index 29d992a156..75538275e4 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.java @@ -24,6 +24,8 @@ import org.dolphinemu.dolphinemu.ui.main.TvMainActivity; import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; import org.dolphinemu.dolphinemu.utils.TvUtil; +import java.util.Set; + public final class SettingsActivity extends AppCompatActivity implements SettingsActivityView { private static final String ARG_MENU_TAG = "menu_tag"; @@ -172,13 +174,19 @@ public final class SettingsActivity extends AppCompatActivity implements Setting // If the user picked a file, as opposed to just backing out. if (resultCode == MainActivity.RESULT_OK) { - if (requestCode == MainPresenter.REQUEST_SD_FILE) + if (requestCode != MainPresenter.REQUEST_DIRECTORY) { Uri uri = canonicalizeIfPossible(result.getData()); - int takeFlags = result.getFlags() & - (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.RAW_EXTENSION, () -> + Set validExtensions = requestCode == MainPresenter.REQUEST_GAME_FILE ? + FileBrowserHelper.GAME_EXTENSIONS : FileBrowserHelper.RAW_EXTENSION; + + int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION; + if (requestCode != MainPresenter.REQUEST_GAME_FILE) + flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; + int takeFlags = flags & result.getFlags(); + + FileBrowserHelper.runAfterExtensionCheck(this, uri, validExtensions, () -> { getContentResolver().takePersistableUriPermission(uri, takeFlags); getFragment().getAdapter().onFilePickerConfirmation(uri.toString()); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java index 3442c72fa5..c82a13e2cc 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java @@ -306,28 +306,17 @@ public final class SettingsAdapter extends RecyclerView.Adapter= Build.VERSION_CODES.O) { - case MainPresenter.REQUEST_SD_FILE: - Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setType("*/*"); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - { - intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, - filePicker.getSelectedValue(mView.getSettings())); - } - - mView.getActivity().startActivityForResult(intent, filePicker.getRequestType()); - break; - case MainPresenter.REQUEST_GAME_FILE: - FileBrowserHelper.openFilePicker(mView.getActivity(), filePicker.getRequestType(), false, - FileBrowserHelper.GAME_EXTENSIONS); - break; - default: - throw new InvalidParameterException("Unhandled request code"); + intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, + filePicker.getSelectedValue(mView.getSettings())); } + + mView.getActivity().startActivityForResult(intent, filePicker.getRequestType()); } public void onFilePickerConfirmation(String selectedFile) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java index d5525bcf61..c7515cc511 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java @@ -2,6 +2,7 @@ package org.dolphinemu.dolphinemu.ui.main; import android.content.Intent; import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; @@ -168,8 +169,10 @@ public final class MainActivity extends AppCompatActivity implements MainView @Override public void launchOpenFileActivity() { - FileBrowserHelper.openFilePicker(this, MainPresenter.REQUEST_GAME_FILE, false, - FileBrowserHelper.GAME_EXTENSIONS); + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + startActivityForResult(intent, MainPresenter.REQUEST_GAME_FILE); } @Override @@ -194,6 +197,7 @@ public final class MainActivity extends AppCompatActivity implements MainView // If the user picked a file, as opposed to just backing out. if (resultCode == MainActivity.RESULT_OK) { + Uri uri = result.getData(); switch (requestCode) { case MainPresenter.REQUEST_DIRECTORY: @@ -201,12 +205,12 @@ public final class MainActivity extends AppCompatActivity implements MainView break; case MainPresenter.REQUEST_GAME_FILE: - EmulationActivity.launch(this, FileBrowserHelper.getSelectedFiles(result)); + FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.GAME_EXTENSIONS, + () -> EmulationActivity.launch(this, result.getData().toString())); break; case MainPresenter.REQUEST_WAD_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, result.getData(), - FileBrowserHelper.WAD_EXTENSION, + FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.WAD_EXTENSION, () -> mPresenter.installWAD(result.getData().toString())); break; } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java index 8d9ab73cd9..8d3d6f2aec 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java @@ -2,6 +2,7 @@ package org.dolphinemu.dolphinemu.ui.main; import android.content.Intent; import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Bundle; import android.widget.Toast; @@ -173,8 +174,10 @@ public final class TvMainActivity extends FragmentActivity implements MainView @Override public void launchOpenFileActivity() { - FileBrowserHelper.openFilePicker(this, MainPresenter.REQUEST_GAME_FILE, false, - FileBrowserHelper.GAME_EXTENSIONS); + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + startActivityForResult(intent, MainPresenter.REQUEST_GAME_FILE); } @Override @@ -218,6 +221,7 @@ public final class TvMainActivity extends FragmentActivity implements MainView // If the user picked a file, as opposed to just backing out. if (resultCode == MainActivity.RESULT_OK) { + Uri uri = result.getData(); switch (requestCode) { case MainPresenter.REQUEST_DIRECTORY: @@ -225,12 +229,12 @@ public final class TvMainActivity extends FragmentActivity implements MainView break; case MainPresenter.REQUEST_GAME_FILE: - EmulationActivity.launch(this, FileBrowserHelper.getSelectedFiles(result)); + FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.GAME_EXTENSIONS, + () -> EmulationActivity.launch(this, result.getData().toString())); break; case MainPresenter.REQUEST_WAD_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, result.getData(), - FileBrowserHelper.WAD_EXTENSION, + FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.WAD_EXTENSION, () -> mPresenter.installWAD(result.getData().toString())); break; } 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 79ec49deae..dc3098df9f 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 @@ -50,21 +50,6 @@ public final class FileBrowserHelper activity.startActivityForResult(i, MainPresenter.REQUEST_DIRECTORY); } - public static void openFilePicker(FragmentActivity activity, int requestCode, boolean allowMulti, - HashSet extensions) - { - Intent i = new Intent(activity, CustomFilePickerActivity.class); - - i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, allowMulti); - i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false); - i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE); - i.putExtra(FilePickerActivity.EXTRA_START_PATH, - Environment.getExternalStorageDirectory().getPath()); - i.putExtra(CustomFilePickerActivity.EXTRA_EXTENSIONS, extensions); - - activity.startActivityForResult(i, requestCode); - } - @Nullable public static String getSelectedPath(Intent result) { @@ -79,22 +64,6 @@ public final class FileBrowserHelper return null; } - @Nullable - public static String[] getSelectedFiles(Intent result) - { - // Use the provided utility method to parse the result - List files = Utils.getSelectedFilesFromResult(result); - if (!files.isEmpty()) - { - String[] paths = new String[files.size()]; - for (int i = 0; i < files.size(); i++) - paths[i] = Utils.getFileForUri(files.get(i)).getAbsolutePath(); - return paths; - } - - return null; - } - public static boolean isPathEmptyOrValid(StringSetting path) { return isPathEmptyOrValid(path.getStringGlobal()); From 525268f043a25116dbb5e362d9e3e84b3f4ca5e5 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 8 Nov 2020 15:39:17 +0100 Subject: [PATCH 05/11] Android: Fix opening games with extensionless URI --- .../org/dolphinemu/dolphinemu/utils/ContentHandler.java | 6 ++++++ Source/Android/jni/AndroidCommon/AndroidCommon.cpp | 9 +++++++++ Source/Android/jni/AndroidCommon/AndroidCommon.h | 6 ++++++ Source/Android/jni/AndroidCommon/IDCache.cpp | 8 ++++++++ Source/Android/jni/AndroidCommon/IDCache.h | 1 + Source/Core/Core/Boot/Boot.cpp | 9 +++++++++ 6 files changed, 39 insertions(+) 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 d3fbcce9c0..53e950ac41 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 @@ -99,6 +99,12 @@ public class ContentHandler return -1; } + @Nullable @Keep + public static String getDisplayName(String uri) + { + return getDisplayName(Uri.parse(uri)); + } + @Nullable public static String getDisplayName(@NonNull Uri uri) { diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp index 066b8edf27..373a4699cd 100644 --- a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp +++ b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp @@ -112,6 +112,15 @@ jlong GetAndroidContentSizeAndIsDirectory(const std::string& uri) ToJString(env, uri)); } +std::string GetAndroidContentDisplayName(const std::string& uri) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + jobject display_name = + env->CallStaticObjectMethod(IDCache::GetContentHandlerClass(), + IDCache::GetContentHandlerGetDisplayName(), ToJString(env, uri)); + return display_name ? GetJString(env, reinterpret_cast(display_name)) : ""; +} + int GetNetworkIpAddress() { JNIEnv* env = IDCache::GetEnvForThread(); diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.h b/Source/Android/jni/AndroidCommon/AndroidCommon.h index 5502363c9c..cb14d493b9 100644 --- a/Source/Android/jni/AndroidCommon/AndroidCommon.h +++ b/Source/Android/jni/AndroidCommon/AndroidCommon.h @@ -28,6 +28,12 @@ bool DeleteAndroidContent(const std::string& uri); // Returns -1 if not found, -2 if directory, file size otherwise. jlong GetAndroidContentSizeAndIsDirectory(const std::string& uri); +// An unmangled URI (one which the C++ code has not appended anything to) can't be relied on +// to contain a file name at all. If a file name is desired, this function is the most reliable +// way to get it, but the display name is not guaranteed to always actually be like a file name. +// An empty string will be returned for files which do not exist. +std::string GetAndroidContentDisplayName(const std::string& uri); + int GetNetworkIpAddress(); int GetNetworkPrefixLength(); int GetNetworkGateway(); diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index 5f5503eb87..baf44633f7 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -45,6 +45,7 @@ static jclass s_content_handler_class; static jmethodID s_content_handler_open_fd; 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 jclass s_network_helper_class; static jmethodID s_network_helper_get_network_ip_address; @@ -216,6 +217,11 @@ jmethodID GetContentHandlerGetSizeAndIsDirectory() return s_content_handler_get_size_and_is_directory; } +jmethodID GetContentHandlerGetDisplayName() +{ + return s_content_handler_get_display_name; +} + jclass GetNetworkHelperClass() { return s_network_helper_class; @@ -315,6 +321,8 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) env->GetStaticMethodID(s_content_handler_class, "delete", "(Ljava/lang/String;)Z"); s_content_handler_get_size_and_is_directory = env->GetStaticMethodID( s_content_handler_class, "getSizeAndIsDirectory", "(Ljava/lang/String;)J"); + s_content_handler_get_display_name = env->GetStaticMethodID( + s_content_handler_class, "getDisplayName", "(Ljava/lang/String;)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 2a6c9ccbb8..621a522a6c 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -45,6 +45,7 @@ jclass GetContentHandlerClass(); jmethodID GetContentHandlerOpenFd(); jmethodID GetContentHandlerDelete(); jmethodID GetContentHandlerGetSizeAndIsDirectory(); +jmethodID GetContentHandlerGetDisplayName(); jclass GetNetworkHelperClass(); jmethodID GetNetworkHelperGetNetworkIpAddress(); diff --git a/Source/Core/Core/Boot/Boot.cpp b/Source/Core/Core/Boot/Boot.cpp index 6a8a4461f8..cb1e795a1c 100644 --- a/Source/Core/Core/Boot/Boot.cpp +++ b/Source/Core/Core/Boot/Boot.cpp @@ -158,6 +158,15 @@ BootParameters::GenerateFromFile(std::vector paths, if (paths.size() == 1) paths.clear(); +#ifdef ANDROID + if (extension.empty() && IsPathAndroidContent(path)) + { + const std::string display_name = GetAndroidContentDisplayName(path); + SplitPath(display_name, nullptr, nullptr, &extension); + std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower); + } +#endif + static const std::unordered_set disc_image_extensions = { {".gcm", ".iso", ".tgc", ".wbfs", ".ciso", ".gcz", ".wia", ".rvz", ".dol", ".elf"}}; if (disc_image_extensions.find(extension) != disc_image_extensions.end() || is_drive) From 2126f62111a95e3a2386467747e5bf94b9943e1d Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 8 Nov 2020 16:57:49 +0100 Subject: [PATCH 06/11] Android: Add content provider support to File::ScanDirectoryTree --- .../dolphinemu/utils/ContentHandler.java | 209 ++++++++++++++++-- .../jni/AndroidCommon/AndroidCommon.cpp | 9 + .../Android/jni/AndroidCommon/AndroidCommon.h | 4 + Source/Android/jni/AndroidCommon/IDCache.cpp | 8 + Source/Android/jni/AndroidCommon/IDCache.h | 1 + Source/Core/Common/FileUtil.cpp | 46 +++- 6 files changed, 255 insertions(+), 22 deletions(-) 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 53e950ac41..5233c9dc59 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 @@ -14,15 +14,16 @@ import androidx.annotation.Keep; import org.dolphinemu.dolphinemu.DolphinApplication; import java.io.FileNotFoundException; +import java.util.List; public class ContentHandler { @Keep - public static int openFd(String uri, String mode) + public static int openFd(@NonNull String uri, @NonNull String mode) { try { - return getContentResolver().openFileDescriptor(Uri.parse(uri), mode).detachFd(); + return getContentResolver().openFileDescriptor(unmangle(uri), mode).detachFd(); } catch (SecurityException e) { @@ -38,11 +39,11 @@ public class ContentHandler } @Keep - public static boolean delete(String uri) + public static boolean delete(@NonNull String uri) { try { - return DocumentsContract.deleteDocument(getContentResolver(), Uri.parse(uri)); + return DocumentsContract.deleteDocument(getContentResolver(), unmangle(uri)); } catch (SecurityException e) { @@ -60,8 +61,9 @@ public class ContentHandler { try { + Uri documentUri = treeToDocument(unmangle(uri)); final String[] projection = new String[]{Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE}; - try (Cursor cursor = getContentResolver().query(Uri.parse(uri), projection, null, null, null)) + try (Cursor cursor = getContentResolver().query(documentUri, projection, null, null, null)) { return cursor != null && cursor.getCount() > 0; } @@ -70,6 +72,9 @@ public class ContentHandler { Log.error("Tried to check if " + uri + " exists without permission"); } + catch (FileNotFoundException ignored) + { + } return false; } @@ -78,38 +83,53 @@ public class ContentHandler * @return -1 if not found, -2 if directory, file size otherwise */ @Keep - public static long getSizeAndIsDirectory(String uri) + public static long getSizeAndIsDirectory(@NonNull String uri) { - final String[] projection = new String[]{Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE}; - try (Cursor cursor = getContentResolver().query(Uri.parse(uri), projection, null, null, null)) + try { - if (cursor != null && cursor.moveToFirst()) + Uri documentUri = treeToDocument(unmangle(uri)); + final String[] projection = new String[]{Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE}; + try (Cursor cursor = getContentResolver().query(documentUri, projection, null, null, null)) { - if (Document.MIME_TYPE_DIR.equals(cursor.getString(0))) - return -2; - else - return cursor.isNull(1) ? 0 : cursor.getLong(1); + if (cursor != null && cursor.moveToFirst()) + { + if (Document.MIME_TYPE_DIR.equals(cursor.getString(0))) + return -2; + else + return cursor.isNull(1) ? 0 : cursor.getLong(1); + } } } catch (SecurityException e) { Log.error("Tried to get metadata for " + uri + " without permission"); } + catch (FileNotFoundException ignored) + { + } return -1; } @Nullable @Keep - public static String getDisplayName(String uri) + public static String getDisplayName(@NonNull String uri) { - return getDisplayName(Uri.parse(uri)); + try + { + return getDisplayName(unmangle(uri)); + } + catch (FileNotFoundException e) + { + return null; + } } @Nullable public static String getDisplayName(@NonNull Uri uri) { final String[] projection = new String[]{Document.COLUMN_DISPLAY_NAME}; - try (Cursor cursor = getContentResolver().query(uri, projection, null, null, null)) + Uri documentUri = treeToDocument(uri); + try (Cursor cursor = getContentResolver().query(documentUri, projection, null, null, null)) { if (cursor != null && cursor.moveToFirst()) { @@ -124,6 +144,163 @@ public class ContentHandler return null; } + @NonNull @Keep + public static String[] getChildNames(@NonNull String uri) + { + try + { + Uri unmangledUri = unmangle(uri); + String documentId = DocumentsContract.getDocumentId(treeToDocument(unmangledUri)); + Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(unmangledUri, documentId); + + final String[] projection = new String[]{Document.COLUMN_DISPLAY_NAME}; + try (Cursor cursor = getContentResolver().query(childrenUri, projection, null, null, null)) + { + if (cursor != null) + { + String[] result = new String[cursor.getCount()]; + for (int i = 0; i < result.length; i++) + { + cursor.moveToNext(); + result[i] = cursor.getString(0); + } + return result; + } + } + } + catch (SecurityException e) + { + Log.error("Tried to get children of " + uri + " without permission"); + } + catch (FileNotFoundException ignored) + { + } + + return new String[0]; + } + + @NonNull + private static Uri getChild(@NonNull Uri parentUri, @NonNull String childName) + throws FileNotFoundException, SecurityException + { + String parentId = DocumentsContract.getDocumentId(treeToDocument(parentUri)); + Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(parentUri, parentId); + + final String[] projection = new String[]{Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_DOCUMENT_ID}; + final String selection = Document.COLUMN_DISPLAY_NAME + "=?"; + final String[] selectionArgs = new String[]{childName}; + try (Cursor cursor = getContentResolver().query(childrenUri, projection, selection, + selectionArgs, null)) + { + if (cursor != null) + { + while (cursor.moveToNext()) + { + // FileProvider seemingly doesn't support selections, so we have to manually filter here + if (childName.equals(cursor.getString(0))) + { + return DocumentsContract.buildDocumentUriUsingTree(parentUri, cursor.getString(1)); + } + } + } + } + catch (SecurityException e) + { + Log.error("Tried to get child " + childName + " of " + parentUri + " without permission"); + } + + throw new FileNotFoundException(parentUri + "/" + childName); + } + + /** + * Since our C++ code was written under the assumption that it would be running under a filesystem + * which supports normal paths, it appends a slash followed by a file name when it wants to access + * a file in a directory. This function translates that into the type of URI that SAF requires. + * + * In order to detect whether a URI is mangled or not, we make the assumption that an + * unmangled URI contains at least one % and does not contain any slashes after the last %. + * This seems to hold for all common storage providers, but it is theoretically for a storage + * provider to use URIs without any % characters. + */ + @NonNull + private static Uri unmangle(@NonNull String uri) throws FileNotFoundException, SecurityException + { + int lastComponentEnd = getLastComponentEnd(uri); + int lastComponentStart = getLastComponentStart(uri, lastComponentEnd); + + if (lastComponentStart == 0) + { + return Uri.parse(uri.substring(0, lastComponentEnd)); + } + else + { + Uri parentUri = unmangle(uri.substring(0, lastComponentStart)); + String childName = uri.substring(lastComponentStart, lastComponentEnd); + return getChild(parentUri, childName); + } + } + + /** + * Returns the last character which is not a slash. + */ + private static int getLastComponentEnd(@NonNull String uri) + { + int i = uri.length(); + while (i > 0 && uri.charAt(i - 1) == '/') + i--; + return i; + } + + /** + * Scans backwards starting from lastComponentEnd and returns the index after the first slash + * it finds, but only if there is a % before that slash and there is no % after it. + */ + private static int getLastComponentStart(@NonNull String uri, int lastComponentEnd) + { + int i = lastComponentEnd; + while (i > 0 && uri.charAt(i - 1) != '/') + { + i--; + if (uri.charAt(i) == '%') + return 0; + } + + int j = i; + while (j > 0) + { + j--; + if (uri.charAt(j) == '%') + return i; + } + + return 0; + } + + @NonNull + private static Uri treeToDocument(@NonNull Uri uri) + { + if (isTreeUri(uri)) + { + String documentId = DocumentsContract.getTreeDocumentId(uri); + return DocumentsContract.buildDocumentUriUsingTree(uri, documentId); + } + else + { + return uri; + } + } + + /** + * This is like DocumentsContract.isTreeUri, except it doesn't return true for URIs like + * content://com.example/tree/12/document/24/. We want to treat those as documents, not trees. + */ + private static boolean isTreeUri(@NonNull Uri uri) + { + final List pathSegments = uri.getPathSegments(); + return pathSegments.size() == 2 && "tree".equals(pathSegments.get(0)); + } + private static ContentResolver getContentResolver() { return DolphinApplication.getAppContext().getContentResolver(); diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp index 373a4699cd..b6ecd41d48 100644 --- a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp +++ b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp @@ -121,6 +121,15 @@ std::string GetAndroidContentDisplayName(const std::string& uri) return display_name ? GetJString(env, reinterpret_cast(display_name)) : ""; } +std::vector GetAndroidContentChildNames(const std::string& uri) +{ + JNIEnv* env = IDCache::GetEnvForThread(); + jobject children = + env->CallStaticObjectMethod(IDCache::GetContentHandlerClass(), + IDCache::GetContentHandlerGetChildNames(), ToJString(env, uri)); + return JStringArrayToVector(env, reinterpret_cast(children)); +} + int GetNetworkIpAddress() { JNIEnv* env = IDCache::GetEnvForThread(); diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.h b/Source/Android/jni/AndroidCommon/AndroidCommon.h index cb14d493b9..a1a1643b40 100644 --- a/Source/Android/jni/AndroidCommon/AndroidCommon.h +++ b/Source/Android/jni/AndroidCommon/AndroidCommon.h @@ -6,6 +6,7 @@ #include #include +#include #include @@ -34,6 +35,9 @@ jlong GetAndroidContentSizeAndIsDirectory(const std::string& uri); // An empty string will be returned for files which do not exist. std::string GetAndroidContentDisplayName(const std::string& uri); +// Returns the display names of all children of a directory. +std::vector GetAndroidContentChildNames(const std::string& uri); + int GetNetworkIpAddress(); int GetNetworkPrefixLength(); int GetNetworkGateway(); diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index baf44633f7..c79007baae 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -46,6 +46,7 @@ static jmethodID s_content_handler_open_fd; 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 jclass s_network_helper_class; static jmethodID s_network_helper_get_network_ip_address; @@ -222,6 +223,11 @@ jmethodID GetContentHandlerGetDisplayName() return s_content_handler_get_display_name; } +jmethodID GetContentHandlerGetChildNames() +{ + return s_content_handler_get_child_names; +} + jclass GetNetworkHelperClass() { return s_network_helper_class; @@ -323,6 +329,8 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) s_content_handler_class, "getSizeAndIsDirectory", "(Ljava/lang/String;)J"); s_content_handler_get_display_name = env->GetStaticMethodID( 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;)[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 621a522a6c..3f568e3f73 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.h +++ b/Source/Android/jni/AndroidCommon/IDCache.h @@ -46,6 +46,7 @@ jmethodID GetContentHandlerOpenFd(); jmethodID GetContentHandlerDelete(); jmethodID GetContentHandlerGetSizeAndIsDirectory(); jmethodID GetContentHandlerGetDisplayName(); +jmethodID GetContentHandlerGetChildNames(); jclass GetNetworkHelperClass(); jmethodID GetNetworkHelperGetNetworkIpAddress(); diff --git a/Source/Core/Common/FileUtil.cpp b/Source/Core/Common/FileUtil.cpp index 450eeea0a5..dc26662a2a 100644 --- a/Source/Core/Common/FileUtil.cpp +++ b/Source/Core/Common/FileUtil.cpp @@ -497,14 +497,47 @@ FSTEntry ScanDirectoryTree(const std::string& directory, bool recursive) { const std::string virtual_name(TStrToUTF8(ffd.cFileName)); #else - DIR* dirp = opendir(directory.c_str()); - if (!dirp) - return parent_entry; + DIR* dirp = nullptr; + +#ifdef ANDROID + std::vector child_names; + if (IsPathAndroidContent(directory)) + { + child_names = GetAndroidContentChildNames(directory); + } + else +#endif + { + dirp = opendir(directory.c_str()); + if (!dirp) + return parent_entry; + } + +#ifdef ANDROID + auto it = child_names.cbegin(); +#endif // non Windows loop - while (dirent* result = readdir(dirp)) + while (true) { - const std::string virtual_name(result->d_name); + std::string virtual_name; + +#ifdef ANDROID + if (!dirp) + { + if (it == child_names.cend()) + break; + virtual_name = *it; + ++it; + } + else +#endif + { + dirent* result = readdir(dirp); + if (!result) + break; + virtual_name = result->d_name; + } #endif if (virtual_name == "." || virtual_name == "..") continue; @@ -535,7 +568,8 @@ FSTEntry ScanDirectoryTree(const std::string& directory, bool recursive) FindClose(hFind); #else } - closedir(dirp); + if (dirp) + closedir(dirp); #endif return parent_entry; From ae8de3510533d398869cde03d59a5bf8f27f2ca7 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 8 Nov 2020 18:41:01 +0100 Subject: [PATCH 07/11] Android: Use storage access framework for game list --- .../dolphinemu/model/GameFileCache.java | 4 ++-- .../dolphinemu/ui/main/MainActivity.java | 5 +++-- .../dolphinemu/ui/main/MainPresenter.java | 19 +++++++++++++++++-- .../dolphinemu/ui/main/TvMainActivity.java | 5 +++-- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFileCache.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFileCache.java index 2fdc0cb179..a15e734d0b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFileCache.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFileCache.java @@ -6,6 +6,7 @@ import org.dolphinemu.dolphinemu.NativeLibrary; import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; import org.dolphinemu.dolphinemu.features.settings.model.Settings; import org.dolphinemu.dolphinemu.features.settings.utils.SettingsFile; +import org.dolphinemu.dolphinemu.utils.ContentHandler; import org.dolphinemu.dolphinemu.utils.IniFile; import java.io.File; @@ -58,8 +59,7 @@ public class GameFileCache String path = dolphinIni.getString(Settings.SECTION_INI_GENERAL, SettingsFile.KEY_ISO_PATH_BASE + i, ""); - File folder = new File(path); - if (folder.exists()) + if (path.startsWith("content://") ? ContentHandler.exists(path) : new File(path).exists()) { pathSet.add(path); } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java index c7515cc511..f09668713f 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java @@ -163,7 +163,8 @@ public final class MainActivity extends AppCompatActivity implements MainView @Override public void launchFileListActivity() { - FileBrowserHelper.openDirectoryPicker(this, FileBrowserHelper.GAME_EXTENSIONS); + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + startActivityForResult(intent, MainPresenter.REQUEST_DIRECTORY); } @Override @@ -201,7 +202,7 @@ public final class MainActivity extends AppCompatActivity implements MainView switch (requestCode) { case MainPresenter.REQUEST_DIRECTORY: - mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedPath(result)); + mPresenter.onDirectorySelected(result); break; case MainPresenter.REQUEST_GAME_FILE: 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 a161ac782b..92a4096bfb 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 @@ -2,9 +2,11 @@ package org.dolphinemu.dolphinemu.ui.main; import android.app.Activity; import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.net.Uri; import android.widget.Toast; import androidx.appcompat.app.AlertDialog; @@ -17,6 +19,9 @@ import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag; import org.dolphinemu.dolphinemu.model.GameFileCache; import org.dolphinemu.dolphinemu.services.GameFileCacheService; import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner; +import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; + +import java.util.Set; public final class MainPresenter { @@ -116,9 +121,19 @@ public final class MainPresenter } } - public void onDirectorySelected(String dir) + public void onDirectorySelected(Intent result) { - mDirToAdd = dir; + ContentResolver contentResolver = mContext.getContentResolver(); + Uri uri = result.getData(); + + Uri canonicalizedUri = contentResolver.canonicalize(uri); + if (canonicalizedUri != null) + uri = canonicalizedUri; + + int takeFlags = result.getFlags() & Intent.FLAG_GRANT_READ_URI_PERMISSION; + mContext.getContentResolver().takePersistableUriPermission(uri, takeFlags); + + mDirToAdd = uri.toString(); } public void installWAD(String file) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java index 8d3d6f2aec..262cff6c00 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java @@ -168,7 +168,8 @@ public final class TvMainActivity extends FragmentActivity implements MainView @Override public void launchFileListActivity() { - FileBrowserHelper.openDirectoryPicker(this, FileBrowserHelper.GAME_EXTENSIONS); + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + startActivityForResult(intent, MainPresenter.REQUEST_DIRECTORY); } @Override @@ -225,7 +226,7 @@ public final class TvMainActivity extends FragmentActivity implements MainView switch (requestCode) { case MainPresenter.REQUEST_DIRECTORY: - mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedPath(result)); + mPresenter.onDirectorySelected(result); break; case MainPresenter.REQUEST_GAME_FILE: From 399ede37a66a161e2379df34501e2b9a172d3457 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 8 Nov 2020 20:37:56 +0100 Subject: [PATCH 08/11] Android: Catch all exceptions in ContentHandler --- .../dolphinemu/utils/ContentHandler.java | 54 +++++++++++++------ 1 file changed, 39 insertions(+), 15 deletions(-) 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 5233c9dc59..1ae3a805d3 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 @@ -16,6 +16,21 @@ import org.dolphinemu.dolphinemu.DolphinApplication; import java.io.FileNotFoundException; import java.util.List; +/* + We use a lot of "catch (Exception e)" in this class. This is for two reasons: + + 1. We don't want any exceptions to escape to native code, as this leads to nasty crashes + that often don't have stack traces that make sense. + + 2. The sheer number of different exceptions, both documented and undocumented. These include: + - FileNotFoundException when a file doesn't exist + - FileNotFoundException when using an invalid open mode (according to the documentation) + - IllegalArgumentException when using an invalid open mode (in practice with FileProvider) + - IllegalArgumentException when providing a tree where a document was expected and vice versa + - SecurityException when trying to access something the user hasn't granted us permission to + - UnsupportedOperationException when a URI specifies a storage provider that doesn't exist + */ + public class ContentHandler { @Keep @@ -28,14 +43,12 @@ public class ContentHandler catch (SecurityException e) { Log.error("Tried to open " + uri + " without permission"); - return -1; } - // Some content providers throw IllegalArgumentException for invalid modes, - // despite the documentation saying that invalid modes result in a FileNotFoundException - catch (FileNotFoundException | IllegalArgumentException | NullPointerException e) + catch (Exception ignored) { - return -1; } + + return -1; } @Keep @@ -45,16 +58,20 @@ public class ContentHandler { return DocumentsContract.deleteDocument(getContentResolver(), unmangle(uri)); } - catch (SecurityException e) - { - Log.error("Tried to delete " + uri + " without permission"); - return false; - } catch (FileNotFoundException e) { // Return true because we care about the file not being there, not the actual delete. return true; } + catch (SecurityException e) + { + Log.error("Tried to delete " + uri + " without permission"); + } + catch (Exception ignored) + { + } + + return false; } public static boolean exists(@NonNull String uri) @@ -72,7 +89,7 @@ public class ContentHandler { Log.error("Tried to check if " + uri + " exists without permission"); } - catch (FileNotFoundException ignored) + catch (Exception ignored) { } @@ -104,7 +121,7 @@ public class ContentHandler { Log.error("Tried to get metadata for " + uri + " without permission"); } - catch (FileNotFoundException ignored) + catch (Exception ignored) { } @@ -118,10 +135,11 @@ public class ContentHandler { return getDisplayName(unmangle(uri)); } - catch (FileNotFoundException e) + catch (Exception ignored) { - return null; } + + return null; } @Nullable @@ -140,6 +158,9 @@ public class ContentHandler { Log.error("Tried to get display name of " + uri + " without permission"); } + catch (Exception ignored) + { + } return null; } @@ -172,7 +193,7 @@ public class ContentHandler { Log.error("Tried to get children of " + uri + " without permission"); } - catch (FileNotFoundException ignored) + catch (Exception ignored) { } @@ -209,6 +230,9 @@ public class ContentHandler { Log.error("Tried to get child " + childName + " of " + parentUri + " without permission"); } + catch (Exception ignored) + { + } throw new FileNotFoundException(parentUri + "/" + childName); } From 73855168f3f57f8a3037223235740eb7d9dd76e4 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 8 Nov 2020 23:01:59 +0100 Subject: [PATCH 09/11] Android: Show a message when adding a folder with no games To catch people who try to use unsupported formats. --- .../dolphinemu/ui/main/MainPresenter.java | 17 ++++++- .../dolphinemu/utils/ContentHandler.java | 49 ++++++++++++++----- .../dolphinemu/utils/FileBrowserHelper.java | 15 ++++-- .../app/src/main/res/values/strings.xml | 1 + .../jni/AndroidCommon/AndroidCommon.cpp | 6 +-- .../Android/jni/AndroidCommon/AndroidCommon.h | 2 +- Source/Android/jni/AndroidCommon/IDCache.cpp | 2 +- 7 files changed, 69 insertions(+), 23 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 92a4096bfb..ff34ea2f1b 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 @@ -15,12 +15,15 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.dolphinemu.dolphinemu.BuildConfig; import org.dolphinemu.dolphinemu.NativeLibrary; import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting; import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag; import org.dolphinemu.dolphinemu.model.GameFileCache; import org.dolphinemu.dolphinemu.services.GameFileCacheService; import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner; +import org.dolphinemu.dolphinemu.utils.ContentHandler; import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; +import java.util.Arrays; import java.util.Set; public final class MainPresenter @@ -123,9 +126,21 @@ public final class MainPresenter public void onDirectorySelected(Intent result) { - ContentResolver contentResolver = mContext.getContentResolver(); Uri uri = result.getData(); + 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)))) + { + AlertDialog.Builder builder = new AlertDialog.Builder(mContext, R.style.DolphinDialogBase); + builder.setMessage(mContext.getString(R.string.wrong_file_extension_in_directory, + FileBrowserHelper.setToSortedDelimitedString(FileBrowserHelper.GAME_EXTENSIONS))); + builder.setPositiveButton(R.string.ok, null); + builder.show(); + } + + ContentResolver contentResolver = mContext.getContentResolver(); Uri canonicalizedUri = contentResolver.canonicalize(uri); if (canonicalizedUri != null) uri = canonicalizedUri; 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 1ae3a805d3..ebedef4b60 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 @@ -14,6 +14,7 @@ import androidx.annotation.Keep; import org.dolphinemu.dolphinemu.DolphinApplication; import java.io.FileNotFoundException; +import java.util.ArrayList; import java.util.List; /* @@ -166,26 +167,52 @@ public class ContentHandler } @NonNull @Keep - public static String[] getChildNames(@NonNull String uri) + public static String[] getChildNames(@NonNull String uri, boolean recursive) { try { - Uri unmangledUri = unmangle(uri); - String documentId = DocumentsContract.getDocumentId(treeToDocument(unmangledUri)); - Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(unmangledUri, documentId); + return getChildNames(unmangle(uri), recursive); + } + catch (Exception ignored) + { + } - final String[] projection = new String[]{Document.COLUMN_DISPLAY_NAME}; + return new String[0]; + } + + @NonNull + public static String[] getChildNames(@NonNull Uri uri, boolean recursive) + { + ArrayList result = new ArrayList<>(); + getChildNames(uri, DocumentsContract.getDocumentId(treeToDocument(uri)), recursive, result); + return result.toArray(new String[0]); + } + + private static void getChildNames(@NonNull Uri uri, @NonNull String documentId, boolean recursive, + List resultOut) + { + 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}; try (Cursor cursor = getContentResolver().query(childrenUri, projection, null, null, null)) { if (cursor != null) { - String[] result = new String[cursor.getCount()]; - for (int i = 0; i < result.length; i++) + while (cursor.moveToNext()) { - cursor.moveToNext(); - result[i] = cursor.getString(0); + if (recursive && Document.MIME_TYPE_DIR.equals(cursor.getString(1))) + { + getChildNames(uri, cursor.getString(2), recursive, resultOut); + } + else + { + resultOut.add(cursor.getString(0)); + } } - return result; } } } @@ -196,8 +223,6 @@ public class ContentHandler catch (Exception ignored) { } - - return new String[0]; } @NonNull 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 dc3098df9f..f3735eabea 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 @@ -102,10 +102,8 @@ public final class FileBrowserHelper int messageId = validExtensions.size() == 1 ? R.string.wrong_file_extension_single : R.string.wrong_file_extension_multiple; - ArrayList extensionsList = new ArrayList<>(validExtensions); - Collections.sort(extensionsList); - - message = context.getString(messageId, extension, join(", ", extensionsList)); + message = context.getString(messageId, extension, + setToSortedDelimitedString(validExtensions)); } new AlertDialog.Builder(context, R.style.DolphinDialogBase) @@ -117,7 +115,7 @@ public final class FileBrowserHelper } @Nullable - private static String getExtension(@Nullable String fileName) + public static String getExtension(@Nullable String fileName) { if (fileName == null) return null; @@ -126,6 +124,13 @@ public final class FileBrowserHelper return dotIndex != -1 ? fileName.substring(dotIndex + 1) : null; } + public static String setToSortedDelimitedString(Set set) + { + ArrayList list = new ArrayList<>(set); + Collections.sort(list); + return join(", ", list); + } + // TODO: Replace this with String.join once we can use Java 8 private static String join(CharSequence delimiter, Iterable elements) { diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 4cb7c09a17..d9faf899ac 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -438,6 +438,7 @@ It can efficiently compress both junk data and encrypted Wii data. The selected file does not appear to have a file name extension.\n\nContinue anyway? The selected file has the file name extension \"%1$s\", but \"%2$s\" was expected.\n\nContinue anyway? The selected file has the file name extension \"%1$s\", but one of these extensions was expected: %2$s\n\nContinue anyway? + No compatible files were found in the selected location.\n\nThe supported formats are: %1$s Dolphin does not have permission to access one or more configured paths. Would you like to fix this before starting? diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp index b6ecd41d48..9e57d2764a 100644 --- a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp +++ b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp @@ -124,9 +124,9 @@ std::string GetAndroidContentDisplayName(const std::string& uri) std::vector GetAndroidContentChildNames(const std::string& uri) { JNIEnv* env = IDCache::GetEnvForThread(); - jobject children = - env->CallStaticObjectMethod(IDCache::GetContentHandlerClass(), - IDCache::GetContentHandlerGetChildNames(), ToJString(env, uri)); + jobject children = env->CallStaticObjectMethod(IDCache::GetContentHandlerClass(), + IDCache::GetContentHandlerGetChildNames(), + ToJString(env, uri), false); return JStringArrayToVector(env, reinterpret_cast(children)); } diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.h b/Source/Android/jni/AndroidCommon/AndroidCommon.h index a1a1643b40..1f75864704 100644 --- a/Source/Android/jni/AndroidCommon/AndroidCommon.h +++ b/Source/Android/jni/AndroidCommon/AndroidCommon.h @@ -35,7 +35,7 @@ jlong GetAndroidContentSizeAndIsDirectory(const std::string& uri); // An empty string will be returned for files which do not exist. std::string GetAndroidContentDisplayName(const std::string& uri); -// Returns the display names of all children of a directory. +// Returns the display names of all children of a directory, non-recursively. std::vector GetAndroidContentChildNames(const std::string& uri); int GetNetworkIpAddress(); diff --git a/Source/Android/jni/AndroidCommon/IDCache.cpp b/Source/Android/jni/AndroidCommon/IDCache.cpp index c79007baae..a4a7ef4ebb 100644 --- a/Source/Android/jni/AndroidCommon/IDCache.cpp +++ b/Source/Android/jni/AndroidCommon/IDCache.cpp @@ -330,7 +330,7 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved) s_content_handler_get_display_name = env->GetStaticMethodID( 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;)[Ljava/lang/String;"); + s_content_handler_class, "getChildNames", "(Ljava/lang/String;Z)[Ljava/lang/String;"); const jclass network_helper_class = env->FindClass("org/dolphinemu/dolphinemu/utils/NetworkHelper"); From 01b964b01aad3f46611e5fec8c44b60ed04a9b3f Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 8 Nov 2020 23:50:30 +0100 Subject: [PATCH 10/11] Android: Don't consider .dff files valid for game list --- .../org/dolphinemu/dolphinemu/ui/main/MainActivity.java | 3 ++- .../dolphinemu/dolphinemu/ui/main/TvMainActivity.java | 3 ++- .../dolphinemu/dolphinemu/utils/FileBrowserHelper.java | 9 ++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java index f09668713f..f68e5e19af 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java @@ -206,7 +206,8 @@ public final class MainActivity extends AppCompatActivity implements MainView break; case MainPresenter.REQUEST_GAME_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.GAME_EXTENSIONS, + FileBrowserHelper.runAfterExtensionCheck(this, uri, + FileBrowserHelper.GAME_LIKE_EXTENSIONS, () -> EmulationActivity.launch(this, result.getData().toString())); break; diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java index 262cff6c00..90529d21cf 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java @@ -230,7 +230,8 @@ public final class TvMainActivity extends FragmentActivity implements MainView break; case MainPresenter.REQUEST_GAME_FILE: - FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.GAME_EXTENSIONS, + FileBrowserHelper.runAfterExtensionCheck(this, uri, + FileBrowserHelper.GAME_LIKE_EXTENSIONS, () -> EmulationActivity.launch(this, result.getData().toString())); break; 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 f3735eabea..82f16519fa 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 @@ -28,7 +28,14 @@ import java.util.Set; public final class FileBrowserHelper { public static final HashSet GAME_EXTENSIONS = new HashSet<>(Arrays.asList( - "gcm", "tgc", "iso", "ciso", "gcz", "wbfs", "wia", "rvz", "wad", "dol", "elf", "dff")); + "gcm", "tgc", "iso", "ciso", "gcz", "wbfs", "wia", "rvz", "wad", "dol", "elf")); + + public static final HashSet GAME_LIKE_EXTENSIONS = new HashSet<>(GAME_EXTENSIONS); + + static + { + GAME_LIKE_EXTENSIONS.add("dff"); + } public static final HashSet RAW_EXTENSION = new HashSet<>(Collections.singletonList( "raw")); From d78277c063d43e3234fe80a38d272500f3dfce3a Mon Sep 17 00:00:00 2001 From: JosJuice Date: Mon, 28 Dec 2020 13:25:24 +0100 Subject: [PATCH 11/11] 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