Skip to content
Permalink
Browse files
Merge pull request #9221 from JosJuice/android-saf-sd-card
Android: Use storage access framework for custom SD card paths
  • Loading branch information
JMC47 committed Dec 10, 2020
2 parents cca04d3 + 161f8c3 commit 75899b0
Show file tree
Hide file tree
Showing 14 changed files with 311 additions and 47 deletions.
@@ -35,6 +35,7 @@
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting;
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting;
import org.dolphinemu.dolphinemu.features.settings.model.Settings;
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting;
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag;
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsActivity;
import org.dolphinemu.dolphinemu.features.settings.utils.SettingsFile;
@@ -169,13 +170,38 @@ public static void launch(FragmentActivity activity, String[] filePaths)
if (sIgnoreLaunchRequests)
return;

new AfterDirectoryInitializationRunner().run(activity, true, () ->
{
if (FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_DEFAULT_ISO) &&
FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_FS_PATH) &&
FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_DUMP_PATH) &&
FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_LOAD_PATH) &&
FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_RESOURCEPACK_PATH) &&
FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_SD_PATH))
{
launchWithoutChecks(activity, filePaths);
}
else
{
AlertDialog.Builder builder = new AlertDialog.Builder(activity, R.style.DolphinDialogBase);
builder.setMessage(R.string.unavailable_paths);
builder.setPositiveButton(R.string.yes, (dialogInterface, i) ->
SettingsActivity.launch(activity, MenuTag.CONFIG_PATHS));
builder.setNeutralButton(R.string.continue_anyway, (dialogInterface, i) ->
launchWithoutChecks(activity, filePaths));
builder.show();
}
});
}

private static void launchWithoutChecks(FragmentActivity activity, String[] filePaths)
{
sIgnoreLaunchRequests = true;

Intent launcher = new Intent(activity, EmulationActivity.class);
launcher.putExtra(EXTRA_SELECTED_GAMES, filePaths);

new AfterDirectoryInitializationRunner().run(activity, true,
() -> activity.startActivity(launcher));
activity.startActivity(launcher);
}

public static void stopIgnoringLaunchRequests()
@@ -3,6 +3,7 @@
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.view.Menu;
@@ -18,6 +19,7 @@

import org.dolphinemu.dolphinemu.R;
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.FileBrowserHelper;
import org.dolphinemu.dolphinemu.utils.TvUtil;
@@ -170,11 +172,33 @@ protected void onActivityResult(int requestCode, int resultCode, Intent result)
// If the user picked a file, as opposed to just backing out.
if (resultCode == MainActivity.RESULT_OK)
{
String path = FileBrowserHelper.getSelectedPath(result);
getFragment().getAdapter().onFilePickerConfirmation(path);
if (requestCode == MainPresenter.REQUEST_SD_FILE)
{
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, () ->
{
getContentResolver().takePersistableUriPermission(uri, takeFlags);
getFragment().getAdapter().onFilePickerConfirmation(uri.toString());
});
}
else
{
String path = FileBrowserHelper.getSelectedPath(result);
getFragment().getAdapter().onFilePickerConfirmation(path);
}
}
}

@NonNull
private Uri canonicalizeIfPossible(@NonNull Uri uri)
{
Uri canonicalizedUri = getContentResolver().canonicalize(uri);
return canonicalizedUri != null ? canonicalizedUri : uri;
}

@Override
public void showLoading()
{
@@ -2,6 +2,9 @@

import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Build;
import android.provider.DocumentsContract;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -289,41 +292,53 @@ public void onInputBindingClick(final InputBindingSetting item, final int positi
dialog.show();
}

public void onFilePickerDirectoryClick(SettingsItem item)
public void onFilePickerDirectoryClick(SettingsItem item, int position)
{
mClickedItem = item;
mClickedPosition = position;

FileBrowserHelper.openDirectoryPicker(mView.getActivity(), FileBrowserHelper.GAME_EXTENSIONS);
}

public void onFilePickerFileClick(SettingsItem item)
public void onFilePickerFileClick(SettingsItem item, int position)
{
mClickedItem = item;
mClickedPosition = position;
FilePicker filePicker = (FilePicker) item;

HashSet<String> extensions;
switch (filePicker.getRequestType())
{
case MainPresenter.REQUEST_SD_FILE:
extensions = FileBrowserHelper.RAW_EXTENSION;
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:
extensions = FileBrowserHelper.GAME_EXTENSIONS;
FileBrowserHelper.openFilePicker(mView.getActivity(), filePicker.getRequestType(), false,
FileBrowserHelper.GAME_EXTENSIONS);
break;
default:
throw new InvalidParameterException("Unhandled request code");
}

FileBrowserHelper.openFilePicker(mView.getActivity(), filePicker.getRequestType(), false,
extensions);
}

public void onFilePickerConfirmation(String selectedFile)
{
FilePicker filePicker = (FilePicker) mClickedItem;

if (!filePicker.getSelectedValue(mView.getSettings()).equals(selectedFile))
{
notifyItemChanged(mClickedPosition);
mView.onSettingChanged();
}

filePicker.setSelectedValue(mView.getSettings(), selectedFile);

@@ -1,5 +1,6 @@
package org.dolphinemu.dolphinemu.features.settings.ui.viewholder;

import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View;
import android.widget.TextView;
@@ -12,6 +13,7 @@
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter;
import org.dolphinemu.dolphinemu.ui.main.MainPresenter;
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization;
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;

public final class FilePickerViewHolder extends SettingViewHolder
{
@@ -21,6 +23,8 @@ public final class FilePickerViewHolder extends SettingViewHolder
private TextView mTextSettingName;
private TextView mTextSettingDescription;

private Drawable mDefaultBackground;

public FilePickerViewHolder(View itemView, SettingsAdapter adapter)
{
super(itemView, adapter);
@@ -31,6 +35,8 @@ protected void findViews(View root)
{
mTextSettingName = root.findViewById(R.id.text_setting_name);
mTextSettingDescription = root.findViewById(R.id.text_setting_description);

mDefaultBackground = root.getBackground();
}

@Override
@@ -39,6 +45,17 @@ public void bind(SettingsItem item)
mFilePicker = (FilePicker) item;
mItem = item;

String path = mFilePicker.getSelectedValue(getAdapter().getSettings());

if (FileBrowserHelper.isPathEmptyOrValid(path))
{
itemView.setBackground(mDefaultBackground);
}
else
{
itemView.setBackgroundResource(R.drawable.invalid_setting_background);
}

mTextSettingName.setText(item.getNameId());

if (item.getDescriptionId() > 0)
@@ -47,8 +64,6 @@ public void bind(SettingsItem item)
}
else
{
String path = mFilePicker.getSelectedValue(getAdapter().getSettings());

if (TextUtils.isEmpty(path))
{
String defaultPathRelative = mFilePicker.getDefaultPathRelativeToUserDirectory();
@@ -73,13 +88,14 @@ public void onClick(View clicked)
return;
}

int position = getAdapterPosition();
if (mFilePicker.getRequestType() == MainPresenter.REQUEST_DIRECTORY)
{
getAdapter().onFilePickerDirectoryClick(mItem);
getAdapter().onFilePickerDirectoryClick(mItem, position);
}
else
{
getAdapter().onFilePickerFileClick(mItem);
getAdapter().onFilePickerFileClick(mItem, position);
}

setStyle(mTextSettingName, mItem);
@@ -205,7 +205,9 @@ protected void onActivityResult(int requestCode, int resultCode, Intent result)
break;

case MainPresenter.REQUEST_WAD_FILE:
mPresenter.installWAD(result.getData().toString());
FileBrowserHelper.runAfterExtensionCheck(this, result.getData(),
FileBrowserHelper.WAD_EXTENSION,
() -> mPresenter.installWAD(result.getData().toString()));
break;
}
}
@@ -229,7 +229,9 @@ protected void onActivityResult(int requestCode, int resultCode, Intent result)
break;

case MainPresenter.REQUEST_WAD_FILE:
mPresenter.installWAD(result.getData().toString());
FileBrowserHelper.runAfterExtensionCheck(this, result.getData(),
FileBrowserHelper.WAD_EXTENSION,
() -> mPresenter.installWAD(result.getData().toString()));
break;
}
}
@@ -1,8 +1,13 @@
package org.dolphinemu.dolphinemu.utils;

import android.content.ContentResolver;
import android.database.Cursor;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import androidx.annotation.Keep;

@@ -17,10 +22,16 @@ public static int openFd(String uri, String mode)
{
try
{
return DolphinApplication.getAppContext().getContentResolver()
.openFileDescriptor(Uri.parse(uri), mode).detachFd();
return getContentResolver().openFileDescriptor(Uri.parse(uri), mode).detachFd();
}
catch (SecurityException e)
{
Log.error("Tried to open " + uri + " without permission");
return -1;
}
catch (FileNotFoundException | NullPointerException e)
// Some content providers throw IllegalArgumentException for invalid modes,
// despite the documentation saying that invalid modes result in a FileNotFoundException
catch (FileNotFoundException | IllegalArgumentException | NullPointerException e)
{
return -1;
}
@@ -31,13 +42,59 @@ public static boolean delete(String uri)
{
try
{
ContentResolver resolver = DolphinApplication.getAppContext().getContentResolver();
return DocumentsContract.deleteDocument(resolver, Uri.parse(uri));
return DocumentsContract.deleteDocument(getContentResolver(), Uri.parse(uri));
}
catch (SecurityException e)
{
Log.error("Tried to delete " + uri + " without permission");
return false;
}
catch (FileNotFoundException e)
{
// Return true because we care about the file not being there, not the actual delete.
return true;
}
}

public static boolean exists(@NonNull String uri)
{
try
{
final String[] projection = new String[]{Document.COLUMN_MIME_TYPE, Document.COLUMN_SIZE};
try (Cursor cursor = getContentResolver().query(Uri.parse(uri), projection, null, null, null))
{
return cursor != null && cursor.getCount() > 0;
}
}
catch (SecurityException e)
{
Log.error("Tried to check if " + uri + " exists without permission");
}

return false;
}

@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))
{
if (cursor != null && cursor.moveToFirst())
{
return cursor.getString(0);
}
}
catch (SecurityException e)
{
Log.error("Tried to get display name of " + uri + " without permission");
}

return null;
}

private static ContentResolver getContentResolver()
{
return DolphinApplication.getAppContext().getContentResolver();
}
}

0 comments on commit 75899b0

Please sign in to comment.