summaryrefslogtreecommitdiff
path: root/Source/Android/app/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'Source/Android/app/src/main/java')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java18
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.java16
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java29
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/utils/SettingsFile.java3
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/model/GameFileCache.java104
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/services/GameFileCacheService.java9
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java24
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java38
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java26
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java349
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java63
11 files changed, 519 insertions, 160 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 2d7688bbaf..9cb2406cb1 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;
@@ -164,6 +165,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)
@@ -410,11 +416,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());
}
}
}
@@ -639,8 +641,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 02c2040f7b..df21cec99c 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
@@ -25,6 +25,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";
@@ -179,13 +181,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<String> 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<SettingViewHolde
mClickedPosition = position;
FilePicker filePicker = (FilePicker) item;
- switch (filePicker.getRequestType())
- {
- 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()));
- }
+ 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/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..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
@@ -1,22 +1,19 @@
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.ContentHandler;
+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<String> EMPTY_SET = new HashSet<>();
-
@Keep
private long mPointer;
@@ -30,50 +27,71 @@ 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<String> folderPaths = preferences.getStringSet(GAME_FOLDER_PATHS_PREFERENCE, EMPTY_SET);
+ File dolphinFile = SettingsFile.getSettingsFile(Settings.FILE_DOLPHIN);
+ IniFile dolphinIni = new IniFile(dolphinFile);
+ LinkedHashSet<String> 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<String> 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<String> getPathSet(boolean removeNonExistentFolders)
{
- SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
- Set<String> folderPaths = preferences.getStringSet(GAME_FOLDER_PATHS_PREFERENCE, EMPTY_SET);
+ File dolphinFile = SettingsFile.getSettingsFile(Settings.FILE_DOLPHIN);
+ IniFile dolphinIni = new IniFile(dolphinFile);
+ LinkedHashSet<String> 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<String> newFolderPaths = new HashSet<>();
- for (String folderPath : folderPaths)
- {
- File folder = new File(folderPath);
- if (folder.exists())
+ if (path.startsWith("content://") ? ContentHandler.exists(path) : new File(path).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<String> folderPathsSet = preferences.getStringSet(GAME_FOLDER_PATHS_PREFERENCE, EMPTY_SET);
-
- if (folderPathsSet == null)
- {
- return false;
- }
+ LinkedHashSet<String> 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<GameFile[]> gameFiles = new AtomicReference<>(new GameFile[]{});
- private static AtomicBoolean hasLoadedCache = new AtomicBoolean(false);
- private static AtomicBoolean hasScannedLibrary = new AtomicBoolean(false);
+ private static final AtomicReference<GameFile[]> 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 bd95809f63..e5150758d4 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;
@@ -45,7 +46,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 +86,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...
@@ -162,14 +163,17 @@ 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
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,19 +198,21 @@ 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:
- mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedPath(result));
+ mPresenter.onDirectorySelected(result);
break;
case MainPresenter.REQUEST_GAME_FILE:
- EmulationActivity.launch(this, FileBrowserHelper.getSelectedFiles(result));
+ FileBrowserHelper.runAfterExtensionCheck(this, uri,
+ FileBrowserHelper.GAME_LIKE_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/MainPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainPresenter.java
index 8db0ee44a1..cc06689c9b 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;
@@ -13,10 +15,16 @@ 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
{
@@ -95,18 +103,40 @@ 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;
}
}
- public void onDirectorySelected(String dir)
+ public void onDirectorySelected(Intent result)
{
- mDirToAdd = dir;
+ 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, false))))
+ {
+ 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;
+
+ 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 c7bc923461..1141b98dbf 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;
@@ -39,11 +40,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<ArrayObjectAdapter> mGameRows = new ArrayList<>();
+ private final ArrayList<ArrayObjectAdapter> mGameRows = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState)
@@ -73,7 +74,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...
@@ -167,14 +168,17 @@ 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
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,19 +222,21 @@ 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:
- mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedPath(result));
+ mPresenter.onDirectorySelected(result);
break;
case MainPresenter.REQUEST_GAME_FILE:
- EmulationActivity.launch(this, FileBrowserHelper.getSelectedFiles(result));
+ FileBrowserHelper.runAfterExtensionCheck(this, uri,
+ FileBrowserHelper.GAME_LIKE_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/ContentHandler.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java
index dbeb410079..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
@@ -14,54 +14,76 @@ import androidx.annotation.Keep;
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:
+
+ 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
- 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)
{
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
- 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 (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)
{
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,15 +92,65 @@ public class ContentHandler
{
Log.error("Tried to check if " + uri + " exists without permission");
}
+ catch (Exception ignored)
+ {
+ }
return false;
}
+ /**
+ * @return -1 if not found, -2 if directory, file size otherwise
+ */
+ @Keep
+ public static long getSizeAndIsDirectory(@NonNull String uri)
+ {
+ try
+ {
+ 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 (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 (Exception ignored)
+ {
+ }
+
+ return -1;
+ }
+
+ @Nullable @Keep
+ public static String getDisplayName(@NonNull String uri)
+ {
+ try
+ {
+ return getDisplayName(unmangle(uri));
+ }
+ catch (Exception ignored)
+ {
+ }
+
+ 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())
{
@@ -89,10 +161,257 @@ public class ContentHandler
{
Log.error("Tried to get display name of " + uri + " without permission");
}
+ catch (Exception ignored)
+ {
+ }
return null;
}
+ @NonNull @Keep
+ public static String[] getChildNames(@NonNull String uri, boolean recursive)
+ {
+ try
+ {
+ return getChildNames(unmangle(uri), recursive);
+ }
+ catch (Exception ignored)
+ {
+ }
+
+ return new String[0];
+ }
+
+ @NonNull
+ public static String[] getChildNames(@NonNull Uri uri, boolean recursive)
+ {
+ ArrayList<String> result = new ArrayList<>();
+
+ 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<String> result = new ArrayList<>();
+
+ try
+ {
+ Uri uri = unmangle(directory);
+ String documentId = DocumentsContract.getDocumentId(treeToDocument(uri));
+ boolean acceptAll = extensions.length == 0;
+ Predicate<String> extensionCheck = (displayName) ->
+ {
+ String extension = FileBrowserHelper.getExtension(displayName, true);
+ return extension != null && Arrays.stream(extensions).anyMatch(extension::equalsIgnoreCase);
+ };
+ doFileSearch(uri, directory, documentId, recursive, result, acceptAll, extensionCheck);
+ }
+ catch (Exception ignored)
+ {
+ }
+
+ return result.toArray(new String[0]);
+ }
+
+ private static void doFileSearch(@NonNull Uri baseUri, @NonNull String path,
+ @NonNull String documentId, boolean recursive, @NonNull List<String> resultOut,
+ boolean acceptAll, @NonNull Predicate<String> extensionCheck)
+ {
+ forEachChild(baseUri, documentId, (displayName, childDocumentId, isDirectory) ->
+ {
+ String childPath = path + '/' + displayName;
+ if (acceptAll || (!isDirectory && extensionCheck.test(displayName)))
+ {
+ resultOut.add(childPath);
+ }
+ if (recursive && isDirectory)
+ {
+ doFileSearch(baseUri, childPath, childDocumentId, recursive, resultOut, acceptAll,
+ extensionCheck);
+ }
+ });
+ }
+
+ private interface ForEachChildCallback
+ {
+ void run(String displayName, String documentId, boolean isDirectory);
+ }
+
+ private static void forEachChild(@NonNull Uri uri, @NonNull String documentId,
+ @NonNull ForEachChildCallback callback)
+ {
+ try
+ {
+ Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, documentId);
+
+ final String[] projection = 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())
+ {
+ callback.run(cursor.getString(0), cursor.getString(2),
+ Document.MIME_TYPE_DIR.equals(cursor.getString(1)));
+ }
+ }
+ }
+ }
+ catch (SecurityException e)
+ {
+ Log.error("Tried to get children of " + uri + " without permission");
+ }
+ catch (Exception ignored)
+ {
+ }
+ }
+
+ @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");
+ }
+ catch (Exception ignored)
+ {
+ }
+
+ 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<String> 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/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..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
@@ -28,7 +28,14 @@ import java.util.Set;
public final class FileBrowserHelper
{
public static final HashSet<String> 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<String> GAME_LIKE_EXTENSIONS = new HashSet<>(GAME_EXTENSIONS);
+
+ static
+ {
+ GAME_LIKE_EXTENSIONS.add("dff");
+ }
public static final HashSet<String> RAW_EXTENSION = new HashSet<>(Collections.singletonList(
"raw"));
@@ -50,21 +57,6 @@ public final class FileBrowserHelper
activity.startActivityForResult(i, MainPresenter.REQUEST_DIRECTORY);
}
- public static void openFilePicker(FragmentActivity activity, int requestCode, boolean allowMulti,
- HashSet<String> 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 +71,6 @@ public final class FileBrowserHelper
return null;
}
- @Nullable
- public static String[] getSelectedFiles(Intent result)
- {
- // Use the provided utility method to parse the result
- List<Uri> 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());
@@ -112,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))
{
@@ -133,10 +109,8 @@ public final class FileBrowserHelper
int messageId = validExtensions.size() == 1 ?
R.string.wrong_file_extension_single : R.string.wrong_file_extension_multiple;
- ArrayList<String> 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)
@@ -148,13 +122,22 @@ public final class FileBrowserHelper
}
@Nullable
- private static String getExtension(@Nullable String fileName)
+ public static String getExtension(@Nullable String fileName, boolean includeDot)
{
if (fileName == null)
return null;
int dotIndex = fileName.lastIndexOf(".");
- return dotIndex != -1 ? fileName.substring(dotIndex + 1) : null;
+ if (dotIndex == -1)
+ return null;
+ return fileName.substring(dotIndex + (includeDot ? 0 : 1));
+ }
+
+ public static String setToSortedDelimitedString(Set<String> set)
+ {
+ ArrayList<String> list = new ArrayList<>(set);
+ Collections.sort(list);
+ return join(", ", list);
}
// TODO: Replace this with String.join once we can use Java 8