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.