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.