Android: Force quit app if external storage isn't mounted

In the past, directory initialization could fail for two reasons:
The user was rejecting the storage permission, or external storage
wasn't mounted. With the introduction of scoped storage, the first of
these two couldn't happen anymore; if the user rejects the storage
permission, we just use the app-specific directory instead of the
dolphin-emu directory.

By making it so Dolphin force quits if external storage isn't mounted,
we can get rid of our code for handling retrying directory initialization
after it fails. I think this slight hit to UX is worth it considering
that basically nobody has an Android device with detachable primary
external storage anymore. And the UX hit is very small; the user just has
to manually open the app again after remounting external storage. The
toast about external storage not being mounted will still be displayed.

The recent merge of the splash screen PR may have made it so that the
code for handling directory initialization failing doesn't work anymore.
To be completely honest, I'm not sure how to even test this in 2022.
This commit is contained in:
JosJuice 2022-08-12 21:29:54 +02:00
parent d29b349a0c
commit 1646197902
11 changed files with 53 additions and 130 deletions

View File

@ -63,7 +63,7 @@ public class AppLinkActivity extends FragmentActivity
private void initResources() private void initResources()
{ {
mAfterDirectoryInitializationRunner = new AfterDirectoryInitializationRunner(); mAfterDirectoryInitializationRunner = new AfterDirectoryInitializationRunner();
mAfterDirectoryInitializationRunner.runWithLifecycle(this, true, () -> tryPlay(playAction)); mAfterDirectoryInitializationRunner.runWithLifecycle(this, () -> tryPlay(playAction));
GameFileCacheManager.isLoading().observe(this, (isLoading) -> GameFileCacheManager.isLoading().observe(this, (isLoading) ->
{ {

View File

@ -185,7 +185,7 @@ public final class EmulationActivity extends AppCompatActivity
private static void performLaunchChecks(FragmentActivity activity, private static void performLaunchChecks(FragmentActivity activity,
Runnable continueCallback) Runnable continueCallback)
{ {
new AfterDirectoryInitializationRunner().runWithLifecycle(activity, true, () -> new AfterDirectoryInitializationRunner().runWithLifecycle(activity, () ->
{ {
if (!FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_DEFAULT_ISO) || if (!FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_DEFAULT_ISO) ||
!FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_FS_PATH) || !FileBrowserHelper.isPathEmptyOrValid(StringSetting.MAIN_FS_PATH) ||

View File

@ -63,6 +63,8 @@ public final class SettingsActivityPresenter
private void loadSettingsUI() private void loadSettingsUI()
{ {
mView.hideLoading();
if (mSettings.isEmpty()) if (mSettings.isEmpty())
{ {
if (!TextUtils.isEmpty(mGameId)) if (!TextUtils.isEmpty(mGameId))
@ -86,18 +88,9 @@ public final class SettingsActivityPresenter
private void prepareDolphinDirectoriesIfNeeded() private void prepareDolphinDirectoriesIfNeeded()
{ {
if (DirectoryInitialization.areDolphinDirectoriesReady()) mView.showLoading();
{
loadSettingsUI();
}
else
{
mView.showLoading();
new AfterDirectoryInitializationRunner() new AfterDirectoryInitializationRunner().runWithLifecycle(mActivity, this::loadSettingsUI);
.setFinishedCallback(mView::hideLoading)
.runWithLifecycle(mActivity, true, this::loadSettingsUI);
}
} }
public Settings getSettings() public Settings getSettings()

View File

@ -100,7 +100,7 @@ public interface SettingsActivityView
void showLoading(); void showLoading();
/** /**
* Hide the loading the dialog * Hide the loading dialog
*/ */
void hideLoading(); void hideLoading();

View File

@ -128,7 +128,7 @@ public final class GameFileCacheManager
if (!loadInProgress.getValue()) if (!loadInProgress.getValue())
{ {
loadInProgress.setValue(true); loadInProgress.setValue(true);
new AfterDirectoryInitializationRunner().runWithoutLifecycle(context, false, new AfterDirectoryInitializationRunner().runWithoutLifecycle(
() -> executor.execute(GameFileCacheManager::load)); () -> executor.execute(GameFileCacheManager::load));
} }
} }
@ -144,7 +144,7 @@ public final class GameFileCacheManager
if (!rescanInProgress.getValue()) if (!rescanInProgress.getValue())
{ {
rescanInProgress.setValue(true); rescanInProgress.setValue(true);
new AfterDirectoryInitializationRunner().runWithoutLifecycle(context, false, new AfterDirectoryInitializationRunner().runWithoutLifecycle(
() -> executor.execute(GameFileCacheManager::rescan)); () -> executor.execute(GameFileCacheManager::rescan));
} }
} }

View File

@ -79,7 +79,7 @@ public final class MainActivity extends AppCompatActivity
if (!DirectoryInitialization.isWaitingForWriteAccess(this)) if (!DirectoryInitialization.isWaitingForWriteAccess(this))
{ {
new AfterDirectoryInitializationRunner() new AfterDirectoryInitializationRunner()
.runWithLifecycle(this, false, this::setPlatformTabsAndStartGameFileCacheService); .runWithLifecycle(this, this::setPlatformTabsAndStartGameFileCacheService);
} }
} }
@ -92,7 +92,7 @@ public final class MainActivity extends AppCompatActivity
{ {
DirectoryInitialization.start(this); DirectoryInitialization.start(this);
new AfterDirectoryInitializationRunner() new AfterDirectoryInitializationRunner()
.runWithLifecycle(this, false, this::setPlatformTabsAndStartGameFileCacheService); .runWithLifecycle(this, this::setPlatformTabsAndStartGameFileCacheService);
} }
mPresenter.onResume(); mPresenter.onResume();
@ -268,7 +268,7 @@ public final class MainActivity extends AppCompatActivity
DirectoryInitialization.start(this); DirectoryInitialization.start(this);
new AfterDirectoryInitializationRunner() new AfterDirectoryInitializationRunner()
.runWithLifecycle(this, false, this::setPlatformTabsAndStartGameFileCacheService); .runWithLifecycle(this, this::setPlatformTabsAndStartGameFileCacheService);
} }
} }

View File

@ -81,7 +81,7 @@ public final class MainPresenter
public void onFabClick() public void onFabClick()
{ {
new AfterDirectoryInitializationRunner().runWithLifecycle(mActivity, true, new AfterDirectoryInitializationRunner().runWithLifecycle(mActivity,
mView::launchFileListActivity); mView::launchFileListActivity);
} }
@ -99,7 +99,7 @@ public final class MainPresenter
return true; return true;
case R.id.button_add_directory: case R.id.button_add_directory:
new AfterDirectoryInitializationRunner().runWithLifecycle(activity, true, new AfterDirectoryInitializationRunner().runWithLifecycle(activity,
mView::launchFileListActivity); mView::launchFileListActivity);
return true; return true;
@ -112,22 +112,22 @@ public final class MainPresenter
return true; return true;
case R.id.menu_online_system_update: case R.id.menu_online_system_update:
new AfterDirectoryInitializationRunner().runWithLifecycle(activity, true, new AfterDirectoryInitializationRunner().runWithLifecycle(activity,
this::launchOnlineUpdate); this::launchOnlineUpdate);
return true; return true;
case R.id.menu_install_wad: case R.id.menu_install_wad:
new AfterDirectoryInitializationRunner().runWithLifecycle(activity, true, new AfterDirectoryInitializationRunner().runWithLifecycle(activity,
() -> mView.launchOpenFileActivity(REQUEST_WAD_FILE)); () -> mView.launchOpenFileActivity(REQUEST_WAD_FILE));
return true; return true;
case R.id.menu_import_wii_save: case R.id.menu_import_wii_save:
new AfterDirectoryInitializationRunner().runWithLifecycle(activity, true, new AfterDirectoryInitializationRunner().runWithLifecycle(activity,
() -> mView.launchOpenFileActivity(REQUEST_WII_SAVE_FILE)); () -> mView.launchOpenFileActivity(REQUEST_WII_SAVE_FILE));
return true; return true;
case R.id.menu_import_nand_backup: case R.id.menu_import_nand_backup:
new AfterDirectoryInitializationRunner().runWithLifecycle(activity, true, new AfterDirectoryInitializationRunner().runWithLifecycle(activity,
() -> mView.launchOpenFileActivity(REQUEST_NAND_BIN_FILE)); () -> mView.launchOpenFileActivity(REQUEST_NAND_BIN_FILE));
return true; return true;
} }
@ -325,7 +325,7 @@ public final class MainPresenter
} }
else else
{ {
new AfterDirectoryInitializationRunner().runWithLifecycle(mActivity, true, () -> new AfterDirectoryInitializationRunner().runWithLifecycle(mActivity, () ->
{ {
SystemMenuNotInstalledDialogFragment dialogFragment = SystemMenuNotInstalledDialogFragment dialogFragment =
new SystemMenuNotInstalledDialogFragment(); new SystemMenuNotInstalledDialogFragment();

View File

@ -2,43 +2,14 @@
package org.dolphinemu.dolphinemu.utils; package org.dolphinemu.dolphinemu.utils;
import android.content.Context;
import android.widget.Toast;
import androidx.core.app.ComponentActivity; import androidx.core.app.ComponentActivity;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization.DirectoryInitializationState; import org.dolphinemu.dolphinemu.utils.DirectoryInitialization.DirectoryInitializationState;
public class AfterDirectoryInitializationRunner public class AfterDirectoryInitializationRunner
{ {
private Observer<DirectoryInitializationState> mObserver; private Observer<DirectoryInitializationState> mObserver;
private Runnable mUnregisterCallback;
/**
* Sets a Runnable which will be called when:
*
* 1. The Runnable supplied to {@link #runWithLifecycle}/{@link #runWithoutLifecycle}
* is just about to run, or
* 2. {@link #runWithLifecycle}/{@link #runWithoutLifecycle} was called with
* abortOnFailure == true and there is a failure
*
* @return this
*/
public AfterDirectoryInitializationRunner setFinishedCallback(Runnable runnable)
{
mUnregisterCallback = runnable;
return this;
}
private void runFinishedCallback()
{
if (mUnregisterCallback != null)
{
mUnregisterCallback.run();
}
}
/** /**
* Executes a Runnable after directory initialization has finished. * Executes a Runnable after directory initialization has finished.
@ -59,23 +30,15 @@ public class AfterDirectoryInitializationRunner
* If the passed-in activity gets destroyed before this operation finishes, * If the passed-in activity gets destroyed before this operation finishes,
* it will be automatically canceled. * it will be automatically canceled.
*/ */
public void runWithLifecycle(ComponentActivity activity, boolean abortOnFailure, public void runWithLifecycle(ComponentActivity activity, Runnable runnable)
Runnable runnable)
{ {
if (DirectoryInitialization.areDolphinDirectoriesReady()) if (DirectoryInitialization.areDolphinDirectoriesReady())
{ {
runFinishedCallback();
runnable.run(); runnable.run();
} }
else if (abortOnFailure &&
showErrorMessage(activity,
DirectoryInitialization.getDolphinDirectoriesState().getValue()))
{
runFinishedCallback();
}
else else
{ {
mObserver = createObserver(activity, abortOnFailure, runnable); mObserver = createObserver(runnable);
DirectoryInitialization.getDolphinDirectoriesState().observe(activity, mObserver); DirectoryInitialization.getDolphinDirectoriesState().observe(activity, mObserver);
} }
} }
@ -96,46 +59,26 @@ public class AfterDirectoryInitializationRunner
* the attempt to run the Runnable will never be aborted, and the Runnable * the attempt to run the Runnable will never be aborted, and the Runnable
* is guaranteed to run if directory initialization ever finishes. * is guaranteed to run if directory initialization ever finishes.
*/ */
public void runWithoutLifecycle(Context context, boolean abortOnFailure, Runnable runnable) public void runWithoutLifecycle(Runnable runnable)
{ {
if (DirectoryInitialization.areDolphinDirectoriesReady()) if (DirectoryInitialization.areDolphinDirectoriesReady())
{ {
runFinishedCallback();
runnable.run(); runnable.run();
} }
else if (abortOnFailure &&
showErrorMessage(context,
DirectoryInitialization.getDolphinDirectoriesState().getValue()))
{
runFinishedCallback();
}
else else
{ {
mObserver = createObserver(context, abortOnFailure, runnable); mObserver = createObserver(runnable);
DirectoryInitialization.getDolphinDirectoriesState().observeForever(mObserver); DirectoryInitialization.getDolphinDirectoriesState().observeForever(mObserver);
} }
} }
private Observer<DirectoryInitializationState> createObserver(Context context, private Observer<DirectoryInitializationState> createObserver(Runnable runnable)
boolean abortOnFailure, Runnable runnable)
{ {
return (state) -> return (state) ->
{ {
boolean done = state == DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED;
if (!done && abortOnFailure)
{
done = showErrorMessage(context, state);
}
if (done)
{
cancel();
runFinishedCallback();
}
if (state == DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED) if (state == DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED)
{ {
cancel();
runnable.run(); runnable.run();
} }
}; };
@ -145,17 +88,4 @@ public class AfterDirectoryInitializationRunner
{ {
DirectoryInitialization.getDolphinDirectoriesState().removeObserver(mObserver); DirectoryInitialization.getDolphinDirectoriesState().removeObserver(mObserver);
} }
private static boolean showErrorMessage(Context context, DirectoryInitializationState state)
{
switch (state)
{
case CANT_FIND_EXTERNAL_STORAGE:
Toast.makeText(context, R.string.external_storage_not_mounted, Toast.LENGTH_LONG).show();
return true;
default:
return false;
}
}
} }

View File

@ -25,7 +25,7 @@ public class Analytics
public static void checkAnalyticsInit(Context context) public static void checkAnalyticsInit(Context context)
{ {
new AfterDirectoryInitializationRunner().runWithoutLifecycle(context, false, () -> new AfterDirectoryInitializationRunner().runWithoutLifecycle(() ->
{ {
if (!BooleanSetting.MAIN_ANALYTICS_PERMISSION_ASKED.getBooleanGlobal()) if (!BooleanSetting.MAIN_ANALYTICS_PERMISSION_ASKED.getBooleanGlobal())
{ {

View File

@ -10,6 +10,7 @@ import android.content.SharedPreferences;
import android.os.Build; import android.os.Build;
import android.os.Environment; import android.os.Environment;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -17,6 +18,7 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import org.dolphinemu.dolphinemu.NativeLibrary; import org.dolphinemu.dolphinemu.NativeLibrary;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.activities.EmulationActivity; import org.dolphinemu.dolphinemu.activities.EmulationActivity;
import java.io.File; import java.io.File;
@ -43,13 +45,12 @@ public final class DirectoryInitialization
{ {
NOT_YET_INITIALIZED, NOT_YET_INITIALIZED,
INITIALIZING, INITIALIZING,
DOLPHIN_DIRECTORIES_INITIALIZED, DOLPHIN_DIRECTORIES_INITIALIZED
CANT_FIND_EXTERNAL_STORAGE
} }
public static void start(Context context) public static void start(Context context)
{ {
if (directoryState.getValue() == DirectoryInitializationState.INITIALIZING) if (directoryState.getValue() != DirectoryInitializationState.NOT_YET_INITIALIZED)
return; return;
directoryState.setValue(DirectoryInitializationState.INITIALIZING); directoryState.setValue(DirectoryInitializationState.INITIALIZING);
@ -60,31 +61,30 @@ public final class DirectoryInitialization
private static void init(Context context) private static void init(Context context)
{ {
if (directoryState.getValue() != DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED) if (directoryState.getValue() == DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED)
return;
if (!setDolphinUserDirectory(context))
{ {
if (setDolphinUserDirectory(context)) Toast.makeText(context, R.string.external_storage_not_mounted, Toast.LENGTH_LONG).show();
{ System.exit(1);
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.postValue(DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED);
}
else
{
directoryState.postValue(DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE);
}
} }
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.postValue(DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED);
} }
@Nullable @Nullable

View File

@ -112,7 +112,7 @@ public final class StartupHandler
final Instant lastOpened = Instant.ofEpochMilli(lastOpen); final Instant lastOpened = Instant.ofEpochMilli(lastOpen);
if (current.isAfter(lastOpened.plus(6, ChronoUnit.HOURS))) if (current.isAfter(lastOpened.plus(6, ChronoUnit.HOURS)))
{ {
new AfterDirectoryInitializationRunner().runWithoutLifecycle(context, false, new AfterDirectoryInitializationRunner().runWithoutLifecycle(
NativeLibrary::ReportStartToAnalytics); NativeLibrary::ReportStartToAnalytics);
} }
} }