Merge pull request #9318 from JosJuice/android-saf-games

Android: Use storage access framework for game list
This commit is contained in:
JosJuice 2020-12-30 11:10:35 +01:00 committed by GitHub
commit c1d041b888
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 795 additions and 193 deletions

View File

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

View File

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

View File

@ -306,28 +306,17 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
mClickedPosition = position;
FilePicker filePicker = (FilePicker) item;
switch (filePicker.getRequestType())
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
case MainPresenter.REQUEST_SD_FILE:
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
{
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI,
filePicker.getSelectedValue(mView.getSettings()));
}
mView.getActivity().startActivityForResult(intent, filePicker.getRequestType());
break;
case MainPresenter.REQUEST_GAME_FILE:
FileBrowserHelper.openFilePicker(mView.getActivity(), filePicker.getRequestType(), false,
FileBrowserHelper.GAME_EXTENSIONS);
break;
default:
throw new InvalidParameterException("Unhandled request code");
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI,
filePicker.getSelectedValue(mView.getSettings()));
}
mView.getActivity().startActivityForResult(intent, filePicker.getRequestType());
}
public void onFilePickerConfirmation(String selectedFile)

View File

@ -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";

View File

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

View File

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

View File

@ -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;
}

View File

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

View File

@ -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;
}

View File

@ -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));
}
catch (SecurityException e)
{
Log.error("Tried to delete " + uri + " without permission");
return false;
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");
}
catch (Exception ignored)
{
}
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();

View File

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

View File

@ -438,6 +438,7 @@ It can efficiently compress both junk data and encrypted Wii data.
<string name="no_file_extension">The selected file does not appear to have a file name extension.\n\nContinue anyway?</string>
<string name="wrong_file_extension_single">The selected file has the file name extension \"%1$s\", but \"%2$s\" was expected.\n\nContinue anyway?</string>
<string name="wrong_file_extension_multiple">The selected file has the file name extension \"%1$s\", but one of these extensions was expected: %2$s\n\nContinue anyway?</string>
<string name="wrong_file_extension_in_directory">No compatible files were found in the selected location.\n\nThe supported formats are: %1$s</string>
<string name="unavailable_paths">Dolphin does not have permission to access one or more configured paths. Would you like to fix this before starting?</string>
<!-- Misc -->

View File

@ -4,6 +4,7 @@
#include "jni/AndroidCommon/AndroidCommon.h"
#include <ios>
#include <string>
#include <string_view>
#include <vector>
@ -43,6 +44,14 @@ std::vector<std::string> JStringArrayToVector(JNIEnv* env, jobjectArray array)
return result;
}
jobjectArray JStringArrayFromVector(JNIEnv* env, std::vector<std::string> vector)
{
jobjectArray result = env->NewObjectArray(vector.size(), IDCache::GetStringClass(), nullptr);
for (jsize i = 0; i < vector.size(); ++i)
env->SetObjectArrayElement(result, i, ToJString(env, vector[i]));
return result;
}
bool IsPathAndroidContent(const std::string& uri)
{
return StringBeginsWith(uri, "content://");
@ -66,6 +75,28 @@ std::string OpenModeToAndroid(std::string mode)
return mode;
}
std::string OpenModeToAndroid(std::ios_base::openmode mode)
{
std::string result;
if (mode & std::ios_base::in)
result += 'r';
if (mode & (std::ios_base::out | std::ios_base::app))
result += 'w';
if (mode & std::ios_base::app)
result += 'a';
constexpr std::ios_base::openmode t = std::ios_base::in | std::ios_base::trunc;
if ((mode & t) == t)
result += 't';
// The 'b' specifier is not supported. Since we're on POSIX, it's fine to just skip it.
return result;
}
int OpenAndroidContent(const std::string& uri, const std::string& mode)
{
JNIEnv* env = IDCache::GetEnvForThread();
@ -81,6 +112,43 @@ bool DeleteAndroidContent(const std::string& uri)
IDCache::GetContentHandlerDelete(), ToJString(env, uri));
}
jlong GetAndroidContentSizeAndIsDirectory(const std::string& uri)
{
JNIEnv* env = IDCache::GetEnvForThread();
return env->CallStaticLongMethod(IDCache::GetContentHandlerClass(),
IDCache::GetContentHandlerGetSizeAndIsDirectory(),
ToJString(env, uri));
}
std::string GetAndroidContentDisplayName(const std::string& uri)
{
JNIEnv* env = IDCache::GetEnvForThread();
jobject display_name =
env->CallStaticObjectMethod(IDCache::GetContentHandlerClass(),
IDCache::GetContentHandlerGetDisplayName(), ToJString(env, uri));
return display_name ? GetJString(env, reinterpret_cast<jstring>(display_name)) : "";
}
std::vector<std::string> GetAndroidContentChildNames(const std::string& uri)
{
JNIEnv* env = IDCache::GetEnvForThread();
jobject children = env->CallStaticObjectMethod(IDCache::GetContentHandlerClass(),
IDCache::GetContentHandlerGetChildNames(),
ToJString(env, uri), false);
return JStringArrayToVector(env, reinterpret_cast<jobjectArray>(children));
}
std::vector<std::string> DoFileSearchAndroidContent(const std::string& directory,
const std::vector<std::string>& extensions,
bool recursive)
{
JNIEnv* env = IDCache::GetEnvForThread();
jobject result = env->CallStaticObjectMethod(
IDCache::GetContentHandlerClass(), IDCache::GetContentHandlerDoFileSearch(),
ToJString(env, directory), JStringArrayFromVector(env, extensions), recursive);
return JStringArrayToVector(env, reinterpret_cast<jobjectArray>(result));
}
int GetNetworkIpAddress()
{
JNIEnv* env = IDCache::GetEnvForThread();

View File

@ -4,7 +4,9 @@
#pragma once
#include <ios>
#include <string>
#include <vector>
#include <jni.h>
@ -17,12 +19,29 @@ bool IsPathAndroidContent(const std::string& uri);
// Turns a C/C++ style mode (e.g. "rb") into one which can be used with OpenAndroidContent.
std::string OpenModeToAndroid(std::string mode);
std::string OpenModeToAndroid(std::ios_base::openmode mode);
// Opens a given file and returns a file descriptor.
int OpenAndroidContent(const std::string& uri, const std::string& mode);
// Deletes a given file.
bool DeleteAndroidContent(const std::string& uri);
// Returns -1 if not found, -2 if directory, file size otherwise.
jlong GetAndroidContentSizeAndIsDirectory(const std::string& uri);
// An unmangled URI (one which the C++ code has not appended anything to) can't be relied on
// to contain a file name at all. If a file name is desired, this function is the most reliable
// way to get it, but the display name is not guaranteed to always actually be like a file name.
// An empty string will be returned for files which do not exist.
std::string GetAndroidContentDisplayName(const std::string& uri);
// Returns the display names of all children of a directory, non-recursively.
std::vector<std::string> GetAndroidContentChildNames(const std::string& uri);
std::vector<std::string> DoFileSearchAndroidContent(const std::string& directory,
const std::vector<std::string>& extensions,
bool recursive);
int GetNetworkIpAddress();
int GetNetworkPrefixLength();
int GetNetworkGateway();

View File

@ -10,6 +10,8 @@ static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
static JavaVM* s_java_vm;
static jclass s_string_class;
static jclass s_native_library_class;
static jmethodID s_display_alert_msg;
static jmethodID s_do_rumble;
@ -44,6 +46,10 @@ static jmethodID s_compress_cb_run;
static jclass s_content_handler_class;
static jmethodID s_content_handler_open_fd;
static jmethodID s_content_handler_delete;
static jmethodID s_content_handler_get_size_and_is_directory;
static jmethodID s_content_handler_get_display_name;
static jmethodID s_content_handler_get_child_names;
static jmethodID s_content_handler_do_file_search;
static jclass s_network_helper_class;
static jmethodID s_network_helper_get_network_ip_address;
@ -75,6 +81,11 @@ JNIEnv* GetEnvForThread()
return owned.env;
}
jclass GetStringClass()
{
return s_string_class;
}
jclass GetNativeLibraryClass()
{
return s_native_library_class;
@ -210,6 +221,26 @@ jmethodID GetContentHandlerDelete()
return s_content_handler_delete;
}
jmethodID GetContentHandlerGetSizeAndIsDirectory()
{
return s_content_handler_get_size_and_is_directory;
}
jmethodID GetContentHandlerGetDisplayName()
{
return s_content_handler_get_display_name;
}
jmethodID GetContentHandlerGetChildNames()
{
return s_content_handler_get_child_names;
}
jmethodID GetContentHandlerDoFileSearch()
{
return s_content_handler_do_file_search;
}
jclass GetNetworkHelperClass()
{
return s_network_helper_class;
@ -229,6 +260,7 @@ jmethodID GetNetworkHelperGetNetworkGateway()
{
return s_network_helper_get_network_gateway;
}
} // namespace IDCache
#ifdef __cplusplus
@ -243,6 +275,9 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved)
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK)
return JNI_ERR;
const jclass string_class = env->FindClass("java/lang/String");
s_string_class = reinterpret_cast<jclass>(env->NewGlobalRef(string_class));
const jclass native_library_class = env->FindClass("org/dolphinemu/dolphinemu/NativeLibrary");
s_native_library_class = reinterpret_cast<jclass>(env->NewGlobalRef(native_library_class));
s_display_alert_msg = env->GetStaticMethodID(s_native_library_class, "displayAlertMsg",
@ -306,6 +341,15 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved)
"(Ljava/lang/String;Ljava/lang/String;)I");
s_content_handler_delete =
env->GetStaticMethodID(s_content_handler_class, "delete", "(Ljava/lang/String;)Z");
s_content_handler_get_size_and_is_directory = env->GetStaticMethodID(
s_content_handler_class, "getSizeAndIsDirectory", "(Ljava/lang/String;)J");
s_content_handler_get_display_name = env->GetStaticMethodID(
s_content_handler_class, "getDisplayName", "(Ljava/lang/String;)Ljava/lang/String;");
s_content_handler_get_child_names = env->GetStaticMethodID(
s_content_handler_class, "getChildNames", "(Ljava/lang/String;Z)[Ljava/lang/String;");
s_content_handler_do_file_search =
env->GetStaticMethodID(s_content_handler_class, "doFileSearch",
"(Ljava/lang/String;[Ljava/lang/String;Z)[Ljava/lang/String;");
const jclass network_helper_class =
env->FindClass("org/dolphinemu/dolphinemu/utils/NetworkHelper");

View File

@ -10,6 +10,8 @@ namespace IDCache
{
JNIEnv* GetEnvForThread();
jclass GetStringClass();
jclass GetNativeLibraryClass();
jmethodID GetDisplayAlertMsg();
jmethodID GetDoRumble();
@ -44,6 +46,10 @@ jmethodID GetCompressCallbackRun();
jclass GetContentHandlerClass();
jmethodID GetContentHandlerOpenFd();
jmethodID GetContentHandlerDelete();
jmethodID GetContentHandlerGetSizeAndIsDirectory();
jmethodID GetContentHandlerGetDisplayName();
jmethodID GetContentHandlerGetChildNames();
jmethodID GetContentHandlerDoFileSearch();
jclass GetNetworkHelperClass();
jmethodID GetNetworkHelperGetNetworkIpAddress();

View File

@ -4,6 +4,7 @@
#include <algorithm>
#include <functional>
#include <iterator>
#include "Common/CommonPaths.h"
#include "Common/FileSearch.h"
@ -15,6 +16,10 @@
namespace fs = std::filesystem;
#define HAS_STD_FILESYSTEM
#else
#ifdef ANDROID
#include "jni/AndroidCommon/AndroidCommon.h"
#endif
#include <cstring>
#include "Common/CommonFuncs.h"
#include "Common/FileUtil.h"
@ -24,36 +29,30 @@ namespace Common
{
#ifndef HAS_STD_FILESYSTEM
static std::vector<std::string>
FileSearchWithTest(const std::vector<std::string>& directories, bool recursive,
std::function<bool(const File::FSTEntry&)> callback)
static void FileSearchWithTest(const std::string& directory, bool recursive,
std::vector<std::string>* result_out,
std::function<bool(const File::FSTEntry&)> callback)
{
std::vector<std::string> result;
for (const std::string& directory : directories)
{
File::FSTEntry top = File::ScanDirectoryTree(directory, recursive);
File::FSTEntry top = File::ScanDirectoryTree(directory, recursive);
std::function<void(File::FSTEntry&)> DoEntry;
DoEntry = [&](File::FSTEntry& entry) {
if (callback(entry))
result.push_back(entry.physicalName);
for (auto& child : entry.children)
DoEntry(child);
};
for (auto& child : top.children)
const std::function<void(File::FSTEntry&)> DoEntry = [&](File::FSTEntry& entry) {
if (callback(entry))
result_out->push_back(entry.physicalName);
for (auto& child : entry.children)
DoEntry(child);
}
// remove duplicates
std::sort(result.begin(), result.end());
result.erase(std::unique(result.begin(), result.end()), result.end());
return result;
};
for (auto& child : top.children)
DoEntry(child);
}
std::vector<std::string> DoFileSearch(const std::vector<std::string>& directories,
const std::vector<std::string>& exts, bool recursive)
{
std::vector<std::string> result;
bool accept_all = exts.empty();
return FileSearchWithTest(directories, recursive, [&](const File::FSTEntry& entry) {
const auto callback = [&exts, accept_all](const File::FSTEntry& entry) {
if (accept_all)
return true;
if (entry.isDirectory)
@ -63,7 +62,34 @@ std::vector<std::string> DoFileSearch(const std::vector<std::string>& directorie
return name.length() >= ext.length() &&
strcasecmp(name.c_str() + name.length() - ext.length(), ext.c_str()) == 0;
});
});
};
for (const std::string& directory : directories)
{
#ifdef ANDROID
// While File::ScanDirectoryTree (which is called in FileSearchWithTest) does handle Android
// content correctly, having a specialized implementation of DoFileSearch for Android content
// provides a much needed performance boost. Also, this specialized implementation will be
// required if we in the future replace the use of File::ScanDirectoryTree with std::filesystem.
if (IsPathAndroidContent(directory))
{
const std::vector<std::string> partial_result =
DoFileSearchAndroidContent(directory, exts, recursive);
result.insert(result.end(), std::make_move_iterator(partial_result.begin()),
std::make_move_iterator(partial_result.end()));
}
else
#endif
{
FileSearchWithTest(directory, recursive, &result, callback);
}
}
// remove duplicates
std::sort(result.begin(), result.end());
result.erase(std::unique(result.begin(), result.end()), result.end());
return result;
}
#else

View File

@ -78,19 +78,40 @@ FileInfo::FileInfo(const char* path) : FileInfo(std::string(path))
#else
FileInfo::FileInfo(const std::string& path) : FileInfo(path.c_str())
{
#ifdef ANDROID
if (IsPathAndroidContent(path))
AndroidContentInit(path);
else
#endif
m_exists = stat(path.c_str(), &m_stat) == 0;
}
FileInfo::FileInfo(const char* path)
{
m_exists = stat(path, &m_stat) == 0;
#ifdef ANDROID
if (IsPathAndroidContent(path))
AndroidContentInit(path);
else
#endif
m_exists = stat(path, &m_stat) == 0;
}
#endif
FileInfo::FileInfo(int fd)
{
m_exists = fstat(fd, &m_stat);
m_exists = fstat(fd, &m_stat) == 0;
}
#ifdef ANDROID
void FileInfo::AndroidContentInit(const std::string& path)
{
const jlong result = GetAndroidContentSizeAndIsDirectory(path);
m_exists = result != -1;
m_stat.st_mode = result == -2 ? S_IFDIR : S_IFREG;
m_stat.st_size = result >= 0 ? result : 0;
}
#endif
bool FileInfo::Exists() const
{
return m_exists;
@ -476,14 +497,47 @@ FSTEntry ScanDirectoryTree(const std::string& directory, bool recursive)
{
const std::string virtual_name(TStrToUTF8(ffd.cFileName));
#else
DIR* dirp = opendir(directory.c_str());
if (!dirp)
return parent_entry;
DIR* dirp = nullptr;
#ifdef ANDROID
std::vector<std::string> child_names;
if (IsPathAndroidContent(directory))
{
child_names = GetAndroidContentChildNames(directory);
}
else
#endif
{
dirp = opendir(directory.c_str());
if (!dirp)
return parent_entry;
}
#ifdef ANDROID
auto it = child_names.cbegin();
#endif
// non Windows loop
while (dirent* result = readdir(dirp))
while (true)
{
const std::string virtual_name(result->d_name);
std::string virtual_name;
#ifdef ANDROID
if (!dirp)
{
if (it == child_names.cend())
break;
virtual_name = *it;
++it;
}
else
#endif
{
dirent* result = readdir(dirp);
if (!result)
break;
virtual_name = result->d_name;
}
#endif
if (virtual_name == "." || virtual_name == "..")
continue;
@ -514,7 +568,8 @@ FSTEntry ScanDirectoryTree(const std::string& directory, bool recursive)
FindClose(hFind);
#else
}
closedir(dirp);
if (dirp)
closedir(dirp);
#endif
return parent_entry;

View File

@ -18,6 +18,11 @@
#include "Common/StringUtil.h"
#endif
#ifdef ANDROID
#include "Common/StringUtil.h"
#include "jni/AndroidCommon/AndroidCommon.h"
#endif
// User directory indices for GetUserPath
enum
{
@ -109,6 +114,10 @@ public:
u64 GetSize() const;
private:
#ifdef ANDROID
void AndroidContentInit(const std::string& path);
#endif
struct stat m_stat;
bool m_exists;
};
@ -214,14 +223,20 @@ std::string GetExeDirectory();
bool WriteStringToFile(const std::string& filename, std::string_view str);
bool ReadFileToString(const std::string& filename, std::string& str);
// To deal with Windows being dumb at unicode:
// To deal with Windows not fully supporting UTF-8 and Android not fully supporting paths.
template <typename T>
void OpenFStream(T& fstream, const std::string& filename, std::ios_base::openmode openmode)
{
#ifdef _WIN32
fstream.open(UTF8ToTStr(filename).c_str(), openmode);
#else
fstream.open(filename.c_str(), openmode);
#ifdef ANDROID
// Unfortunately it seems like the non-standard __open is the only way to use a file descriptor
if (IsPathAndroidContent(filename))
fstream.__open(OpenAndroidContent(filename, OpenModeToAndroid(openmode)), openmode);
else
#endif
fstream.open(filename.c_str(), openmode);
#endif
}

View File

@ -158,6 +158,15 @@ BootParameters::GenerateFromFile(std::vector<std::string> paths,
if (paths.size() == 1)
paths.clear();
#ifdef ANDROID
if (extension.empty() && IsPathAndroidContent(path))
{
const std::string display_name = GetAndroidContentDisplayName(path);
SplitPath(display_name, nullptr, nullptr, &extension);
std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
}
#endif
static const std::unordered_set<std::string> disc_image_extensions = {
{".gcm", ".iso", ".tgc", ".wbfs", ".ciso", ".gcz", ".wia", ".rvz", ".dol", ".elf"}};
if (disc_image_extensions.find(extension) != disc_image_extensions.end() || is_drive)