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.
This commit is contained in:
JosJuice 2021-05-08 15:46:52 +02:00
parent 7379450633
commit 820420c5f5
7 changed files with 133 additions and 99 deletions

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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);

View File

@ -481,7 +481,6 @@ It can efficiently compress both junk data and encrypted Wii data.
<!-- Rumble -->
<string name="rumble_not_found">Device rumble not found</string>
<string name="write_permission_needed">You need to allow write access to external storage for the emulator to work</string>
<string name="load_settings">Loading Settings...</string>
<string name="setting_not_runtime_editable">This setting can\'t be changed while a game is running.</string>
<string name="setting_clear_info">Long press a setting to clear it.</string>