Expand Up @@ -3,30 +3,33 @@
package org.dolphinemu.dolphinemu.utils;

import android.content.Context;
import android.content.IntentFilter;
import android.widget.Toast;

import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.core.app.ComponentActivity;
import androidx.lifecycle.Observer;

import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization.DirectoryInitializationState;

public class AfterDirectoryInitializationRunner
{
private DirectoryStateReceiver mDirectoryStateReceiver;
private LocalBroadcastManager mLocalBroadcastManager;

private Observer<DirectoryInitializationState> mObserver;
private Runnable mUnregisterCallback;

/**
* Sets a Runnable which will be called when:
*
* 1. The Runnable supplied to {@link #run} is just about to run, or
* 2. {@link #run} was called with abortOnFailure == true and there is a failure
* 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 void setFinishedCallback(Runnable runnable)
public AfterDirectoryInitializationRunner setFinishedCallback(Runnable runnable)
{
mUnregisterCallback = runnable;
return this;
}

private void runFinishedCallback()
Expand All @@ -48,33 +51,75 @@ private void runFinishedCallback()
*
* Calling this function multiple times per object is not supported.
*
* If abortOnFailure is true and the user has not granted the required
* permission or the external storage was not found, a message will be
* shown to the user and the Runnable will not run. If it is false, the
* attempt to run the Runnable will never be aborted, and the Runnable
* If abortOnFailure is true and external storage was not found, a message
* will be shown to the user and the Runnable will not run. If it is false,
* the attempt to run the Runnable will never be aborted, and the Runnable
* is guaranteed to run if directory initialization ever finishes.
*
* If the passed-in activity gets destroyed before this operation finishes,
* it will be automatically canceled.
*/
public void run(Context context, boolean abortOnFailure, Runnable runnable)
public void runWithLifecycle(ComponentActivity activity, boolean abortOnFailure,
Runnable runnable)
{
if (DirectoryInitialization.areDolphinDirectoriesReady())
{
runFinishedCallback();
runnable.run();
}
else if (abortOnFailure &&
showErrorMessage(context, DirectoryInitialization.getDolphinDirectoriesState()))
showErrorMessage(activity,
DirectoryInitialization.getDolphinDirectoriesState().getValue()))
{
runFinishedCallback();
}
else
{
runAfterInitialization(context, abortOnFailure, runnable);
mObserver = createObserver(activity, abortOnFailure, runnable);
DirectoryInitialization.getDolphinDirectoriesState().observe(activity, mObserver);
}
}

private void runAfterInitialization(Context context, boolean abortOnFailure, Runnable runnable)
/**
* Executes a Runnable after directory initialization has finished.
*
* If this is called when directory initialization already is done,
* the Runnable will be executed immediately. If this is called before
* directory initialization is done, the Runnable will be executed
* after directory initialization finishes successfully, or never
* in case directory initialization doesn't finish successfully.
*
* Calling this function multiple times per object is not supported.
*
* If abortOnFailure is true and external storage was not found, a message
* will be shown to the user and the Runnable will not run. If it is false,
* the attempt to run the Runnable will never be aborted, and the Runnable
* is guaranteed to run if directory initialization ever finishes.
*/
public void runWithoutLifecycle(Context context, boolean abortOnFailure, Runnable runnable)
{
mDirectoryStateReceiver = new DirectoryStateReceiver(state ->
if (DirectoryInitialization.areDolphinDirectoriesReady())
{
runFinishedCallback();
runnable.run();
}
else if (abortOnFailure &&
showErrorMessage(context,
DirectoryInitialization.getDolphinDirectoriesState().getValue()))
{
runFinishedCallback();
}
else
{
mObserver = createObserver(context, abortOnFailure, runnable);
DirectoryInitialization.getDolphinDirectoriesState().observeForever(mObserver);
}
}

private Observer<DirectoryInitializationState> createObserver(Context context,
boolean abortOnFailure, Runnable runnable)
{
return (state) ->
{
boolean done = state == DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED;

Expand All @@ -93,22 +138,12 @@ private void runAfterInitialization(Context context, boolean abortOnFailure, Run
{
runnable.run();
}
});

mLocalBroadcastManager = LocalBroadcastManager.getInstance(context);

IntentFilter statusIntentFilter = new IntentFilter(DirectoryInitialization.BROADCAST_ACTION);
mLocalBroadcastManager.registerReceiver(mDirectoryStateReceiver, statusIntentFilter);
};
}

public void cancel()
{
if (mDirectoryStateReceiver != null)
{
mLocalBroadcastManager.unregisterReceiver(mDirectoryStateReceiver);
mDirectoryStateReceiver = null;
mLocalBroadcastManager = null;
}
DirectoryInitialization.getDolphinDirectoriesState().removeObserver(mObserver);
}

private static boolean showErrorMessage(Context context, DirectoryInitializationState state)
Expand Down
Expand Up @@ -25,7 +25,7 @@

public static void checkAnalyticsInit(Context context)
{
new AfterDirectoryInitializationRunner().run(context, false, () ->
new AfterDirectoryInitializationRunner().runWithoutLifecycle(context, false, () ->
{
if (!BooleanSetting.MAIN_ANALYTICS_PERMISSION_ASKED.getBooleanGlobal())
{
Expand Down
Expand Up @@ -6,65 +6,62 @@
package org.dolphinemu.dolphinemu.utils;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Environment;
import android.preference.PreferenceManager;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;

import org.dolphinemu.dolphinemu.NativeLibrary;
import org.dolphinemu.dolphinemu.activities.EmulationActivity;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicBoolean;

/**
* A service that spawns its own thread in order to copy several binary and shader files
* from the Dolphin APK to the external file system.
*/
public final class DirectoryInitialization
{
public static final String BROADCAST_ACTION =
"org.dolphinemu.dolphinemu.DIRECTORY_INITIALIZATION";

public static final String EXTRA_STATE = "directoryState";
private static final int WiimoteNewVersion = 5; // Last changed in PR 8907
private static volatile DirectoryInitializationState directoryState =
DirectoryInitializationState.NOT_YET_INITIALIZED;
private static final MutableLiveData<DirectoryInitializationState> directoryState =
new MutableLiveData<>(DirectoryInitializationState.NOT_YET_INITIALIZED);
private static volatile boolean areDirectoriesAvailable = false;
private static String userPath;
private static AtomicBoolean isDolphinDirectoryInitializationRunning = new AtomicBoolean(false);
private static boolean isUsingLegacyUserDirectory = false;

public enum DirectoryInitializationState
{
NOT_YET_INITIALIZED,
INITIALIZING,
DOLPHIN_DIRECTORIES_INITIALIZED,
CANT_FIND_EXTERNAL_STORAGE
}

public static void start(Context context)
{
if (!isDolphinDirectoryInitializationRunning.compareAndSet(false, true))
if (directoryState.getValue() == DirectoryInitializationState.INITIALIZING)
return;

directoryState.setValue(DirectoryInitializationState.INITIALIZING);

// Can take a few seconds to run, so don't block UI thread.
//noinspection TrivialFunctionalExpressionUsage
((Runnable) () -> init(context)).run();
}

private static void init(Context context)
{
if (directoryState != DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED)
if (directoryState.getValue() != DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED)
{
if (setDolphinUserDirectory(context))
{
Expand All @@ -82,16 +79,13 @@ private static void init(Context context)
EmulationActivity.updateWiimoteNewIniPreferences(context);
}

directoryState = DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED;
directoryState.postValue(DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED);
}
else
{
directoryState = DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE;
directoryState.postValue(DirectoryInitializationState.CANT_FIND_EXTERNAL_STORAGE);
}
}

isDolphinDirectoryInitializationRunning.set(false);
sendBroadcastState(directoryState, context);
}

@Nullable
Expand Down Expand Up @@ -216,17 +210,18 @@ private static void deleteDirectoryRecursively(@NonNull final File file)

public static boolean shouldStart(Context context)
{
return !isDolphinDirectoryInitializationRunning.get() &&
getDolphinDirectoriesState() == DirectoryInitializationState.NOT_YET_INITIALIZED &&
return getDolphinDirectoriesState().getValue() ==
DirectoryInitializationState.NOT_YET_INITIALIZED &&
!isWaitingForWriteAccess(context);
}

public static boolean areDolphinDirectoriesReady()
{
return directoryState == DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED;
return directoryState.getValue() ==
DirectoryInitializationState.DOLPHIN_DIRECTORIES_INITIALIZED;
}

public static DirectoryInitializationState getDolphinDirectoriesState()
public static LiveData<DirectoryInitializationState> getDolphinDirectoriesState()
{
return directoryState;
}
Expand All @@ -241,14 +236,6 @@ public static String getUserDirectory()
return userPath;
}

private static void sendBroadcastState(DirectoryInitializationState state, Context context)
{
Intent localIntent =
new Intent(BROADCAST_ACTION)
.putExtra(EXTRA_STATE, state);
LocalBroadcastManager.getInstance(context).sendBroadcast(localIntent);
}

private static boolean copyAsset(String asset, File output, Boolean overwrite, Context context)
{
Log.verbose("[DirectoryInitialization] Copying File " + asset + " to " + output);
Expand Down Expand Up @@ -400,7 +387,7 @@ public static boolean isUsingLegacyUserDirectory()
public static boolean isWaitingForWriteAccess(Context context)
{
// This first check is only for performance, not correctness
if (getDolphinDirectoriesState() != DirectoryInitializationState.NOT_YET_INITIALIZED)
if (directoryState.getValue() != DirectoryInitializationState.NOT_YET_INITIALIZED)
return false;

return preferLegacyUserDirectory(context) && !PermissionsHandler.hasWriteAccess(context);
Expand Down

This file was deleted.

Expand Up @@ -79,7 +79,7 @@ public static void checkSessionReset(Context context)
final Instant lastOpened = Instant.ofEpochMilli(lastOpen);
if (current.isAfter(lastOpened.plus(6, ChronoUnit.HOURS)))
{
new AfterDirectoryInitializationRunner().run(context, false,
new AfterDirectoryInitializationRunner().runWithoutLifecycle(context, false,
NativeLibrary::ReportStartToAnalytics);
}
}
Expand Down