From 64afe97491c1b713c18a79e4cd18a0cca732ade1 Mon Sep 17 00:00:00 2001 From: Ryan Meredith Date: Thu, 29 Oct 2020 15:06:55 -0400 Subject: Android: Convert ISOPaths to INI settings --- .../features/settings/utils/SettingsFile.java | 3 + .../dolphinemu/dolphinemu/model/GameFileCache.java | 102 +++++++++++---------- .../dolphinemu/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(-) (limited to 'Source/Android/app/src/main/java') 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... -- cgit v1.2.3 From a7c05d7e84df5949e8edf42fa6332d6aa58282f6 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Thu, 5 Nov 2020 19:47:23 +0100 Subject: Android: Add content provider support to File::FileInfo --- .../dolphinemu/utils/ContentHandler.java | 25 ++++++++++++++++++++++ 1 file changed, 25 insertions(+) (limited to 'Source/Android/app/src/main/java') 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) { -- cgit v1.2.3 From e60665da94917812313f1ca4b96b7a03fd5b4a1c Mon Sep 17 00:00:00 2001 From: JosJuice Date: Thu, 5 Nov 2020 19:29:13 +0100 Subject: Android: Use storage access framework for picking single games --- .../dolphinemu/activities/EmulationActivity.java | 18 ++++++++----- .../features/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(-) (limited to 'Source/Android/app/src/main/java') 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) - { - intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, - filePicker.getSelectedValue(mView.getSettings())); - } + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); - 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"); + 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()); } 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()); -- cgit v1.2.3 From 525268f043a25116dbb5e362d9e3e84b3f4ca5e5 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 8 Nov 2020 15:39:17 +0100 Subject: Android: Fix opening games with extensionless URI --- .../main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'Source/Android/app/src/main/java') 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) { -- cgit v1.2.3 From 2126f62111a95e3a2386467747e5bf94b9943e1d Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 8 Nov 2020 16:57:49 +0100 Subject: Android: Add content provider support to File::ScanDirectoryTree --- .../dolphinemu/utils/ContentHandler.java | 209 +++++++++++++++++++-- 1 file changed, 193 insertions(+), 16 deletions(-) (limited to 'Source/Android/app/src/main/java') 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(); -- cgit v1.2.3 From ae8de3510533d398869cde03d59a5bf8f27f2ca7 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 8 Nov 2020 18:41:01 +0100 Subject: Android: Use storage access framework for game list --- .../dolphinemu/dolphinemu/model/GameFileCache.java | 4 ++-- .../dolphinemu/dolphinemu/ui/main/MainActivity.java | 5 +++-- .../dolphinemu/dolphinemu/ui/main/MainPresenter.java | 19 +++++++++++++++++-- .../dolphinemu/dolphinemu/ui/main/TvMainActivity.java | 5 +++-- 4 files changed, 25 insertions(+), 8 deletions(-) (limited to 'Source/Android/app/src/main/java') 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: -- cgit v1.2.3 From 399ede37a66a161e2379df34501e2b9a172d3457 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 8 Nov 2020 20:37:56 +0100 Subject: Android: Catch all exceptions in ContentHandler --- .../dolphinemu/utils/ContentHandler.java | 52 ++++++++++++++++------ 1 file changed, 38 insertions(+), 14 deletions(-) (limited to 'Source/Android/app/src/main/java') 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 (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"); - return false; } - catch (FileNotFoundException e) + catch (Exception ignored) { - // Return true because we care about the file not being there, not the actual delete. - return true; } + + 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); } -- cgit v1.2.3 From 73855168f3f57f8a3037223235740eb7d9dd76e4 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 8 Nov 2020 23:01:59 +0100 Subject: 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 ++++--- 3 files changed, 63 insertions(+), 18 deletions(-) (limited to 'Source/Android/app/src/main/java') 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) { -- cgit v1.2.3 From 01b964b01aad3f46611e5fec8c44b60ed04a9b3f Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 8 Nov 2020 23:50:30 +0100 Subject: Android: Don't consider .dff files valid for game list --- .../java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java | 3 ++- .../java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java | 3 ++- .../java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java | 9 ++++++++- 3 files changed, 12 insertions(+), 3 deletions(-) (limited to 'Source/Android/app/src/main/java') 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")); -- cgit v1.2.3 From d78277c063d43e3234fe80a38d272500f3dfce3a Mon Sep 17 00:00:00 2001 From: JosJuice Date: Mon, 28 Dec 2020 13:25:24 +0100 Subject: Android: Add specialized content provider implementation of DoFileSearch --- .../dolphinemu/ui/main/MainPresenter.java | 4 +- .../dolphinemu/utils/ContentHandler.java | 90 ++++++++++++++++++---- .../dolphinemu/utils/FileBrowserHelper.java | 10 ++- 3 files changed, 84 insertions(+), 20 deletions(-) (limited to 'Source/Android/app/src/main/java') 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]); + } + + @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 getChildNames(@NonNull Uri uri, @NonNull String documentId, boolean recursive, - List resultOut) + 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) -- cgit v1.2.3