From 820420c5f5afb6c7317320fc9bda4ba6cd68b5a6 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 8 May 2021 15:46:52 +0200 Subject: [PATCH 1/6] Android: Use app-specific directory as User folder by default This lets Dolphin function without the user granting access to external storage. We need this for scoped storage compatibility. When scoped storage is not active, we still ask for permission to access external storage the first time the app is started so that we can use the existing dolphin-emu folder if there is one. But if it doesn't exist, or the user denies the permission, or scoped storage is active, the app-specific directory will be used instead. --- .../dolphinemu/ui/main/MainActivity.java | 16 +-- .../dolphinemu/ui/main/TvMainActivity.java | 15 +- .../AfterDirectoryInitializationRunner.java | 6 +- .../utils/DirectoryInitialization.java | 136 +++++++++++------- .../dolphinemu/utils/PermissionsHandler.java | 53 +++---- .../dolphinemu/utils/StartupHandler.java | 5 +- .../app/src/main/res/values/strings.xml | 1 - 7 files changed, 133 insertions(+), 99 deletions(-) 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 46302965b5..f55486d51d 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 @@ -72,7 +72,7 @@ public final class MainActivity extends AppCompatActivity if (savedInstanceState == null) StartupHandler.HandleInit(this); - if (PermissionsHandler.hasWriteAccess(this)) + if (!DirectoryInitialization.isWaitingForWriteAccess(this)) { new AfterDirectoryInitializationRunner() .run(this, false, this::setPlatformTabsAndStartGameFileCacheService); @@ -249,16 +249,14 @@ public final class MainActivity extends AppCompatActivity if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) + if (grantResults[0] == PackageManager.PERMISSION_DENIED) { - DirectoryInitialization.start(this); - new AfterDirectoryInitializationRunner() - .run(this, false, this::setPlatformTabsAndStartGameFileCacheService); - } - else - { - Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_LONG).show(); + PermissionsHandler.setWritePermissionDenied(); } + + DirectoryInitialization.start(this); + new AfterDirectoryInitializationRunner() + .run(this, false, this::setPlatformTabsAndStartGameFileCacheService); } } 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 ac846d4c24..38b8fed094 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 @@ -30,6 +30,7 @@ import org.dolphinemu.dolphinemu.model.GameFile; import org.dolphinemu.dolphinemu.model.TvSettingsItem; import org.dolphinemu.dolphinemu.services.GameFileCacheService; import org.dolphinemu.dolphinemu.ui.platform.Platform; +import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner; import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; import org.dolphinemu.dolphinemu.utils.PermissionsHandler; @@ -287,15 +288,13 @@ public final class TvMainActivity extends FragmentActivity if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) + if (grantResults[0] == PackageManager.PERMISSION_DENIED) { - DirectoryInitialization.start(this); - GameFileCacheService.startLoad(this); - } - else - { - Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_LONG).show(); + PermissionsHandler.setWritePermissionDenied(); } + + DirectoryInitialization.start(this); + GameFileCacheService.startLoad(this); } } @@ -314,7 +313,7 @@ public final class TvMainActivity extends FragmentActivity ArrayObjectAdapter rowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); mGameRows.clear(); - if (PermissionsHandler.hasWriteAccess(this)) + if (!DirectoryInitialization.isWaitingForWriteAccess(this)) { GameFileCacheService.startLoad(this); } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AfterDirectoryInitializationRunner.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AfterDirectoryInitializationRunner.java index 5a7f33f42a..4b07636a64 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AfterDirectoryInitializationRunner.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/AfterDirectoryInitializationRunner.java @@ -62,7 +62,7 @@ public class AfterDirectoryInitializationRunner runnable.run(); } else if (abortOnFailure && - showErrorMessage(context, DirectoryInitialization.getDolphinDirectoriesState(context))) + showErrorMessage(context, DirectoryInitialization.getDolphinDirectoriesState())) { runFinishedCallback(); } @@ -115,10 +115,6 @@ public class AfterDirectoryInitializationRunner { switch (state) { - case EXTERNAL_STORAGE_PERMISSION_NEEDED: - Toast.makeText(context, R.string.write_permission_needed, Toast.LENGTH_LONG).show(); - return true; - case CANT_FIND_EXTERNAL_STORAGE: Toast.makeText(context, R.string.external_storage_not_mounted, Toast.LENGTH_LONG).show(); return true; diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java index 0dc5432c54..c2f6d638df 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java @@ -13,6 +13,7 @@ import android.os.Environment; import android.preference.PreferenceManager; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import org.dolphinemu.dolphinemu.NativeLibrary; @@ -47,7 +48,6 @@ public final class DirectoryInitialization { NOT_YET_INITIALIZED, DOLPHIN_DIRECTORIES_INITIALIZED, - EXTERNAL_STORAGE_PERMISSION_NEEDED, CANT_FIND_EXTERNAL_STORAGE } @@ -65,34 +65,27 @@ public final class DirectoryInitialization { if (directoryState != DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED) { - if (PermissionsHandler.hasWriteAccess(context)) + if (setDolphinUserDirectory(context)) { - if (setDolphinUserDirectory(context)) + initializeInternalStorage(context); + boolean wiimoteIniWritten = initializeExternalStorage(context); + NativeLibrary.Initialize(); + NativeLibrary.ReportStartToAnalytics(); + + areDirectoriesAvailable = true; + + if (wiimoteIniWritten) { - initializeInternalStorage(context); - boolean wiimoteIniWritten = initializeExternalStorage(context); - NativeLibrary.Initialize(); - NativeLibrary.ReportStartToAnalytics(); - - areDirectoriesAvailable = true; - - if (wiimoteIniWritten) - { - // This has to be done after calling NativeLibrary.Initialize(), - // as it relies on the config system - EmulationActivity.updateWiimoteNewIniPreferences(context); - } - - directoryState = DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED; - } - else - { - directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE; + // This has to be done after calling NativeLibrary.Initialize(), + // as it relies on the config system + EmulationActivity.updateWiimoteNewIniPreferences(context); } + + directoryState = DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED; } else { - directoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED; + directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE; } } @@ -100,16 +93,29 @@ public final class DirectoryInitialization sendBroadcastState(directoryState, context); } + @Nullable + private static File getLegacyUserDirectoryPath() + { + File externalPath = Environment.getExternalStorageDirectory(); + if (externalPath == null) + return null; + + return new File(externalPath, "dolphin-emu"); + } + private static boolean setDolphinUserDirectory(Context context) { if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) return false; - File externalPath = Environment.getExternalStorageDirectory(); - if (externalPath == null) + File path = preferLegacyUserDirectory(context) && PermissionsHandler.hasWriteAccess(context) ? + getLegacyUserDirectoryPath() : context.getExternalFilesDir(null); + + if (path == null) return false; - userPath = externalPath.getAbsolutePath() + "/dolphin-emu"; + userPath = path.getAbsolutePath(); + Log.debug("[DirectoryInitialization] User Dir: " + userPath); NativeLibrary.SetUserDirectory(userPath); @@ -207,7 +213,8 @@ public final class DirectoryInitialization public static boolean shouldStart(Context context) { return !isDolphinDirectoryInitializationRunning.get() && - getDolphinDirectoriesState(context) == DirectoryInitializationState.NOT_YET_INITIALIZED; + getDolphinDirectoriesState() == DirectoryInitializationState.NOT_YET_INITIALIZED && + !isWaitingForWriteAccess(context); } public static boolean areDolphinDirectoriesReady() @@ -215,17 +222,9 @@ public final class DirectoryInitialization return directoryState == DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED; } - public static DirectoryInitializationState getDolphinDirectoriesState(Context context) + public static DirectoryInitializationState getDolphinDirectoriesState() { - if (directoryState == DirectoryInitializationState.NOT_YET_INITIALIZED && - !PermissionsHandler.hasWriteAccess(context)) - { - return DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED; - } - else - { - return directoryState; - } + return directoryState; } public static String getUserDirectory() @@ -335,11 +334,6 @@ public final class DirectoryInitialization } } - public static boolean isExternalStorageLegacy() - { - return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || Environment.isExternalStorageLegacy(); - } - public static boolean preferOldFolderPicker(Context context) { // As of January 2021, ACTION_OPEN_DOCUMENT_TREE seems to be broken on the Nvidia Shield TV @@ -347,16 +341,60 @@ public final class DirectoryInitialization // for the time being - Android 11 hasn't been released for this device. We have an explicit // check for Android 11 below in hopes that Nvidia will fix this before releasing Android 11. // - // No Android TV device other than the Nvidia Shield TV is known to have an implementation - // of ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE that even launches, but "fortunately" - // for us, the Nvidia Shield TV is the only Android TV device in existence so far that can - // run Dolphin at all (due to the 64-bit requirement), so we can ignore this problem. + // No Android TV device other than the Nvidia Shield TV is known to have an implementation of + // ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE that even launches, but "fortunately", no + // Android TV device other than the Shield TV is known to be able to run Dolphin (either due to + // the 64-bit requirement or due to the GLES 3.0 requirement), so we can ignore this problem. // // All phones which are running a compatible version of Android support ACTION_OPEN_DOCUMENT and - // ACTION_OPEN_DOCUMENT_TREE, as this is required by the Android CTS (unlike with Android TV). + // ACTION_OPEN_DOCUMENT_TREE, as this is required by the mobile Android CTS (unlike Android TV). - return Build.VERSION.SDK_INT < Build.VERSION_CODES.R && isExternalStorageLegacy() && - TvUtil.isLeanback(context); + return Build.VERSION.SDK_INT < Build.VERSION_CODES.R && + PermissionsHandler.isExternalStorageLegacy() && TvUtil.isLeanback(context); + } + + private static boolean isExternalFilesDirEmpty(Context context) + { + File dir = context.getExternalFilesDir(null); + if (dir == null) + return false; // External storage not available + + File[] contents = dir.listFiles(); + return contents == null || contents.length == 0; + } + + private static boolean legacyUserDirectoryExists() + { + try + { + return getLegacyUserDirectoryPath().exists(); + } + catch (SecurityException e) + { + // Most likely we don't have permission to read external storage. + // Return true so that external storage permissions will be requested. + // + // Strangely, we don't seem to trigger this case in practice, even with no permissions... + // But this only makes things more convenient for users, so no harm done. + + return true; + } + } + + private static boolean preferLegacyUserDirectory(Context context) + { + return PermissionsHandler.isExternalStorageLegacy() && + !PermissionsHandler.isWritePermissionDenied() && + isExternalFilesDirEmpty(context) && legacyUserDirectoryExists(); + } + + public static boolean isWaitingForWriteAccess(Context context) + { + // This first check is only for performance, not correctness + if (getDolphinDirectoriesState() != DirectoryInitializationState.NOT_YET_INITIALIZED) + return false; + + return preferLegacyUserDirectory(context) && !PermissionsHandler.hasWriteAccess(context); } private static native void CreateUserDirectories(); diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java index 63a70870bf..a4c69281f2 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/PermissionsHandler.java @@ -2,10 +2,10 @@ package org.dolphinemu.dolphinemu.utils; -import android.annotation.TargetApi; import android.content.Context; import android.content.pm.PackageManager; import android.os.Build; +import android.os.Environment; import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentActivity; @@ -15,38 +15,41 @@ import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE; public class PermissionsHandler { public static final int REQUEST_CODE_WRITE_PERMISSION = 500; + private static boolean sWritePermissionDenied = false; - @TargetApi(Build.VERSION_CODES.M) - public static boolean checkWritePermission(final FragmentActivity activity) + public static void requestWritePermission(final FragmentActivity activity) { if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M) - { - return true; - } + return; - int hasWritePermission = ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE); - - if (hasWritePermission != PackageManager.PERMISSION_GRANTED) - { - // We only care about displaying the "Don't ask again" check and can ignore the result. - // Previous toasts already explained the rationale. - activity.shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE); - activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, - REQUEST_CODE_WRITE_PERMISSION); - return false; - } - - return true; + activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, + REQUEST_CODE_WRITE_PERMISSION); } public static boolean hasWriteAccess(Context context) { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - { - int hasWritePermission = ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE); - return hasWritePermission == PackageManager.PERMISSION_GRANTED; - } + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + return true; - return true; + if (!isExternalStorageLegacy()) + return false; + + int hasWritePermission = ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE); + return hasWritePermission == PackageManager.PERMISSION_GRANTED; + } + + public static boolean isExternalStorageLegacy() + { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || Environment.isExternalStorageLegacy(); + } + + public static void setWritePermissionDenied() + { + sWritePermissionDenied = true; + } + + public static boolean isWritePermissionDenied() + { + return sWritePermissionDenied; } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/StartupHandler.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/StartupHandler.java index a3ce3d2802..547d6934d1 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/StartupHandler.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/StartupHandler.java @@ -22,8 +22,9 @@ public final class StartupHandler public static void HandleInit(FragmentActivity parent) { - // Ask the user to grant write permission if it's not already granted - PermissionsHandler.checkWritePermission(parent); + // Ask the user to grant write permission if relevant and not already granted + if (DirectoryInitialization.isWaitingForWriteAccess(parent)) + PermissionsHandler.requestWritePermission(parent); // Ask the user if he wants to enable analytics if we haven't yet. Analytics.checkAnalyticsInit(parent); diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 2cb4b45643..897aa639e5 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -481,7 +481,6 @@ It can efficiently compress both junk data and encrypted Wii data. Device rumble not found - You need to allow write access to external storage for the emulator to work Loading Settings... This setting can\'t be changed while a game is running. Long press a setting to clear it. From 3adb07ac74aa8d5d2ee46dc71990c464b16e1bdc Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 8 May 2021 16:10:39 +0200 Subject: [PATCH 2/6] Android: Show a dialog for scoped storage incompatible settings The following settings are currently not SAF compatible, and might never be due to the performance impact: Dump Path Load Path Resource Pack Path Wii NAND Root This commit makes us show a message to the user if they try to change one of these settings while scoped storage is active. I don't want to entirely remove the settings from being listed in the settings activity, because it's important that the user is able to reset them if they were set to something custom in a previous version of Dolphin. --- .../features/settings/ui/SettingsAdapter.java | 13 ++++++++++++- Source/Android/app/src/main/res/values/strings.xml | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) 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 753dddf522..a26be57754 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 @@ -44,6 +44,7 @@ import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SubmenuViewHold import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; import org.dolphinemu.dolphinemu.utils.FileBrowserHelper; import org.dolphinemu.dolphinemu.utils.Log; +import org.dolphinemu.dolphinemu.utils.PermissionsHandler; import java.io.File; import java.io.IOException; @@ -300,7 +301,17 @@ public final class SettingsAdapter extends RecyclerView.Adapter dialog.dismiss()); + builder.show(); + } + else + { + FileBrowserHelper.openDirectoryPicker(mView.getActivity(), FileBrowserHelper.GAME_EXTENSIONS); + } } public void onFilePickerFileClick(SettingsItem item, int position) diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 897aa639e5..6671023dea 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -481,6 +481,7 @@ It can efficiently compress both junk data and encrypted Wii data. Device rumble not found + Due to the Scoped Storage policy in Android 11 and newer, you can\'t change this path. Loading Settings... This setting can\'t be changed while a game is running. Long press a setting to clear it. From 6119f7762644ade5073e3df54add93e708864073 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 8 May 2021 16:15:04 +0200 Subject: [PATCH 3/6] Android: Increment targetSdkVersion to 30 (Android 11) This enables scoped storage for new Dolphin installs on Android 11 and up (along with a few other changes in behavior which unlike scoped storage are uncontroversial). Existing installs are unaffected. We have to do this in order to be able to release updates on Google Play from November 2021 and on. --- Source/Android/app/build.gradle | 2 +- Source/Android/app/src/main/AndroidManifest.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Android/app/build.gradle b/Source/Android/app/build.gradle index 57715c02dc..b798bc08a9 100644 --- a/Source/Android/app/build.gradle +++ b/Source/Android/app/build.gradle @@ -25,7 +25,7 @@ android { // TODO If this is ever modified, change application_id in strings.xml applicationId "org.dolphinemu.dolphinemu" minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 30 versionCode(getBuildVersionCode()) diff --git a/Source/Android/app/src/main/AndroidManifest.xml b/Source/Android/app/src/main/AndroidManifest.xml index 9074dfb3da..3b68ccebea 100644 --- a/Source/Android/app/src/main/AndroidManifest.xml +++ b/Source/Android/app/src/main/AndroidManifest.xml @@ -35,6 +35,7 @@ android:label="@string/app_name" android:icon="@drawable/ic_launcher" android:requestLegacyExternalStorage="true" + android:preserveLegacyExternalStorage="true" android:allowBackup="false" android:supportsRtl="true" android:isGame="true" From 258832dad463e12ef0d281454cc6cd9812e84d6e Mon Sep 17 00:00:00 2001 From: JosJuice Date: Thu, 6 May 2021 22:04:16 +0200 Subject: [PATCH 4/6] Android: Enable backup of external app-specific directory --- Source/Android/app/src/main/AndroidManifest.xml | 5 +++-- .../app/src/main/res/xml/backup_rules.xml | 6 ++++++ .../src/main/res/xml/backup_rules_api_31.xml | 17 +++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 Source/Android/app/src/main/res/xml/backup_rules.xml create mode 100644 Source/Android/app/src/main/res/xml/backup_rules_api_31.xml diff --git a/Source/Android/app/src/main/AndroidManifest.xml b/Source/Android/app/src/main/AndroidManifest.xml index 3b68ccebea..4a1798d975 100644 --- a/Source/Android/app/src/main/AndroidManifest.xml +++ b/Source/Android/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ + diff --git a/Source/Android/app/src/main/res/xml/backup_rules.xml b/Source/Android/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000000..1cef48295a --- /dev/null +++ b/Source/Android/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/Source/Android/app/src/main/res/xml/backup_rules_api_31.xml b/Source/Android/app/src/main/res/xml/backup_rules_api_31.xml new file mode 100644 index 0000000000..345a41cd65 --- /dev/null +++ b/Source/Android/app/src/main/res/xml/backup_rules_api_31.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + From 9c8bb24293820c9be973e085511e24808b9bf707 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sun, 9 May 2021 15:23:11 +0200 Subject: [PATCH 5/6] Android: Add a "user data" screen To make it clearer for users where Dolphin is storing user data, now that there's more than one possible place. --- .../Android/app/src/main/AndroidManifest.xml | 6 +++ .../activities/UserDataActivity.java | 45 +++++++++++++++++++ .../ui/SettingsFragmentPresenter.java | 3 ++ .../utils/DirectoryInitialization.java | 11 ++++- .../main/res/layout/activity_user_data.xml | 45 +++++++++++++++++++ .../app/src/main/res/values/strings.xml | 6 +++ 6 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.java create mode 100644 Source/Android/app/src/main/res/layout/activity_user_data.xml diff --git a/Source/Android/app/src/main/AndroidManifest.xml b/Source/Android/app/src/main/AndroidManifest.xml index 4a1798d975..15bd0ee7fc 100644 --- a/Source/Android/app/src/main/AndroidManifest.xml +++ b/Source/Android/app/src/main/AndroidManifest.xml @@ -120,6 +120,12 @@ android:exported="false" android:theme="@style/DolphinBase" /> + + diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.java new file mode 100644 index 0000000000..79fcc09123 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.java @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.activities; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; + +import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; + +public class UserDataActivity extends AppCompatActivity +{ + public static void launch(Context context) + { + Intent launcher = new Intent(context, UserDataActivity.class); + context.startActivity(launcher); + } + + @Override + protected void onCreate(Bundle savedInstanceState) + { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_user_data); + + TextView textType = findViewById(R.id.text_type); + TextView textPath = findViewById(R.id.text_path); + TextView textAndroid11 = findViewById(R.id.text_android_11); + + textType.setText(DirectoryInitialization.isUsingLegacyUserDirectory() ? + R.string.user_data_old_location : R.string.user_data_new_location); + + textPath.setText(DirectoryInitialization.getUserDirectory()); + + boolean show_android_11_text = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && + !DirectoryInitialization.isUsingLegacyUserDirectory(); + textAndroid11.setVisibility(show_android_11_text ? View.VISIBLE : View.GONE); + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.java index d91faa775e..c718bab15e 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentPresenter.java @@ -10,6 +10,7 @@ import android.text.TextUtils; import org.dolphinemu.dolphinemu.DolphinApplication; import org.dolphinemu.dolphinemu.NativeLibrary; import org.dolphinemu.dolphinemu.R; +import org.dolphinemu.dolphinemu.activities.UserDataActivity; import org.dolphinemu.dolphinemu.features.settings.model.AbstractIntSetting; import org.dolphinemu.dolphinemu.features.settings.model.AbstractStringSetting; import org.dolphinemu.dolphinemu.features.settings.model.AdHocBooleanSetting; @@ -252,6 +253,8 @@ public final class SettingsFragmentPresenter sl.add(new SubmenuSetting(mContext, R.string.advanced_submenu, MenuTag.CONFIG_ADVANCED)); sl.add(new SubmenuSetting(mContext, R.string.log_submenu, MenuTag.CONFIG_LOG)); sl.add(new SubmenuSetting(mContext, R.string.debug_submenu, MenuTag.DEBUG)); + sl.add(new RunRunnable(mContext, R.string.user_data_submenu, 0, 0, 0, + () -> UserDataActivity.launch(mContext))); } private void addGeneralSettings(ArrayList sl) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java index c2f6d638df..48963052dc 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/DirectoryInitialization.java @@ -43,6 +43,7 @@ public final class DirectoryInitialization private static volatile boolean areDirectoriesAvailable = false; private static String userPath; private static AtomicBoolean isDolphinDirectoryInitializationRunning = new AtomicBoolean(false); + private static boolean isUsingLegacyUserDirectory = false; public enum DirectoryInitializationState { @@ -108,7 +109,10 @@ public final class DirectoryInitialization if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) return false; - File path = preferLegacyUserDirectory(context) && PermissionsHandler.hasWriteAccess(context) ? + isUsingLegacyUserDirectory = + preferLegacyUserDirectory(context) && PermissionsHandler.hasWriteAccess(context); + + File path = isUsingLegacyUserDirectory ? getLegacyUserDirectoryPath() : context.getExternalFilesDir(null); if (path == null) @@ -388,6 +392,11 @@ public final class DirectoryInitialization isExternalFilesDirEmpty(context) && legacyUserDirectoryExists(); } + public static boolean isUsingLegacyUserDirectory() + { + return isUsingLegacyUserDirectory; + } + public static boolean isWaitingForWriteAccess(Context context) { // This first check is only for performance, not correctness diff --git a/Source/Android/app/src/main/res/layout/activity_user_data.xml b/Source/Android/app/src/main/res/layout/activity_user_data.xml new file mode 100644 index 0000000000..dc7aa50fb9 --- /dev/null +++ b/Source/Android/app/src/main/res/layout/activity_user_data.xml @@ -0,0 +1,45 @@ + + + + + + + + + + diff --git a/Source/Android/app/src/main/res/values/strings.xml b/Source/Android/app/src/main/res/values/strings.xml index 6671023dea..5020e1b93e 100644 --- a/Source/Android/app/src/main/res/values/strings.xml +++ b/Source/Android/app/src/main/res/values/strings.xml @@ -324,6 +324,12 @@ Jit Branch Disabled Jit Register Cache Disabled + + User Data + Your user data is stored in a location which will not be deleted when you uninstall the app: + Your user data is stored in a location which will be deleted when you uninstall the app: + Because you\'re using Android 11 or newer, you can\'t access this location using file manager apps. However, you can access it using the file manager in the system settings, or by connecting your device to a PC. + Yes No From 4e7aaba77aa9a27ba3e17c5cea4bf59fe729165e Mon Sep 17 00:00:00 2001 From: JosJuice Date: Thu, 12 Aug 2021 19:27:50 +0200 Subject: [PATCH 6/6] Android: Add button for opening system file manager Apparently some phones (at least some from Samsung) don't expose the system file manager in the system settings despite it being the only on-device file manager that can open app-specific directories... --- .../activities/UserDataActivity.java | 36 ++++++++++++++++++- .../main/res/layout/activity_user_data.xml | 13 ++++++- .../app/src/main/res/values/strings.xml | 4 ++- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.java index 79fcc09123..532dcc8962 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/UserDataActivity.java @@ -2,19 +2,22 @@ package org.dolphinemu.dolphinemu.activities; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.view.View; +import android.widget.Button; import android.widget.TextView; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import org.dolphinemu.dolphinemu.R; import org.dolphinemu.dolphinemu.utils.DirectoryInitialization; -public class UserDataActivity extends AppCompatActivity +public class UserDataActivity extends AppCompatActivity implements View.OnClickListener { public static void launch(Context context) { @@ -32,6 +35,7 @@ public class UserDataActivity extends AppCompatActivity TextView textType = findViewById(R.id.text_type); TextView textPath = findViewById(R.id.text_path); TextView textAndroid11 = findViewById(R.id.text_android_11); + Button buttonOpenSystemFileManager = findViewById(R.id.button_open_system_file_manager); textType.setText(DirectoryInitialization.isUsingLegacyUserDirectory() ? R.string.user_data_old_location : R.string.user_data_new_location); @@ -41,5 +45,35 @@ public class UserDataActivity extends AppCompatActivity boolean show_android_11_text = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !DirectoryInitialization.isUsingLegacyUserDirectory(); textAndroid11.setVisibility(show_android_11_text ? View.VISIBLE : View.GONE); + + boolean show_file_manager_button = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R; + buttonOpenSystemFileManager.setVisibility(show_file_manager_button ? View.VISIBLE : View.GONE); + + buttonOpenSystemFileManager.setOnClickListener(this); + } + + @Override + public void onClick(View v) + { + try + { + startActivity(getFileManagerIntent()); + } + catch (ActivityNotFoundException e) + { + new AlertDialog.Builder(this, R.style.DolphinDialogBase) + .setMessage(R.string.user_data_open_system_file_manager_failed) + .setPositiveButton(R.string.ok, null) + .show(); + } + } + + private Intent getFileManagerIntent() + { + // Fragile, but some phones don't expose the system file manager in any better way + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClassName("com.android.documentsui", "com.android.documentsui.files.FilesActivity"); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return intent; } } diff --git a/Source/Android/app/src/main/res/layout/activity_user_data.xml b/Source/Android/app/src/main/res/layout/activity_user_data.xml index dc7aa50fb9..4c8d3bfe45 100644 --- a/Source/Android/app/src/main/res/layout/activity_user_data.xml +++ b/Source/Android/app/src/main/res/layout/activity_user_data.xml @@ -39,7 +39,18 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/text_path" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/button_open_system_file_manager" app:layout_constraintWidth_max="400dp" /> +