Merge pull request #9318 from JosJuice/android-saf-games
Android: Use storage access framework for game list
This commit is contained in:
commit
c1d041b888
|
@ -45,6 +45,7 @@ import org.dolphinemu.dolphinemu.fragments.SaveLoadStateFragment;
|
||||||
import org.dolphinemu.dolphinemu.overlay.InputOverlay;
|
import org.dolphinemu.dolphinemu.overlay.InputOverlay;
|
||||||
import org.dolphinemu.dolphinemu.overlay.InputOverlayPointer;
|
import org.dolphinemu.dolphinemu.overlay.InputOverlayPointer;
|
||||||
import org.dolphinemu.dolphinemu.ui.main.MainActivity;
|
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.ui.main.TvMainActivity;
|
||||||
import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner;
|
import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner;
|
||||||
import org.dolphinemu.dolphinemu.utils.ControllerMappingHelper;
|
import org.dolphinemu.dolphinemu.utils.ControllerMappingHelper;
|
||||||
|
@ -164,6 +165,11 @@ public final class EmulationActivity extends AppCompatActivity
|
||||||
EmulationActivity.MENU_ACTION_MOTION_CONTROLS);
|
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)
|
public static void launch(FragmentActivity activity, String[] filePaths)
|
||||||
{
|
{
|
||||||
if (sIgnoreLaunchRequests)
|
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 the user picked a file, as opposed to just backing out.
|
||||||
if (resultCode == MainActivity.RESULT_OK)
|
if (resultCode == MainActivity.RESULT_OK)
|
||||||
{
|
{
|
||||||
String newDiscPath = FileBrowserHelper.getSelectedPath(result);
|
NativeLibrary.ChangeDisc(result.getData().toString());
|
||||||
if (!TextUtils.isEmpty(newDiscPath))
|
|
||||||
{
|
|
||||||
NativeLibrary.ChangeDisc(newDiscPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -639,8 +641,10 @@ public final class EmulationActivity extends AppCompatActivity
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MENU_ACTION_CHANGE_DISC:
|
case MENU_ACTION_CHANGE_DISC:
|
||||||
FileBrowserHelper.openFilePicker(this, REQUEST_CHANGE_DISC, false,
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||||
FileBrowserHelper.GAME_EXTENSIONS);
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
intent.setType("*/*");
|
||||||
|
startActivityForResult(intent, REQUEST_CHANGE_DISC);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MENU_SET_IR_SENSITIVITY:
|
case MENU_SET_IR_SENSITIVITY:
|
||||||
|
|
|
@ -25,6 +25,8 @@ import org.dolphinemu.dolphinemu.ui.main.TvMainActivity;
|
||||||
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;
|
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;
|
||||||
import org.dolphinemu.dolphinemu.utils.TvUtil;
|
import org.dolphinemu.dolphinemu.utils.TvUtil;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public final class SettingsActivity extends AppCompatActivity implements SettingsActivityView
|
public final class SettingsActivity extends AppCompatActivity implements SettingsActivityView
|
||||||
{
|
{
|
||||||
private static final String ARG_MENU_TAG = "menu_tag";
|
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 the user picked a file, as opposed to just backing out.
|
||||||
if (resultCode == MainActivity.RESULT_OK)
|
if (resultCode == MainActivity.RESULT_OK)
|
||||||
{
|
{
|
||||||
if (requestCode == MainPresenter.REQUEST_SD_FILE)
|
if (requestCode != MainPresenter.REQUEST_DIRECTORY)
|
||||||
{
|
{
|
||||||
Uri uri = canonicalizeIfPossible(result.getData());
|
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);
|
getContentResolver().takePersistableUriPermission(uri, takeFlags);
|
||||||
getFragment().getAdapter().onFilePickerConfirmation(uri.toString());
|
getFragment().getAdapter().onFilePickerConfirmation(uri.toString());
|
||||||
|
|
|
@ -306,28 +306,17 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
|
||||||
mClickedPosition = position;
|
mClickedPosition = position;
|
||||||
FilePicker filePicker = (FilePicker) item;
|
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.putExtra(DocumentsContract.EXTRA_INITIAL_URI,
|
||||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
filePicker.getSelectedValue(mView.getSettings()));
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mView.getActivity().startActivityForResult(intent, filePicker.getRequestType());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onFilePickerConfirmation(String selectedFile)
|
public void onFilePickerConfirmation(String selectedFile)
|
||||||
|
|
|
@ -16,6 +16,9 @@ import java.io.File;
|
||||||
*/
|
*/
|
||||||
public final class SettingsFile
|
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_TYPE = "SIDevice";
|
||||||
public static final String KEY_GCPAD_PLAYER_1 = "SIDevice0";
|
public static final String KEY_GCPAD_PLAYER_1 = "SIDevice0";
|
||||||
public static final String KEY_GCPAD_G_TYPE = "PadType";
|
public static final String KEY_GCPAD_G_TYPE = "PadType";
|
||||||
|
|
|
@ -1,22 +1,19 @@
|
||||||
package org.dolphinemu.dolphinemu.model;
|
package org.dolphinemu.dolphinemu.model;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
|
|
||||||
import androidx.annotation.Keep;
|
import androidx.annotation.Keep;
|
||||||
|
|
||||||
|
import org.dolphinemu.dolphinemu.NativeLibrary;
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting;
|
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.io.File;
|
||||||
import java.util.HashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public class GameFileCache
|
public class GameFileCache
|
||||||
{
|
{
|
||||||
private static final String GAME_FOLDER_PATHS_PREFERENCE = "gameFolderPaths";
|
|
||||||
private static final Set<String> EMPTY_SET = new HashSet<>();
|
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
private long mPointer;
|
private long mPointer;
|
||||||
|
|
||||||
|
@ -30,50 +27,71 @@ public class GameFileCache
|
||||||
@Override
|
@Override
|
||||||
public native void finalize();
|
public native void finalize();
|
||||||
|
|
||||||
public static void addGameFolder(String path, Context context)
|
public static void addGameFolder(String path)
|
||||||
{
|
{
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
File dolphinFile = SettingsFile.getSettingsFile(Settings.FILE_DOLPHIN);
|
||||||
Set<String> folderPaths = preferences.getStringSet(GAME_FOLDER_PATHS_PREFERENCE, EMPTY_SET);
|
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);
|
File dolphinFile = SettingsFile.getSettingsFile(Settings.FILE_DOLPHIN);
|
||||||
Set<String> folderPaths = preferences.getStringSet(GAME_FOLDER_PATHS_PREFERENCE, EMPTY_SET);
|
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<>();
|
if (path.startsWith("content://") ? ContentHandler.exists(path) : new File(path).exists())
|
||||||
for (String folderPath : folderPaths)
|
|
||||||
{
|
|
||||||
File folder = new File(folderPath);
|
|
||||||
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
|
int setIndex = 0;
|
||||||
SharedPreferences.Editor editor = preferences.edit();
|
|
||||||
editor.putStringSet(GAME_FOLDER_PATHS_PREFERENCE, newFolderPaths);
|
dolphinIni.setInt(Settings.SECTION_INI_GENERAL, SettingsFile.KEY_ISO_PATHS,
|
||||||
editor.apply();
|
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
|
* @return true if the cache was modified
|
||||||
*/
|
*/
|
||||||
public boolean scanLibrary(Context context)
|
public boolean scanLibrary()
|
||||||
{
|
{
|
||||||
boolean recursiveScan = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.getBooleanGlobal();
|
boolean recursiveScan = BooleanSetting.MAIN_RECURSIVE_ISO_PATHS.getBooleanGlobal();
|
||||||
|
|
||||||
removeNonExistentGameFolders(context);
|
LinkedHashSet<String> folderPathsSet = getPathSet(true);
|
||||||
|
|
||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
Set<String> folderPathsSet = preferences.getStringSet(GAME_FOLDER_PATHS_PREFERENCE, EMPTY_SET);
|
|
||||||
|
|
||||||
if (folderPathsSet == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] folderPaths = folderPathsSet.toArray(new String[0]);
|
String[] folderPaths = folderPathsSet.toArray(new String[0]);
|
||||||
|
|
||||||
|
|
|
@ -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 final String ACTION_RESCAN = "org.dolphinemu.dolphinemu.RESCAN_GAME_FILE_CACHE";
|
||||||
|
|
||||||
private static GameFileCache gameFileCache = null;
|
private static GameFileCache gameFileCache = null;
|
||||||
private static AtomicReference<GameFile[]> gameFiles = new AtomicReference<>(new GameFile[]{});
|
private static final AtomicReference<GameFile[]> gameFiles =
|
||||||
private static AtomicBoolean hasLoadedCache = new AtomicBoolean(false);
|
new AtomicReference<>(new GameFile[]{});
|
||||||
private static AtomicBoolean hasScannedLibrary = new AtomicBoolean(false);
|
private static final AtomicBoolean hasLoadedCache = new AtomicBoolean(false);
|
||||||
|
private static final AtomicBoolean hasScannedLibrary = new AtomicBoolean(false);
|
||||||
|
|
||||||
public GameFileCacheService()
|
public GameFileCacheService()
|
||||||
{
|
{
|
||||||
|
@ -166,7 +167,7 @@ public final class GameFileCacheService extends IntentService
|
||||||
{
|
{
|
||||||
synchronized (gameFileCache)
|
synchronized (gameFileCache)
|
||||||
{
|
{
|
||||||
boolean changed = gameFileCache.scanLibrary(this);
|
boolean changed = gameFileCache.scanLibrary();
|
||||||
if (changed)
|
if (changed)
|
||||||
updateGameFileArray();
|
updateGameFileArray();
|
||||||
hasScannedLibrary.set(true);
|
hasScannedLibrary.set(true);
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.dolphinemu.dolphinemu.ui.main;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
|
@ -45,7 +46,7 @@ public final class MainActivity extends AppCompatActivity implements MainView
|
||||||
private FloatingActionButton mFab;
|
private FloatingActionButton mFab;
|
||||||
private static boolean sShouldRescanLibrary = true;
|
private static boolean sShouldRescanLibrary = true;
|
||||||
|
|
||||||
private MainPresenter mPresenter = new MainPresenter(this, this);
|
private final MainPresenter mPresenter = new MainPresenter(this, this);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState)
|
protected void onCreate(Bundle savedInstanceState)
|
||||||
|
@ -85,7 +86,7 @@ public final class MainActivity extends AppCompatActivity implements MainView
|
||||||
.run(this, false, this::setPlatformTabsAndStartGameFileCacheService);
|
.run(this, false, this::setPlatformTabsAndStartGameFileCacheService);
|
||||||
}
|
}
|
||||||
|
|
||||||
mPresenter.addDirIfNeeded(this);
|
mPresenter.addDirIfNeeded();
|
||||||
|
|
||||||
// In case the user changed a setting that affects how games are displayed,
|
// In case the user changed a setting that affects how games are displayed,
|
||||||
// such as system language, cover downloading...
|
// such as system language, cover downloading...
|
||||||
|
@ -162,14 +163,17 @@ public final class MainActivity extends AppCompatActivity implements MainView
|
||||||
@Override
|
@Override
|
||||||
public void launchFileListActivity()
|
public void launchFileListActivity()
|
||||||
{
|
{
|
||||||
FileBrowserHelper.openDirectoryPicker(this, FileBrowserHelper.GAME_EXTENSIONS);
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
|
startActivityForResult(intent, MainPresenter.REQUEST_DIRECTORY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void launchOpenFileActivity()
|
public void launchOpenFileActivity()
|
||||||
{
|
{
|
||||||
FileBrowserHelper.openFilePicker(this, MainPresenter.REQUEST_GAME_FILE, false,
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||||
FileBrowserHelper.GAME_EXTENSIONS);
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
intent.setType("*/*");
|
||||||
|
startActivityForResult(intent, MainPresenter.REQUEST_GAME_FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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 the user picked a file, as opposed to just backing out.
|
||||||
if (resultCode == MainActivity.RESULT_OK)
|
if (resultCode == MainActivity.RESULT_OK)
|
||||||
{
|
{
|
||||||
|
Uri uri = result.getData();
|
||||||
switch (requestCode)
|
switch (requestCode)
|
||||||
{
|
{
|
||||||
case MainPresenter.REQUEST_DIRECTORY:
|
case MainPresenter.REQUEST_DIRECTORY:
|
||||||
mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedPath(result));
|
mPresenter.onDirectorySelected(result);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MainPresenter.REQUEST_GAME_FILE:
|
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;
|
break;
|
||||||
|
|
||||||
case MainPresenter.REQUEST_WAD_FILE:
|
case MainPresenter.REQUEST_WAD_FILE:
|
||||||
FileBrowserHelper.runAfterExtensionCheck(this, result.getData(),
|
FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.WAD_EXTENSION,
|
||||||
FileBrowserHelper.WAD_EXTENSION,
|
|
||||||
() -> mPresenter.installWAD(result.getData().toString()));
|
() -> mPresenter.installWAD(result.getData().toString()));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,11 @@ package org.dolphinemu.dolphinemu.ui.main;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.BroadcastReceiver;
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
|
import android.net.Uri;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
@ -13,10 +15,16 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
import org.dolphinemu.dolphinemu.BuildConfig;
|
import org.dolphinemu.dolphinemu.BuildConfig;
|
||||||
import org.dolphinemu.dolphinemu.NativeLibrary;
|
import org.dolphinemu.dolphinemu.NativeLibrary;
|
||||||
import org.dolphinemu.dolphinemu.R;
|
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.features.settings.ui.MenuTag;
|
||||||
import org.dolphinemu.dolphinemu.model.GameFileCache;
|
import org.dolphinemu.dolphinemu.model.GameFileCache;
|
||||||
import org.dolphinemu.dolphinemu.services.GameFileCacheService;
|
import org.dolphinemu.dolphinemu.services.GameFileCacheService;
|
||||||
import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner;
|
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
|
public final class MainPresenter
|
||||||
{
|
{
|
||||||
|
@ -95,18 +103,40 @@ public final class MainPresenter
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addDirIfNeeded(Context context)
|
public void addDirIfNeeded()
|
||||||
{
|
{
|
||||||
if (mDirToAdd != null)
|
if (mDirToAdd != null)
|
||||||
{
|
{
|
||||||
GameFileCache.addGameFolder(mDirToAdd, context);
|
GameFileCache.addGameFolder(mDirToAdd);
|
||||||
mDirToAdd = null;
|
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)
|
public void installWAD(String file)
|
||||||
|
|
|
@ -2,6 +2,7 @@ package org.dolphinemu.dolphinemu.ui.main;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
@ -39,11 +40,11 @@ public final class TvMainActivity extends FragmentActivity implements MainView
|
||||||
{
|
{
|
||||||
private static boolean sShouldRescanLibrary = true;
|
private static boolean sShouldRescanLibrary = true;
|
||||||
|
|
||||||
private MainPresenter mPresenter = new MainPresenter(this, this);
|
private final MainPresenter mPresenter = new MainPresenter(this, this);
|
||||||
|
|
||||||
private BrowseSupportFragment mBrowseFragment;
|
private BrowseSupportFragment mBrowseFragment;
|
||||||
|
|
||||||
private ArrayList<ArrayObjectAdapter> mGameRows = new ArrayList<>();
|
private final ArrayList<ArrayObjectAdapter> mGameRows = new ArrayList<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState)
|
protected void onCreate(Bundle savedInstanceState)
|
||||||
|
@ -73,7 +74,7 @@ public final class TvMainActivity extends FragmentActivity implements MainView
|
||||||
GameFileCacheService.startLoad(this);
|
GameFileCacheService.startLoad(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
mPresenter.addDirIfNeeded(this);
|
mPresenter.addDirIfNeeded();
|
||||||
|
|
||||||
// In case the user changed a setting that affects how games are displayed,
|
// In case the user changed a setting that affects how games are displayed,
|
||||||
// such as system language, cover downloading...
|
// such as system language, cover downloading...
|
||||||
|
@ -167,14 +168,17 @@ public final class TvMainActivity extends FragmentActivity implements MainView
|
||||||
@Override
|
@Override
|
||||||
public void launchFileListActivity()
|
public void launchFileListActivity()
|
||||||
{
|
{
|
||||||
FileBrowserHelper.openDirectoryPicker(this, FileBrowserHelper.GAME_EXTENSIONS);
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
|
startActivityForResult(intent, MainPresenter.REQUEST_DIRECTORY);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void launchOpenFileActivity()
|
public void launchOpenFileActivity()
|
||||||
{
|
{
|
||||||
FileBrowserHelper.openFilePicker(this, MainPresenter.REQUEST_GAME_FILE, false,
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||||
FileBrowserHelper.GAME_EXTENSIONS);
|
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||||
|
intent.setType("*/*");
|
||||||
|
startActivityForResult(intent, MainPresenter.REQUEST_GAME_FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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 the user picked a file, as opposed to just backing out.
|
||||||
if (resultCode == MainActivity.RESULT_OK)
|
if (resultCode == MainActivity.RESULT_OK)
|
||||||
{
|
{
|
||||||
|
Uri uri = result.getData();
|
||||||
switch (requestCode)
|
switch (requestCode)
|
||||||
{
|
{
|
||||||
case MainPresenter.REQUEST_DIRECTORY:
|
case MainPresenter.REQUEST_DIRECTORY:
|
||||||
mPresenter.onDirectorySelected(FileBrowserHelper.getSelectedPath(result));
|
mPresenter.onDirectorySelected(result);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MainPresenter.REQUEST_GAME_FILE:
|
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;
|
break;
|
||||||
|
|
||||||
case MainPresenter.REQUEST_WAD_FILE:
|
case MainPresenter.REQUEST_WAD_FILE:
|
||||||
FileBrowserHelper.runAfterExtensionCheck(this, result.getData(),
|
FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.WAD_EXTENSION,
|
||||||
FileBrowserHelper.WAD_EXTENSION,
|
|
||||||
() -> mPresenter.installWAD(result.getData().toString()));
|
() -> mPresenter.installWAD(result.getData().toString()));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,54 +14,76 @@ import androidx.annotation.Keep;
|
||||||
import org.dolphinemu.dolphinemu.DolphinApplication;
|
import org.dolphinemu.dolphinemu.DolphinApplication;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
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
|
public class ContentHandler
|
||||||
{
|
{
|
||||||
@Keep
|
@Keep
|
||||||
public static int openFd(String uri, String mode)
|
public static int openFd(@NonNull String uri, @NonNull String mode)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return getContentResolver().openFileDescriptor(Uri.parse(uri), mode).detachFd();
|
return getContentResolver().openFileDescriptor(unmangle(uri), mode).detachFd();
|
||||||
}
|
}
|
||||||
catch (SecurityException e)
|
catch (SecurityException e)
|
||||||
{
|
{
|
||||||
Log.error("Tried to open " + uri + " without permission");
|
Log.error("Tried to open " + uri + " without permission");
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
// Some content providers throw IllegalArgumentException for invalid modes,
|
catch (Exception ignored)
|
||||||
// despite the documentation saying that invalid modes result in a FileNotFoundException
|
|
||||||
catch (FileNotFoundException | IllegalArgumentException | NullPointerException e)
|
|
||||||
{
|
{
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
public static boolean delete(String uri)
|
public static boolean delete(@NonNull String uri)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return DocumentsContract.deleteDocument(getContentResolver(), Uri.parse(uri));
|
return DocumentsContract.deleteDocument(getContentResolver(), unmangle(uri));
|
||||||
}
|
|
||||||
catch (SecurityException e)
|
|
||||||
{
|
|
||||||
Log.error("Tried to delete " + uri + " without permission");
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
catch (FileNotFoundException e)
|
catch (FileNotFoundException e)
|
||||||
{
|
{
|
||||||
// Return true because we care about the file not being there, not the actual delete.
|
// Return true because we care about the file not being there, not the actual delete.
|
||||||
return true;
|
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)
|
public static boolean exists(@NonNull String uri)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
Uri documentUri = treeToDocument(unmangle(uri));
|
||||||
final String[] projection = new String[]{Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE};
|
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;
|
return cursor != null && cursor.getCount() > 0;
|
||||||
}
|
}
|
||||||
|
@ -70,15 +92,65 @@ public class ContentHandler
|
||||||
{
|
{
|
||||||
Log.error("Tried to check if " + uri + " exists without permission");
|
Log.error("Tried to check if " + uri + " exists without permission");
|
||||||
}
|
}
|
||||||
|
catch (Exception ignored)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
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
|
@Nullable
|
||||||
public static String getDisplayName(@NonNull Uri uri)
|
public static String getDisplayName(@NonNull Uri uri)
|
||||||
{
|
{
|
||||||
final String[] projection = new String[]{Document.COLUMN_DISPLAY_NAME};
|
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())
|
if (cursor != null && cursor.moveToFirst())
|
||||||
{
|
{
|
||||||
|
@ -89,10 +161,257 @@ public class ContentHandler
|
||||||
{
|
{
|
||||||
Log.error("Tried to get display name of " + uri + " without permission");
|
Log.error("Tried to get display name of " + uri + " without permission");
|
||||||
}
|
}
|
||||||
|
catch (Exception ignored)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
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()
|
private static ContentResolver getContentResolver()
|
||||||
{
|
{
|
||||||
return DolphinApplication.getAppContext().getContentResolver();
|
return DolphinApplication.getAppContext().getContentResolver();
|
||||||
|
|
|
@ -28,7 +28,14 @@ import java.util.Set;
|
||||||
public final class FileBrowserHelper
|
public final class FileBrowserHelper
|
||||||
{
|
{
|
||||||
public static final HashSet<String> GAME_EXTENSIONS = new HashSet<>(Arrays.asList(
|
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(
|
public static final HashSet<String> RAW_EXTENSION = new HashSet<>(Collections.singletonList(
|
||||||
"raw"));
|
"raw"));
|
||||||
|
@ -50,21 +57,6 @@ public final class FileBrowserHelper
|
||||||
activity.startActivityForResult(i, MainPresenter.REQUEST_DIRECTORY);
|
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
|
@Nullable
|
||||||
public static String getSelectedPath(Intent result)
|
public static String getSelectedPath(Intent result)
|
||||||
{
|
{
|
||||||
|
@ -79,22 +71,6 @@ public final class FileBrowserHelper
|
||||||
return null;
|
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)
|
public static boolean isPathEmptyOrValid(StringSetting path)
|
||||||
{
|
{
|
||||||
return isPathEmptyOrValid(path.getStringGlobal());
|
return isPathEmptyOrValid(path.getStringGlobal());
|
||||||
|
@ -112,10 +88,10 @@ public final class FileBrowserHelper
|
||||||
|
|
||||||
String path = uri.getLastPathSegment();
|
String path = uri.getLastPathSegment();
|
||||||
if (path != null)
|
if (path != null)
|
||||||
extension = getExtension(new File(path).getName());
|
extension = getExtension(new File(path).getName(), false);
|
||||||
|
|
||||||
if (extension == null)
|
if (extension == null)
|
||||||
extension = getExtension(ContentHandler.getDisplayName(uri));
|
extension = getExtension(ContentHandler.getDisplayName(uri), false);
|
||||||
|
|
||||||
if (extension != null && validExtensions.contains(extension))
|
if (extension != null && validExtensions.contains(extension))
|
||||||
{
|
{
|
||||||
|
@ -133,10 +109,8 @@ public final class FileBrowserHelper
|
||||||
int messageId = validExtensions.size() == 1 ?
|
int messageId = validExtensions.size() == 1 ?
|
||||||
R.string.wrong_file_extension_single : R.string.wrong_file_extension_multiple;
|
R.string.wrong_file_extension_single : R.string.wrong_file_extension_multiple;
|
||||||
|
|
||||||
ArrayList<String> extensionsList = new ArrayList<>(validExtensions);
|
message = context.getString(messageId, extension,
|
||||||
Collections.sort(extensionsList);
|
setToSortedDelimitedString(validExtensions));
|
||||||
|
|
||||||
message = context.getString(messageId, extension, join(", ", extensionsList));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
new AlertDialog.Builder(context, R.style.DolphinDialogBase)
|
new AlertDialog.Builder(context, R.style.DolphinDialogBase)
|
||||||
|
@ -148,13 +122,22 @@ public final class FileBrowserHelper
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static String getExtension(@Nullable String fileName)
|
public static String getExtension(@Nullable String fileName, boolean includeDot)
|
||||||
{
|
{
|
||||||
if (fileName == null)
|
if (fileName == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
int dotIndex = fileName.lastIndexOf(".");
|
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
|
// TODO: Replace this with String.join once we can use Java 8
|
||||||
|
|
|
@ -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="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_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_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>
|
<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 -->
|
<!-- Misc -->
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
#include "jni/AndroidCommon/AndroidCommon.h"
|
#include "jni/AndroidCommon/AndroidCommon.h"
|
||||||
|
|
||||||
|
#include <ios>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
@ -43,6 +44,14 @@ std::vector<std::string> JStringArrayToVector(JNIEnv* env, jobjectArray array)
|
||||||
return result;
|
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)
|
bool IsPathAndroidContent(const std::string& uri)
|
||||||
{
|
{
|
||||||
return StringBeginsWith(uri, "content://");
|
return StringBeginsWith(uri, "content://");
|
||||||
|
@ -66,6 +75,28 @@ std::string OpenModeToAndroid(std::string mode)
|
||||||
return 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)
|
int OpenAndroidContent(const std::string& uri, const std::string& mode)
|
||||||
{
|
{
|
||||||
JNIEnv* env = IDCache::GetEnvForThread();
|
JNIEnv* env = IDCache::GetEnvForThread();
|
||||||
|
@ -81,6 +112,43 @@ bool DeleteAndroidContent(const std::string& uri)
|
||||||
IDCache::GetContentHandlerDelete(), ToJString(env, 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()
|
int GetNetworkIpAddress()
|
||||||
{
|
{
|
||||||
JNIEnv* env = IDCache::GetEnvForThread();
|
JNIEnv* env = IDCache::GetEnvForThread();
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <ios>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <jni.h>
|
#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.
|
// 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::string mode);
|
||||||
|
std::string OpenModeToAndroid(std::ios_base::openmode mode);
|
||||||
|
|
||||||
// Opens a given file and returns a file descriptor.
|
// Opens a given file and returns a file descriptor.
|
||||||
int OpenAndroidContent(const std::string& uri, const std::string& mode);
|
int OpenAndroidContent(const std::string& uri, const std::string& mode);
|
||||||
|
|
||||||
// Deletes a given file.
|
// Deletes a given file.
|
||||||
bool DeleteAndroidContent(const std::string& uri);
|
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 GetNetworkIpAddress();
|
||||||
int GetNetworkPrefixLength();
|
int GetNetworkPrefixLength();
|
||||||
int GetNetworkGateway();
|
int GetNetworkGateway();
|
||||||
|
|
|
@ -10,6 +10,8 @@ static constexpr jint JNI_VERSION = JNI_VERSION_1_6;
|
||||||
|
|
||||||
static JavaVM* s_java_vm;
|
static JavaVM* s_java_vm;
|
||||||
|
|
||||||
|
static jclass s_string_class;
|
||||||
|
|
||||||
static jclass s_native_library_class;
|
static jclass s_native_library_class;
|
||||||
static jmethodID s_display_alert_msg;
|
static jmethodID s_display_alert_msg;
|
||||||
static jmethodID s_do_rumble;
|
static jmethodID s_do_rumble;
|
||||||
|
@ -44,6 +46,10 @@ static jmethodID s_compress_cb_run;
|
||||||
static jclass s_content_handler_class;
|
static jclass s_content_handler_class;
|
||||||
static jmethodID s_content_handler_open_fd;
|
static jmethodID s_content_handler_open_fd;
|
||||||
static jmethodID s_content_handler_delete;
|
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 jclass s_network_helper_class;
|
||||||
static jmethodID s_network_helper_get_network_ip_address;
|
static jmethodID s_network_helper_get_network_ip_address;
|
||||||
|
@ -75,6 +81,11 @@ JNIEnv* GetEnvForThread()
|
||||||
return owned.env;
|
return owned.env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
jclass GetStringClass()
|
||||||
|
{
|
||||||
|
return s_string_class;
|
||||||
|
}
|
||||||
|
|
||||||
jclass GetNativeLibraryClass()
|
jclass GetNativeLibraryClass()
|
||||||
{
|
{
|
||||||
return s_native_library_class;
|
return s_native_library_class;
|
||||||
|
@ -210,6 +221,26 @@ jmethodID GetContentHandlerDelete()
|
||||||
return s_content_handler_delete;
|
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()
|
jclass GetNetworkHelperClass()
|
||||||
{
|
{
|
||||||
return s_network_helper_class;
|
return s_network_helper_class;
|
||||||
|
@ -229,6 +260,7 @@ jmethodID GetNetworkHelperGetNetworkGateway()
|
||||||
{
|
{
|
||||||
return s_network_helper_get_network_gateway;
|
return s_network_helper_get_network_gateway;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace IDCache
|
} // namespace IDCache
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
@ -243,6 +275,9 @@ jint JNI_OnLoad(JavaVM* vm, void* reserved)
|
||||||
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK)
|
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION) != JNI_OK)
|
||||||
return JNI_ERR;
|
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");
|
const jclass native_library_class = env->FindClass("org/dolphinemu/dolphinemu/NativeLibrary");
|
||||||
s_native_library_class = reinterpret_cast<jclass>(env->NewGlobalRef(native_library_class));
|
s_native_library_class = reinterpret_cast<jclass>(env->NewGlobalRef(native_library_class));
|
||||||
s_display_alert_msg = env->GetStaticMethodID(s_native_library_class, "displayAlertMsg",
|
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");
|
"(Ljava/lang/String;Ljava/lang/String;)I");
|
||||||
s_content_handler_delete =
|
s_content_handler_delete =
|
||||||
env->GetStaticMethodID(s_content_handler_class, "delete", "(Ljava/lang/String;)Z");
|
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 =
|
const jclass network_helper_class =
|
||||||
env->FindClass("org/dolphinemu/dolphinemu/utils/NetworkHelper");
|
env->FindClass("org/dolphinemu/dolphinemu/utils/NetworkHelper");
|
||||||
|
|
|
@ -10,6 +10,8 @@ namespace IDCache
|
||||||
{
|
{
|
||||||
JNIEnv* GetEnvForThread();
|
JNIEnv* GetEnvForThread();
|
||||||
|
|
||||||
|
jclass GetStringClass();
|
||||||
|
|
||||||
jclass GetNativeLibraryClass();
|
jclass GetNativeLibraryClass();
|
||||||
jmethodID GetDisplayAlertMsg();
|
jmethodID GetDisplayAlertMsg();
|
||||||
jmethodID GetDoRumble();
|
jmethodID GetDoRumble();
|
||||||
|
@ -44,6 +46,10 @@ jmethodID GetCompressCallbackRun();
|
||||||
jclass GetContentHandlerClass();
|
jclass GetContentHandlerClass();
|
||||||
jmethodID GetContentHandlerOpenFd();
|
jmethodID GetContentHandlerOpenFd();
|
||||||
jmethodID GetContentHandlerDelete();
|
jmethodID GetContentHandlerDelete();
|
||||||
|
jmethodID GetContentHandlerGetSizeAndIsDirectory();
|
||||||
|
jmethodID GetContentHandlerGetDisplayName();
|
||||||
|
jmethodID GetContentHandlerGetChildNames();
|
||||||
|
jmethodID GetContentHandlerDoFileSearch();
|
||||||
|
|
||||||
jclass GetNetworkHelperClass();
|
jclass GetNetworkHelperClass();
|
||||||
jmethodID GetNetworkHelperGetNetworkIpAddress();
|
jmethodID GetNetworkHelperGetNetworkIpAddress();
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <iterator>
|
||||||
|
|
||||||
#include "Common/CommonPaths.h"
|
#include "Common/CommonPaths.h"
|
||||||
#include "Common/FileSearch.h"
|
#include "Common/FileSearch.h"
|
||||||
|
@ -15,6 +16,10 @@
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
#define HAS_STD_FILESYSTEM
|
#define HAS_STD_FILESYSTEM
|
||||||
#else
|
#else
|
||||||
|
#ifdef ANDROID
|
||||||
|
#include "jni/AndroidCommon/AndroidCommon.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include "Common/CommonFuncs.h"
|
#include "Common/CommonFuncs.h"
|
||||||
#include "Common/FileUtil.h"
|
#include "Common/FileUtil.h"
|
||||||
|
@ -24,36 +29,30 @@ namespace Common
|
||||||
{
|
{
|
||||||
#ifndef HAS_STD_FILESYSTEM
|
#ifndef HAS_STD_FILESYSTEM
|
||||||
|
|
||||||
static std::vector<std::string>
|
static void FileSearchWithTest(const std::string& directory, bool recursive,
|
||||||
FileSearchWithTest(const std::vector<std::string>& directories, bool recursive,
|
std::vector<std::string>* result_out,
|
||||||
std::function<bool(const File::FSTEntry&)> callback)
|
std::function<bool(const File::FSTEntry&)> callback)
|
||||||
{
|
{
|
||||||
std::vector<std::string> result;
|
File::FSTEntry top = File::ScanDirectoryTree(directory, recursive);
|
||||||
for (const std::string& directory : directories)
|
|
||||||
{
|
|
||||||
File::FSTEntry top = File::ScanDirectoryTree(directory, recursive);
|
|
||||||
|
|
||||||
std::function<void(File::FSTEntry&)> DoEntry;
|
const std::function<void(File::FSTEntry&)> DoEntry = [&](File::FSTEntry& entry) {
|
||||||
DoEntry = [&](File::FSTEntry& entry) {
|
if (callback(entry))
|
||||||
if (callback(entry))
|
result_out->push_back(entry.physicalName);
|
||||||
result.push_back(entry.physicalName);
|
for (auto& child : entry.children)
|
||||||
for (auto& child : entry.children)
|
|
||||||
DoEntry(child);
|
|
||||||
};
|
|
||||||
for (auto& child : top.children)
|
|
||||||
DoEntry(child);
|
DoEntry(child);
|
||||||
}
|
};
|
||||||
// remove duplicates
|
|
||||||
std::sort(result.begin(), result.end());
|
for (auto& child : top.children)
|
||||||
result.erase(std::unique(result.begin(), result.end()), result.end());
|
DoEntry(child);
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> DoFileSearch(const std::vector<std::string>& directories,
|
std::vector<std::string> DoFileSearch(const std::vector<std::string>& directories,
|
||||||
const std::vector<std::string>& exts, bool recursive)
|
const std::vector<std::string>& exts, bool recursive)
|
||||||
{
|
{
|
||||||
|
std::vector<std::string> result;
|
||||||
|
|
||||||
bool accept_all = exts.empty();
|
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)
|
if (accept_all)
|
||||||
return true;
|
return true;
|
||||||
if (entry.isDirectory)
|
if (entry.isDirectory)
|
||||||
|
@ -63,7 +62,34 @@ std::vector<std::string> DoFileSearch(const std::vector<std::string>& directorie
|
||||||
return name.length() >= ext.length() &&
|
return name.length() >= ext.length() &&
|
||||||
strcasecmp(name.c_str() + name.length() - ext.length(), ext.c_str()) == 0;
|
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
|
#else
|
||||||
|
|
|
@ -78,19 +78,40 @@ FileInfo::FileInfo(const char* path) : FileInfo(std::string(path))
|
||||||
#else
|
#else
|
||||||
FileInfo::FileInfo(const std::string& path) : FileInfo(path.c_str())
|
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)
|
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
|
#endif
|
||||||
|
|
||||||
FileInfo::FileInfo(int fd)
|
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
|
bool FileInfo::Exists() const
|
||||||
{
|
{
|
||||||
return m_exists;
|
return m_exists;
|
||||||
|
@ -476,14 +497,47 @@ FSTEntry ScanDirectoryTree(const std::string& directory, bool recursive)
|
||||||
{
|
{
|
||||||
const std::string virtual_name(TStrToUTF8(ffd.cFileName));
|
const std::string virtual_name(TStrToUTF8(ffd.cFileName));
|
||||||
#else
|
#else
|
||||||
DIR* dirp = opendir(directory.c_str());
|
DIR* dirp = nullptr;
|
||||||
if (!dirp)
|
|
||||||
return parent_entry;
|
#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
|
// 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
|
#endif
|
||||||
if (virtual_name == "." || virtual_name == "..")
|
if (virtual_name == "." || virtual_name == "..")
|
||||||
continue;
|
continue;
|
||||||
|
@ -514,7 +568,8 @@ FSTEntry ScanDirectoryTree(const std::string& directory, bool recursive)
|
||||||
FindClose(hFind);
|
FindClose(hFind);
|
||||||
#else
|
#else
|
||||||
}
|
}
|
||||||
closedir(dirp);
|
if (dirp)
|
||||||
|
closedir(dirp);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return parent_entry;
|
return parent_entry;
|
||||||
|
|
|
@ -18,6 +18,11 @@
|
||||||
#include "Common/StringUtil.h"
|
#include "Common/StringUtil.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef ANDROID
|
||||||
|
#include "Common/StringUtil.h"
|
||||||
|
#include "jni/AndroidCommon/AndroidCommon.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
// User directory indices for GetUserPath
|
// User directory indices for GetUserPath
|
||||||
enum
|
enum
|
||||||
{
|
{
|
||||||
|
@ -109,6 +114,10 @@ public:
|
||||||
u64 GetSize() const;
|
u64 GetSize() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
#ifdef ANDROID
|
||||||
|
void AndroidContentInit(const std::string& path);
|
||||||
|
#endif
|
||||||
|
|
||||||
struct stat m_stat;
|
struct stat m_stat;
|
||||||
bool m_exists;
|
bool m_exists;
|
||||||
};
|
};
|
||||||
|
@ -214,14 +223,20 @@ std::string GetExeDirectory();
|
||||||
bool WriteStringToFile(const std::string& filename, std::string_view str);
|
bool WriteStringToFile(const std::string& filename, std::string_view str);
|
||||||
bool ReadFileToString(const std::string& filename, std::string& 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>
|
template <typename T>
|
||||||
void OpenFStream(T& fstream, const std::string& filename, std::ios_base::openmode openmode)
|
void OpenFStream(T& fstream, const std::string& filename, std::ios_base::openmode openmode)
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
fstream.open(UTF8ToTStr(filename).c_str(), openmode);
|
fstream.open(UTF8ToTStr(filename).c_str(), openmode);
|
||||||
#else
|
#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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -158,6 +158,15 @@ BootParameters::GenerateFromFile(std::vector<std::string> paths,
|
||||||
if (paths.size() == 1)
|
if (paths.size() == 1)
|
||||||
paths.clear();
|
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 = {
|
static const std::unordered_set<std::string> disc_image_extensions = {
|
||||||
{".gcm", ".iso", ".tgc", ".wbfs", ".ciso", ".gcz", ".wia", ".rvz", ".dol", ".elf"}};
|
{".gcm", ".iso", ".tgc", ".wbfs", ".ciso", ".gcz", ".wia", ".rvz", ".dol", ".elf"}};
|
||||||
if (disc_image_extensions.find(extension) != disc_image_extensions.end() || is_drive)
|
if (disc_image_extensions.find(extension) != disc_image_extensions.end() || is_drive)
|
||||||
|
|
Loading…
Reference in New Issue