summaryrefslogtreecommitdiff
path: root/Source/Android/app/src/main/java
diff options
context:
space:
mode:
authorJMC47 <JMC4789@gmail.com>2020-12-10 16:32:43 -0500
committerGitHub <noreply@github.com>2020-12-10 16:32:43 -0500
commit75899b0e1121f01afdd606e1dd3a5822fa408530 (patch)
tree53f8c2ef2a5b61248f588aae625181233e487e1a /Source/Android/app/src/main/java
parentcca04d36e21377f3292dbceb05f4768523f17df4 (diff)
parent161f8c3fad9df97ae4f97b7f45125e5e4c9608a7 (diff)
Merge pull request #9221 from JosJuice/android-saf-sd-card
Android: Use storage access framework for custom SD card paths
Diffstat (limited to 'Source/Android/app/src/main/java')
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java30
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.java28
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java31
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/FilePickerViewHolder.java24
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java4
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java4
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java67
-rw-r--r--Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java88
8 files changed, 253 insertions, 23 deletions
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java
index 0602128a56..9bb3be3878 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java
@@ -35,6 +35,7 @@ import org.dolphinemu.dolphinemu.R;
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 final class EmulationActivity extends AppCompatActivity
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()
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.java
index e043b36f24..29d992a156 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.java
@@ -3,6 +3,7 @@ package org.dolphinemu.dolphinemu.features.settings.ui;
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 androidx.lifecycle.ViewModelProvider;
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 @@ 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)
{
- 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()
{
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java
index c751a7750e..3442c72fa5 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.java
@@ -2,6 +2,9 @@ package org.dolphinemu.dolphinemu.features.settings.ui;
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,33 +292,42 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
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)
@@ -323,7 +335,10 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
FilePicker filePicker = (FilePicker) mClickedItem;
if (!filePicker.getSelectedValue(mView.getSettings()).equals(selectedFile))
+ {
+ notifyItemChanged(mClickedPosition);
mView.onSettingChanged();
+ }
filePicker.setSelectedValue(mView.getSettings(), selectedFile);
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/FilePickerViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/FilePickerViewHolder.java
index 790ce1acf9..1ee60b30b5 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/FilePickerViewHolder.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/FilePickerViewHolder.java
@@ -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.model.view.SettingsItem;
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 @@ public final class FilePickerViewHolder extends SettingViewHolder
{
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 final class FilePickerViewHolder extends SettingViewHolder
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 final class FilePickerViewHolder extends SettingViewHolder
}
else
{
- String path = mFilePicker.getSelectedValue(getAdapter().getSettings());
-
if (TextUtils.isEmpty(path))
{
String defaultPathRelative = mFilePicker.getDefaultPathRelativeToUserDirectory();
@@ -73,13 +88,14 @@ public final class FilePickerViewHolder extends SettingViewHolder
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);
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java
index 53306d8e4d..cd5dd80c75 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/MainActivity.java
@@ -205,7 +205,9 @@ public final class MainActivity extends AppCompatActivity implements MainView
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;
}
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java
index f1c8c37a41..23d1050d04 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/ui/main/TvMainActivity.java
@@ -229,7 +229,9 @@ public final class TvMainActivity extends FragmentActivity implements MainView
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;
}
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java
index 62d36553c8..dbeb410079 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ContentHandler.java
@@ -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 class ContentHandler
{
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,8 +42,12 @@ public class ContentHandler
{
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)
{
@@ -40,4 +55,46 @@ public class ContentHandler
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();
+ }
}
diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java
index 949cee554c..79ec49deae 100644
--- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java
+++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/FileBrowserHelper.java
@@ -1,23 +1,29 @@
package org.dolphinemu.dolphinemu.utils;
+import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity;
import com.nononsenseapps.filepicker.FilePickerActivity;
import com.nononsenseapps.filepicker.Utils;
+import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.activities.CustomFilePickerActivity;
+import org.dolphinemu.dolphinemu.features.settings.model.StringSetting;
import org.dolphinemu.dolphinemu.ui.main.MainPresenter;
import java.io.File;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
+import java.util.Set;
public final class FileBrowserHelper
{
@@ -27,6 +33,9 @@ public final class FileBrowserHelper
public static final HashSet<String> RAW_EXTENSION = new HashSet<>(Collections.singletonList(
"raw"));
+ public static final HashSet<String> WAD_EXTENSION = new HashSet<>(Collections.singletonList(
+ "wad"));
+
public static void openDirectoryPicker(FragmentActivity activity, HashSet<String> extensions)
{
Intent i = new Intent(activity, CustomFilePickerActivity.class);
@@ -85,4 +94,83 @@ public final class FileBrowserHelper
return null;
}
+
+ public static boolean isPathEmptyOrValid(StringSetting path)
+ {
+ return isPathEmptyOrValid(path.getStringGlobal());
+ }
+
+ public static boolean isPathEmptyOrValid(String path)
+ {
+ return !path.startsWith("content://") || ContentHandler.exists(path);
+ }
+
+ public static void runAfterExtensionCheck(Context context, Uri uri, Set<String> validExtensions,
+ Runnable runnable)
+ {
+ String extension = null;
+
+ String path = uri.getLastPathSegment();
+ if (path != null)
+ extension = getExtension(new File(path).getName());
+
+ if (extension == null)
+ extension = getExtension(ContentHandler.getDisplayName(uri));
+
+ if (extension != null && validExtensions.contains(extension))
+ {
+ runnable.run();
+ return;
+ }
+
+ String message;
+ if (extension == null)
+ {
+ message = context.getString(R.string.no_file_extension);
+ }
+ else
+ {
+ 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));
+ }
+
+ new AlertDialog.Builder(context, R.style.DolphinDialogBase)
+ .setMessage(message)
+ .setPositiveButton(R.string.yes, (dialogInterface, i) -> runnable.run())
+ .setNegativeButton(R.string.no, null)
+ .setCancelable(false)
+ .show();
+ }
+
+ @Nullable
+ private static String getExtension(@Nullable String fileName)
+ {
+ if (fileName == null)
+ return null;
+
+ int dotIndex = fileName.lastIndexOf(".");
+ return dotIndex != -1 ? fileName.substring(dotIndex + 1) : null;
+ }
+
+ // TODO: Replace this with String.join once we can use Java 8
+ private static String join(CharSequence delimiter, Iterable<? extends CharSequence> elements)
+ {
+ StringBuilder sb = new StringBuilder();
+
+ boolean first = true;
+ for (CharSequence element : elements)
+ {
+ if (!first)
+ sb.append(delimiter);
+ first = false;
+ sb.append(element);
+ }
+
+ return sb.toString();
+ }
}