From 70df5446d3f0bc203363ad74e737fd772e8e9c2c Mon Sep 17 00:00:00 2001 From: JosJuice Date: Wed, 4 Nov 2020 20:59:39 +0100 Subject: [PATCH 1/6] Android: Make the handling of SAF open modes more robust --- .../dolphinemu/utils/ContentHandler.java | 4 +- .../jni/AndroidCommon/AndroidCommon.cpp | 39 +++++++++++++------ .../Android/jni/AndroidCommon/AndroidCommon.h | 9 +++++ Source/Core/Common/File.cpp | 16 ++------ 4 files changed, 43 insertions(+), 25 deletions(-) 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 83b9c585c9..dc4472a212 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 @@ -17,7 +17,9 @@ public class ContentHandler return DolphinApplication.getAppContext().getContentResolver() .openFileDescriptor(Uri.parse(uri), mode).detachFd(); } - 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; } diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp index 73405b6d8e..344e34c7b6 100644 --- a/Source/Android/jni/AndroidCommon/AndroidCommon.cpp +++ b/Source/Android/jni/AndroidCommon/AndroidCommon.cpp @@ -10,6 +10,7 @@ #include +#include "Common/Assert.h" #include "Common/StringUtil.h" #include "jni/AndroidCommon/IDCache.h" @@ -42,21 +43,35 @@ std::vector JStringArrayToVector(JNIEnv* env, jobjectArray array) return result; } +bool IsPathAndroidContent(const std::string& uri) +{ + return StringBeginsWith(uri, "content://"); +} + +std::string OpenModeToAndroid(std::string mode) +{ + // The 'b' specifier is not supported. Since we're on POSIX, it's fine to just skip it. + if (!mode.empty() && mode.back() == 'b') + mode.pop_back(); + + if (mode == "r+") + mode = "rw"; + else if (mode == "w+") + mode = "rwt"; + else if (mode == "a+") + mode = "rwa"; + else if (mode == "a") + mode = "wa"; + + return mode; +} + int OpenAndroidContent(const std::string& uri, const std::string& mode) { JNIEnv* env = IDCache::GetEnvForThread(); - const jint fd = env->CallStaticIntMethod(IDCache::GetContentHandlerClass(), - IDCache::GetContentHandlerOpenFd(), ToJString(env, uri), - ToJString(env, mode)); - - // We can get an IllegalArgumentException when passing an invalid mode - if (env->ExceptionCheck()) - { - env->ExceptionDescribe(); - abort(); - } - - return fd; + return env->CallStaticIntMethod(IDCache::GetContentHandlerClass(), + IDCache::GetContentHandlerOpenFd(), ToJString(env, uri), + ToJString(env, mode)); } bool DeleteAndroidContent(const std::string& uri) diff --git a/Source/Android/jni/AndroidCommon/AndroidCommon.h b/Source/Android/jni/AndroidCommon/AndroidCommon.h index ca8245182d..967933bdf8 100644 --- a/Source/Android/jni/AndroidCommon/AndroidCommon.h +++ b/Source/Android/jni/AndroidCommon/AndroidCommon.h @@ -12,5 +12,14 @@ std::string GetJString(JNIEnv* env, jstring jstr); jstring ToJString(JNIEnv* env, const std::string& str); std::vector JStringArrayToVector(JNIEnv* env, jobjectArray array); +// Returns true if the given path should be opened as Android content instead of a normal file. +bool IsPathAndroidContent(const std::string& uri); + +// Turns a C/C++ style mode (e.g. "rb") into one which can be used with OpenAndroidContent. +std::string OpenModeToAndroid(std::string mode); + +// Opens a given file and returns a file descriptor. int OpenAndroidContent(const std::string& uri, const std::string& mode); + +// Deletes a given file. bool DeleteAndroidContent(const std::string& uri); diff --git a/Source/Core/Common/File.cpp b/Source/Core/Common/File.cpp index a724cbb723..c601eb33f2 100644 --- a/Source/Core/Common/File.cpp +++ b/Source/Core/Common/File.cpp @@ -18,7 +18,6 @@ #ifdef ANDROID #include -#include "Common/StringUtil.h" #include "jni/AndroidCommon/AndroidCommon.h" #endif @@ -66,24 +65,17 @@ void IOFile::Swap(IOFile& other) noexcept bool IOFile::Open(const std::string& filename, const char openmode[]) { Close(); + #ifdef _WIN32 m_good = _tfopen_s(&m_file, UTF8ToTStr(filename).c_str(), UTF8ToTStr(openmode).c_str()) == 0; #else #ifdef ANDROID - if (StringBeginsWith(filename, "content://")) - { - // The Java method which OpenAndroidContent passes the mode to does not support the b specifier. - // Since we're on POSIX, it's fine to just remove the b. - std::string mode_without_b(openmode); - mode_without_b.erase(std::remove(mode_without_b.begin(), mode_without_b.end(), 'b'), - mode_without_b.end()); - m_file = fdopen(OpenAndroidContent(filename, mode_without_b), mode_without_b.c_str()); - } + if (IsPathAndroidContent(filename)) + m_file = fdopen(OpenAndroidContent(filename, OpenModeToAndroid(openmode)), openmode); else #endif - { m_file = std::fopen(filename.c_str(), openmode); - } + m_good = m_file != nullptr; #endif From 73f013e3cce5429dc8bbfbd5e1fc073a87c8b0b8 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Wed, 4 Nov 2020 23:09:00 +0100 Subject: [PATCH 2/6] Android: Use storage access framework for custom SD card paths This is part of my efforts to add support for scoped storage. --- .../settings/ui/SettingsActivity.java | 26 +++++++++++++++++-- .../features/settings/ui/SettingsAdapter.java | 22 +++++++++++----- 2 files changed, 40 insertions(+), 8 deletions(-) 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..6486b0353b 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,31 @@ 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); + 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 8873a668fb..f8e6d312b7 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; @@ -288,21 +291,28 @@ public final class SettingsAdapter extends RecyclerView.Adapter 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) From 62e6bedd25a8c7333c1dd74b54bbf7014494ca8e Mon Sep 17 00:00:00 2001 From: JosJuice Date: Wed, 4 Nov 2020 23:09:00 +0100 Subject: [PATCH 3/6] Android: Show warning after picking file with wrong extension --- .../settings/ui/SettingsActivity.java | 8 +- .../dolphinemu/ui/main/MainActivity.java | 4 +- .../dolphinemu/ui/main/TvMainActivity.java | 4 +- .../dolphinemu/utils/ContentHandler.java | 31 +++++++- .../dolphinemu/utils/FileBrowserHelper.java | 76 +++++++++++++++++++ .../app/src/main/res/values/strings.xml | 5 ++ 6 files changed, 119 insertions(+), 9 deletions(-) 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 6486b0353b..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 @@ -175,12 +175,14 @@ public final class SettingsActivity extends AppCompatActivity implements Setting 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); - getContentResolver().takePersistableUriPermission(uri, takeFlags); - getFragment().getAdapter().onFilePickerConfirmation(uri.toString()); + FileBrowserHelper.runAfterExtensionCheck(this, uri, FileBrowserHelper.RAW_EXTENSION, () -> + { + getContentResolver().takePersistableUriPermission(uri, takeFlags); + getFragment().getAdapter().onFilePickerConfirmation(uri.toString()); + }); } else { 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 dc4472a212..7cf7c24253 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 org.dolphinemu.dolphinemu.DolphinApplication; @@ -14,8 +19,7 @@ public class ContentHandler { try { - return DolphinApplication.getAppContext().getContentResolver() - .openFileDescriptor(Uri.parse(uri), mode).detachFd(); + return getContentResolver().openFileDescriptor(Uri.parse(uri), mode).detachFd(); } // Some content providers throw IllegalArgumentException for invalid modes, // despite the documentation saying that invalid modes result in a FileNotFoundException @@ -29,8 +33,7 @@ public class ContentHandler { try { - ContentResolver resolver = DolphinApplication.getAppContext().getContentResolver(); - return DocumentsContract.deleteDocument(resolver, Uri.parse(uri)); + return DocumentsContract.deleteDocument(getContentResolver(), Uri.parse(uri)); } catch (FileNotFoundException e) { @@ -38,4 +41,24 @@ public class ContentHandler return true; } } + + @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); + } + } + + 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..1d0c982b65 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,28 @@ 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.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 +32,9 @@ public final class FileBrowserHelper public static final HashSet RAW_EXTENSION = new HashSet<>(Collections.singletonList( "raw")); + public static final HashSet WAD_EXTENSION = new HashSet<>(Collections.singletonList( + "wad")); + public static void openDirectoryPicker(FragmentActivity activity, HashSet extensions) { Intent i = new Intent(activity, CustomFilePickerActivity.class); @@ -85,4 +93,72 @@ public final class FileBrowserHelper return null; } + + public static void runAfterExtensionCheck(Context context, Uri uri, Set 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 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) + .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 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(); + } } diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 902665ba05..15557ca9d8 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -427,6 +427,11 @@ It can efficiently compress both junk data and encrypted Wii data. Select This Directory + + The selected file does not appear to have a file name extension.\n\nContinue anyway? + The selected file has the file name extension \"%1$s\", but \"%2$s\" was expected.\n\nContinue anyway? + The selected file has the file name extension \"%1$s\", but one of these extensions was expected: %2$s\n\nContinue anyway? + Total Pitch Total Yaw From 713d309386c7c3dbe851bc59ea548a06c28dc4bd Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 7 Nov 2020 15:23:26 +0100 Subject: [PATCH 4/6] Android: Call notifyItemChanged after file picker --- .../dolphinemu/features/settings/ui/SettingsAdapter.java | 9 +++++++-- .../settings/ui/viewholder/FilePickerViewHolder.java | 5 +++-- 2 files changed, 10 insertions(+), 4 deletions(-) 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 f8e6d312b7..16cef7d23b 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 @@ -279,16 +279,18 @@ public final class SettingsAdapter extends RecyclerView.Adapter Date: Sun, 8 Nov 2020 12:37:32 +0100 Subject: [PATCH 5/6] Android: Catch SecurityException in ContentHandler --- .../dolphinemu/utils/ContentHandler.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) 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 7cf7c24253..c2fa96999f 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 @@ -21,6 +21,11 @@ public class ContentHandler { return getContentResolver().openFileDescriptor(Uri.parse(uri), mode).detachFd(); } + catch (SecurityException e) + { + Log.error("Tried to open " + uri + " without permission"); + return -1; + } // Some content providers throw IllegalArgumentException for invalid modes, // despite the documentation saying that invalid modes result in a FileNotFoundException catch (FileNotFoundException | IllegalArgumentException | NullPointerException e) @@ -35,6 +40,11 @@ public class ContentHandler { 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. @@ -53,6 +63,10 @@ public class ContentHandler return cursor.getString(0); } } + catch (SecurityException e) + { + Log.error("Tried to get display name of " + uri + " without permission"); + } return null; } From 161f8c3fad9df97ae4f97b7f45125e5e4c9608a7 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Mon, 9 Nov 2020 23:15:35 +0100 Subject: [PATCH 6/6] Android: Warn when path in config is unavailable Content URIs stop working if Dolphin loses permissions, which happens for instance when reinstalling Dolphin. --- .../activities/EmulationActivity.java | 30 +++++++++++++++++-- .../ui/viewholder/FilePickerViewHolder.java | 19 ++++++++++-- .../dolphinemu/utils/ContentHandler.java | 18 +++++++++++ .../dolphinemu/utils/FileBrowserHelper.java | 12 ++++++++ .../drawable/invalid_setting_background.xml | 9 ++++++ .../app/src/main/res/values/colors.xml | 2 ++ .../app/src/main/res/values/strings.xml | 2 ++ 7 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 Source/Android/app/src/main/res/drawable/invalid_setting_background.xml 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 d8d1329259..2c142f05cb 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 @@ -36,6 +36,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; @@ -170,13 +171,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/viewholder/FilePickerViewHolder.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/FilePickerViewHolder.java index 1bc8000635..0be8c93188 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; @@ -10,6 +11,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 { @@ -19,6 +21,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); @@ -29,6 +33,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 @@ -37,6 +43,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) @@ -45,8 +62,6 @@ public final class FilePickerViewHolder extends SettingViewHolder } else { - String path = mFilePicker.getSelectedValue(getAdapter().getSettings()); - if (TextUtils.isEmpty(path)) { String defaultPathRelative = mFilePicker.getDefaultPathRelativeToUserDirectory(); 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 c2fa96999f..6ec418a1f9 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 @@ -52,6 +52,24 @@ public class ContentHandler } } + 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) { 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 1d0c982b65..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 @@ -14,6 +14,7 @@ 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; @@ -94,6 +95,16 @@ 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 validExtensions, Runnable runnable) { @@ -132,6 +143,7 @@ public final class FileBrowserHelper .setMessage(message) .setPositiveButton(R.string.yes, (dialogInterface, i) -> runnable.run()) .setNegativeButton(R.string.no, null) + .setCancelable(false) .show(); } diff --git a/Source/Android/app/src/main/res/drawable/invalid_setting_background.xml b/Source/Android/app/src/main/res/drawable/invalid_setting_background.xml new file mode 100644 index 0000000000..ad58c2e30a --- /dev/null +++ b/Source/Android/app/src/main/res/drawable/invalid_setting_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/Source/Android/app/src/main/res/values/colors.xml b/Source/Android/app/src/main/res/values/colors.xml index b62030758e..111c03802b 100644 --- a/Source/Android/app/src/main/res/values/colors.xml +++ b/Source/Android/app/src/main/res/values/colors.xml @@ -11,4 +11,6 @@ #444444 + #36ff0000 + diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 15557ca9d8..df0a151220 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -311,6 +311,7 @@ Clear Disabled Other + Continue Anyway Add Folder to Library @@ -431,6 +432,7 @@ It can efficiently compress both junk data and encrypted Wii data. The selected file does not appear to have a file name extension.\n\nContinue anyway? The selected file has the file name extension \"%1$s\", but \"%2$s\" was expected.\n\nContinue anyway? The selected file has the file name extension \"%1$s\", but one of these extensions was expected: %2$s\n\nContinue anyway? + Dolphin does not have permission to access one or more configured paths. Would you like to fix this before starting? Total Pitch