Merge pull request #9696 from JosJuice/android-scoped-storage
Android: Scoped storage [To be merged in October]
This commit is contained in:
commit
6caf51f966
|
@ -25,7 +25,7 @@ android {
|
||||||
// TODO If this is ever modified, change application_id in strings.xml
|
// TODO If this is ever modified, change application_id in strings.xml
|
||||||
applicationId "org.dolphinemu.dolphinemu"
|
applicationId "org.dolphinemu.dolphinemu"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 29
|
targetSdkVersion 30
|
||||||
|
|
||||||
versionCode(getBuildVersionCode())
|
versionCode(getBuildVersionCode())
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
package="org.dolphinemu.dolphinemu">
|
package="org.dolphinemu.dolphinemu">
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
|
@ -30,12 +29,15 @@
|
||||||
android:name="android.permission.VIBRATE"
|
android:name="android.permission.VIBRATE"
|
||||||
android:required="false"/>
|
android:required="false"/>
|
||||||
|
|
||||||
|
<!-- Once compileSdkVersion is 31, add: android:dataExtractionRules="@xml/backup_rules_api_31" -->
|
||||||
<application
|
<application
|
||||||
android:name=".DolphinApplication"
|
android:name=".DolphinApplication"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:allowBackup="false"
|
android:preserveLegacyExternalStorage="true"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:isGame="true"
|
android:isGame="true"
|
||||||
android:banner="@drawable/banner_tv">
|
android:banner="@drawable/banner_tv">
|
||||||
|
@ -118,6 +120,12 @@
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@style/DolphinBase" />
|
android:theme="@style/DolphinBase" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".activities.UserDataActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/user_data_submenu"
|
||||||
|
android:theme="@style/DolphinSettingsBase" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".utils.DirectoryInitialization"
|
android:name=".utils.DirectoryInitialization"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
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 implements View.OnClickListener
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,6 +44,7 @@ import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SubmenuViewHold
|
||||||
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization;
|
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization;
|
||||||
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;
|
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;
|
||||||
import org.dolphinemu.dolphinemu.utils.Log;
|
import org.dolphinemu.dolphinemu.utils.Log;
|
||||||
|
import org.dolphinemu.dolphinemu.utils.PermissionsHandler;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -300,7 +301,17 @@ public final class SettingsAdapter extends RecyclerView.Adapter<SettingViewHolde
|
||||||
mClickedItem = item;
|
mClickedItem = item;
|
||||||
mClickedPosition = position;
|
mClickedPosition = position;
|
||||||
|
|
||||||
FileBrowserHelper.openDirectoryPicker(mView.getActivity(), FileBrowserHelper.GAME_EXTENSIONS);
|
if (!PermissionsHandler.isExternalStorageLegacy())
|
||||||
|
{
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(mContext, R.style.DolphinDialogBase);
|
||||||
|
builder.setMessage(R.string.path_not_changeable_scoped_storage);
|
||||||
|
builder.setPositiveButton(R.string.ok, (dialog, i) -> dialog.dismiss());
|
||||||
|
builder.show();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FileBrowserHelper.openDirectoryPicker(mView.getActivity(), FileBrowserHelper.GAME_EXTENSIONS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onFilePickerFileClick(SettingsItem item, int position)
|
public void onFilePickerFileClick(SettingsItem item, int position)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import android.text.TextUtils;
|
||||||
import org.dolphinemu.dolphinemu.DolphinApplication;
|
import org.dolphinemu.dolphinemu.DolphinApplication;
|
||||||
import org.dolphinemu.dolphinemu.NativeLibrary;
|
import org.dolphinemu.dolphinemu.NativeLibrary;
|
||||||
import org.dolphinemu.dolphinemu.R;
|
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.AbstractIntSetting;
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.AbstractStringSetting;
|
import org.dolphinemu.dolphinemu.features.settings.model.AbstractStringSetting;
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.AdHocBooleanSetting;
|
import org.dolphinemu.dolphinemu.features.settings.model.AdHocBooleanSetting;
|
||||||
|
@ -256,6 +257,8 @@ public final class SettingsFragmentPresenter
|
||||||
sl.add(new SubmenuSetting(mContext, R.string.advanced_submenu, MenuTag.CONFIG_ADVANCED));
|
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.log_submenu, MenuTag.CONFIG_LOG));
|
||||||
sl.add(new SubmenuSetting(mContext, R.string.debug_submenu, MenuTag.DEBUG));
|
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<SettingsItem> sl)
|
private void addGeneralSettings(ArrayList<SettingsItem> sl)
|
||||||
|
|
|
@ -72,7 +72,7 @@ public final class MainActivity extends AppCompatActivity
|
||||||
if (savedInstanceState == null)
|
if (savedInstanceState == null)
|
||||||
StartupHandler.HandleInit(this);
|
StartupHandler.HandleInit(this);
|
||||||
|
|
||||||
if (PermissionsHandler.hasWriteAccess(this))
|
if (!DirectoryInitialization.isWaitingForWriteAccess(this))
|
||||||
{
|
{
|
||||||
new AfterDirectoryInitializationRunner()
|
new AfterDirectoryInitializationRunner()
|
||||||
.run(this, false, this::setPlatformTabsAndStartGameFileCacheService);
|
.run(this, false, this::setPlatformTabsAndStartGameFileCacheService);
|
||||||
|
@ -249,16 +249,14 @@ public final class MainActivity extends AppCompatActivity
|
||||||
|
|
||||||
if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION)
|
if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION)
|
||||||
{
|
{
|
||||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED)
|
if (grantResults[0] == PackageManager.PERMISSION_DENIED)
|
||||||
{
|
{
|
||||||
DirectoryInitialization.start(this);
|
PermissionsHandler.setWritePermissionDenied();
|
||||||
new AfterDirectoryInitializationRunner()
|
|
||||||
.run(this, false, this::setPlatformTabsAndStartGameFileCacheService);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DirectoryInitialization.start(this);
|
||||||
|
new AfterDirectoryInitializationRunner()
|
||||||
|
.run(this, false, this::setPlatformTabsAndStartGameFileCacheService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ import org.dolphinemu.dolphinemu.model.GameFile;
|
||||||
import org.dolphinemu.dolphinemu.model.TvSettingsItem;
|
import org.dolphinemu.dolphinemu.model.TvSettingsItem;
|
||||||
import org.dolphinemu.dolphinemu.services.GameFileCacheService;
|
import org.dolphinemu.dolphinemu.services.GameFileCacheService;
|
||||||
import org.dolphinemu.dolphinemu.ui.platform.Platform;
|
import org.dolphinemu.dolphinemu.ui.platform.Platform;
|
||||||
|
import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner;
|
||||||
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization;
|
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization;
|
||||||
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;
|
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper;
|
||||||
import org.dolphinemu.dolphinemu.utils.PermissionsHandler;
|
import org.dolphinemu.dolphinemu.utils.PermissionsHandler;
|
||||||
|
@ -287,15 +288,13 @@ public final class TvMainActivity extends FragmentActivity
|
||||||
|
|
||||||
if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION)
|
if (requestCode == PermissionsHandler.REQUEST_CODE_WRITE_PERMISSION)
|
||||||
{
|
{
|
||||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED)
|
if (grantResults[0] == PackageManager.PERMISSION_DENIED)
|
||||||
{
|
{
|
||||||
DirectoryInitialization.start(this);
|
PermissionsHandler.setWritePermissionDenied();
|
||||||
GameFileCacheService.startLoad(this);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Toast.makeText(this, R.string.write_permission_needed, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DirectoryInitialization.start(this);
|
||||||
|
GameFileCacheService.startLoad(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -314,7 +313,7 @@ public final class TvMainActivity extends FragmentActivity
|
||||||
ArrayObjectAdapter rowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
|
ArrayObjectAdapter rowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
|
||||||
mGameRows.clear();
|
mGameRows.clear();
|
||||||
|
|
||||||
if (PermissionsHandler.hasWriteAccess(this))
|
if (!DirectoryInitialization.isWaitingForWriteAccess(this))
|
||||||
{
|
{
|
||||||
GameFileCacheService.startLoad(this);
|
GameFileCacheService.startLoad(this);
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ public class AfterDirectoryInitializationRunner
|
||||||
runnable.run();
|
runnable.run();
|
||||||
}
|
}
|
||||||
else if (abortOnFailure &&
|
else if (abortOnFailure &&
|
||||||
showErrorMessage(context, DirectoryInitialization.getDolphinDirectoriesState(context)))
|
showErrorMessage(context, DirectoryInitialization.getDolphinDirectoriesState()))
|
||||||
{
|
{
|
||||||
runFinishedCallback();
|
runFinishedCallback();
|
||||||
}
|
}
|
||||||
|
@ -115,10 +115,6 @@ public class AfterDirectoryInitializationRunner
|
||||||
{
|
{
|
||||||
switch (state)
|
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:
|
case CANT_FIND_EXTERNAL_STORAGE:
|
||||||
Toast.makeText(context, R.string.external_storage_not_mounted, Toast.LENGTH_LONG).show();
|
Toast.makeText(context, R.string.external_storage_not_mounted, Toast.LENGTH_LONG).show();
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -13,6 +13,7 @@ import android.os.Environment;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||||
|
|
||||||
import org.dolphinemu.dolphinemu.NativeLibrary;
|
import org.dolphinemu.dolphinemu.NativeLibrary;
|
||||||
|
@ -42,12 +43,12 @@ public final class DirectoryInitialization
|
||||||
private static volatile boolean areDirectoriesAvailable = false;
|
private static volatile boolean areDirectoriesAvailable = false;
|
||||||
private static String userPath;
|
private static String userPath;
|
||||||
private static AtomicBoolean isDolphinDirectoryInitializationRunning = new AtomicBoolean(false);
|
private static AtomicBoolean isDolphinDirectoryInitializationRunning = new AtomicBoolean(false);
|
||||||
|
private static boolean isUsingLegacyUserDirectory = false;
|
||||||
|
|
||||||
public enum DirectoryInitializationState
|
public enum DirectoryInitializationState
|
||||||
{
|
{
|
||||||
NOT_YET_INITIALIZED,
|
NOT_YET_INITIALIZED,
|
||||||
DOLPHIN_DIRECTORIES_INITIALIZED,
|
DOLPHIN_DIRECTORIES_INITIALIZED,
|
||||||
EXTERNAL_STORAGE_PERMISSION_NEEDED,
|
|
||||||
CANT_FIND_EXTERNAL_STORAGE
|
CANT_FIND_EXTERNAL_STORAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,34 +66,27 @@ public final class DirectoryInitialization
|
||||||
{
|
{
|
||||||
if (directoryState != DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED)
|
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);
|
// This has to be done after calling NativeLibrary.Initialize(),
|
||||||
boolean wiimoteIniWritten = initializeExternalStorage(context);
|
// as it relies on the config system
|
||||||
NativeLibrary.Initialize();
|
EmulationActivity.updateWiimoteNewIniPreferences(context);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
directoryState = DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
directoryState = DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED;
|
directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,16 +94,32 @@ public final class DirectoryInitialization
|
||||||
sendBroadcastState(directoryState, context);
|
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)
|
private static boolean setDolphinUserDirectory(Context context)
|
||||||
{
|
{
|
||||||
if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()))
|
if (!Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
File externalPath = Environment.getExternalStorageDirectory();
|
isUsingLegacyUserDirectory =
|
||||||
if (externalPath == null)
|
preferLegacyUserDirectory(context) && PermissionsHandler.hasWriteAccess(context);
|
||||||
|
|
||||||
|
File path = isUsingLegacyUserDirectory ?
|
||||||
|
getLegacyUserDirectoryPath() : context.getExternalFilesDir(null);
|
||||||
|
|
||||||
|
if (path == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
userPath = externalPath.getAbsolutePath() + "/dolphin-emu";
|
userPath = path.getAbsolutePath();
|
||||||
|
|
||||||
Log.debug("[DirectoryInitialization] User Dir: " + userPath);
|
Log.debug("[DirectoryInitialization] User Dir: " + userPath);
|
||||||
NativeLibrary.SetUserDirectory(userPath);
|
NativeLibrary.SetUserDirectory(userPath);
|
||||||
|
|
||||||
|
@ -207,7 +217,8 @@ public final class DirectoryInitialization
|
||||||
public static boolean shouldStart(Context context)
|
public static boolean shouldStart(Context context)
|
||||||
{
|
{
|
||||||
return !isDolphinDirectoryInitializationRunning.get() &&
|
return !isDolphinDirectoryInitializationRunning.get() &&
|
||||||
getDolphinDirectoriesState(context) == DirectoryInitializationState.NOT_YET_INITIALIZED;
|
getDolphinDirectoriesState() == DirectoryInitializationState.NOT_YET_INITIALIZED &&
|
||||||
|
!isWaitingForWriteAccess(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean areDolphinDirectoriesReady()
|
public static boolean areDolphinDirectoriesReady()
|
||||||
|
@ -215,17 +226,9 @@ public final class DirectoryInitialization
|
||||||
return directoryState == DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED;
|
return directoryState == DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DirectoryInitializationState getDolphinDirectoriesState(Context context)
|
public static DirectoryInitializationState getDolphinDirectoriesState()
|
||||||
{
|
{
|
||||||
if (directoryState == DirectoryInitializationState.NOT_YET_INITIALIZED &&
|
return directoryState;
|
||||||
!PermissionsHandler.hasWriteAccess(context))
|
|
||||||
{
|
|
||||||
return DirectoryInitializationState.EXTERNAL_STORAGE_PERMISSION_NEEDED;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return directoryState;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getUserDirectory()
|
public static String getUserDirectory()
|
||||||
|
@ -335,11 +338,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)
|
public static boolean preferOldFolderPicker(Context context)
|
||||||
{
|
{
|
||||||
// As of January 2021, ACTION_OPEN_DOCUMENT_TREE seems to be broken on the Nvidia Shield TV
|
// As of January 2021, ACTION_OPEN_DOCUMENT_TREE seems to be broken on the Nvidia Shield TV
|
||||||
|
@ -347,16 +345,65 @@ public final class DirectoryInitialization
|
||||||
// for the time being - Android 11 hasn't been released for this device. We have an explicit
|
// 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.
|
// 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
|
// No Android TV device other than the Nvidia Shield TV is known to have an implementation of
|
||||||
// of ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE that even launches, but "fortunately"
|
// ACTION_OPEN_DOCUMENT or ACTION_OPEN_DOCUMENT_TREE that even launches, but "fortunately", no
|
||||||
// for us, the Nvidia Shield TV is the only Android TV device in existence so far that can
|
// Android TV device other than the Shield TV is known to be able to run Dolphin (either due to
|
||||||
// run Dolphin at all (due to the 64-bit requirement), so we can ignore this problem.
|
// 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
|
// 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() &&
|
return Build.VERSION.SDK_INT < Build.VERSION_CODES.R &&
|
||||||
TvUtil.isLeanback(context);
|
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 isUsingLegacyUserDirectory()
|
||||||
|
{
|
||||||
|
return isUsingLegacyUserDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
private static native void CreateUserDirectories();
|
||||||
|
|
|
@ -2,10 +2,10 @@
|
||||||
|
|
||||||
package org.dolphinemu.dolphinemu.utils;
|
package org.dolphinemu.dolphinemu.utils;
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.os.Environment;
|
||||||
|
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.fragment.app.FragmentActivity;
|
import androidx.fragment.app.FragmentActivity;
|
||||||
|
@ -15,38 +15,41 @@ import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
||||||
public class PermissionsHandler
|
public class PermissionsHandler
|
||||||
{
|
{
|
||||||
public static final int REQUEST_CODE_WRITE_PERMISSION = 500;
|
public static final int REQUEST_CODE_WRITE_PERMISSION = 500;
|
||||||
|
private static boolean sWritePermissionDenied = false;
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.M)
|
public static void requestWritePermission(final FragmentActivity activity)
|
||||||
public static boolean checkWritePermission(final FragmentActivity activity)
|
|
||||||
{
|
{
|
||||||
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
|
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
|
||||||
{
|
return;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int hasWritePermission = ContextCompat.checkSelfPermission(activity, WRITE_EXTERNAL_STORAGE);
|
activity.requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE},
|
||||||
|
REQUEST_CODE_WRITE_PERMISSION);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hasWriteAccess(Context context)
|
public static boolean hasWriteAccess(Context context)
|
||||||
{
|
{
|
||||||
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
|
||||||
{
|
return true;
|
||||||
int hasWritePermission = ContextCompat.checkSelfPermission(context, WRITE_EXTERNAL_STORAGE);
|
|
||||||
return hasWritePermission == PackageManager.PERMISSION_GRANTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,9 @@ public final class StartupHandler
|
||||||
|
|
||||||
public static void HandleInit(FragmentActivity parent)
|
public static void HandleInit(FragmentActivity parent)
|
||||||
{
|
{
|
||||||
// Ask the user to grant write permission if it's not already granted
|
// Ask the user to grant write permission if relevant and not already granted
|
||||||
PermissionsHandler.checkWritePermission(parent);
|
if (DirectoryInitialization.isWaitingForWriteAccess(parent))
|
||||||
|
PermissionsHandler.requestWritePermission(parent);
|
||||||
|
|
||||||
// Ask the user if he wants to enable analytics if we haven't yet.
|
// Ask the user if he wants to enable analytics if we haven't yet.
|
||||||
Analytics.checkAnalyticsInit(parent);
|
Analytics.checkAnalyticsInit(parent);
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_type"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/spacing_medlarge"
|
||||||
|
tools:text="@string/user_data_new_location"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/text_path"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
app:layout_constraintWidth_max="400dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_path"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/spacing_medlarge"
|
||||||
|
tools:text="/storage/emulated/0/Android/data/org.dolphinemu.dolphinemu/files"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text_type"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/text_android_11"
|
||||||
|
app:layout_constraintWidth_max="400dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_android_11"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/spacing_medlarge"
|
||||||
|
android:text="@string/user_data_new_location_android_11"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text_path"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/button_open_system_file_manager"
|
||||||
|
app:layout_constraintWidth_max="400dp" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_open_system_file_manager"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="@dimen/spacing_medlarge"
|
||||||
|
android:text="@string/user_data_open_system_file_manager"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/text_android_11"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -354,6 +354,14 @@
|
||||||
<string name="debug_jitbranchoff">Jit Branch Disabled</string>
|
<string name="debug_jitbranchoff">Jit Branch Disabled</string>
|
||||||
<string name="debug_jitregistercacheoff">Jit Register Cache Disabled</string>
|
<string name="debug_jitregistercacheoff">Jit Register Cache Disabled</string>
|
||||||
|
|
||||||
|
<!-- User Data -->
|
||||||
|
<string name="user_data_submenu">User Data</string>
|
||||||
|
<string name="user_data_old_location">Your user data is stored in a location which will <b>not</b> be deleted when you uninstall the app:</string>
|
||||||
|
<string name="user_data_new_location">Your user data is stored in a location which <b>will be deleted</b> when you uninstall the app:</string>
|
||||||
|
<string name="user_data_new_location_android_11">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 system file manager, or by connecting your device to a PC.</string>
|
||||||
|
<string name="user_data_open_system_file_manager">Open System File Manager</string>
|
||||||
|
<string name="user_data_open_system_file_manager_failed">Sorry, Dolphin couldn\'t find the system file manager on your device.</string>
|
||||||
|
|
||||||
<!-- Miscellaneous -->
|
<!-- Miscellaneous -->
|
||||||
<string name="yes">Yes</string>
|
<string name="yes">Yes</string>
|
||||||
<string name="no">No</string>
|
<string name="no">No</string>
|
||||||
|
@ -513,7 +521,7 @@ It can efficiently compress both junk data and encrypted Wii data.
|
||||||
<!-- Rumble -->
|
<!-- Rumble -->
|
||||||
<string name="rumble_not_found">Device rumble not found</string>
|
<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="path_not_changeable_scoped_storage">Due to the Scoped Storage policy in Android 11 and newer, you can\'t change this path.</string>
|
||||||
<string name="load_settings">Loading Settings...</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_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>
|
<string name="setting_clear_info">Long press a setting to clear it.</string>
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<full-backup-content>
|
||||||
|
<include domain="external" path="."/>
|
||||||
|
|
||||||
|
<exclude domain="external" path="./Cache/"/>
|
||||||
|
</full-backup-content>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<data-extraction-rules>
|
||||||
|
<cloud-backup disableIfNoEncryptionCapabilities="false">
|
||||||
|
<include domain="external" path="."/>
|
||||||
|
|
||||||
|
<exclude domain="external" path="./Cache/"/>
|
||||||
|
<exclude domain="external" path="./Dump/"/>
|
||||||
|
<exclude domain="external" path="./Load/"/>
|
||||||
|
<exclude domain="external" path="./ResourcePacks/"/>
|
||||||
|
</cloud-backup>
|
||||||
|
|
||||||
|
<device-transfer>
|
||||||
|
<include domain="external" path="."/>
|
||||||
|
|
||||||
|
<exclude domain="external" path="./Cache/"/>
|
||||||
|
</device-transfer>
|
||||||
|
</data-extraction-rules>
|
Loading…
Reference in New Issue