From d5f74af94a7c1553d9366874e7bc538ae4c9941e Mon Sep 17 00:00:00 2001 From: Maksim Belov <45949002+artdeell@users.noreply.github.com> Date: Tue, 7 Nov 2023 21:21:19 +0300 Subject: [PATCH] Feat: Handle MainActivity destruction (#4817) * Feat[launcher]: begin implementing MainActivity destruction handling * Feat[lifecycle]: finalize MainActivity lifecycle awareness implementation * Clean[dialog]: unified halting LifecycleAwareAlertDialog implementation * Fix[mainactivity]: comment truncated --- .../pojavlaunch/JavaGUILauncherActivity.java | 23 ++-- .../net/kdt/pojavlaunch/LauncherActivity.java | 4 +- .../net/kdt/pojavlaunch/MainActivity.java | 39 +++++- .../kdt/pojavlaunch/MinecraftGLSurface.java | 11 +- .../net/kdt/pojavlaunch/PojavApplication.java | 2 +- .../kdt/pojavlaunch/ShowErrorActivity.java | 2 +- .../main/java/net/kdt/pojavlaunch/Tools.java | 31 ++--- .../ContextAwareDoneListener.java | 7 +- .../ContextExecutor.java | 2 +- .../ContextExecutorTask.java | 2 +- .../lifecycle/LifecycleAwareAlertDialog.java | 125 ++++++++++++++++++ .../mirrors/MirrorTamperedException.java | 2 +- .../kdt/pojavlaunch/services/GameService.java | 16 +-- .../net/kdt/pojavlaunch/utils/JREUtils.java | 20 ++- 14 files changed, 218 insertions(+), 68 deletions(-) rename app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/{tasks => lifecycle}/ContextAwareDoneListener.java (92%) rename app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/{contextexecutor => lifecycle}/ContextExecutor.java (98%) rename app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/{contextexecutor => lifecycle}/ContextExecutorTask.java (95%) create mode 100644 app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/lifecycle/LifecycleAwareAlertDialog.java diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JavaGUILauncherActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JavaGUILauncherActivity.java index bf3c3f781..3a87663fe 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JavaGUILauncherActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/JavaGUILauncherActivity.java @@ -1,7 +1,5 @@ package net.kdt.pojavlaunch; -import static net.kdt.pojavlaunch.MainActivity.fullyExit; - import android.annotation.SuppressLint; import android.content.ClipboardManager; import android.os.Bundle; @@ -173,14 +171,10 @@ public class JavaGUILauncherActivity extends BaseActivity implements View.OnTouc openLogOutput(null); new Thread(() -> { try { - final int exit = doCustomInstall(runtime, modFile, javaArgs); - Logger.appendToLog(getString(R.string.toast_optifine_success)); - if (exit != 0) return; - runOnUiThread(() -> { - Toast.makeText(JavaGUILauncherActivity.this, R.string.toast_optifine_success, Toast.LENGTH_SHORT).show(); - fullyExit(); - }); - + // Due to time, the code here became, like, actually useless + // So it was removed + // Tbh this whole class needs a refactor... + doCustomInstall(runtime, modFile, javaArgs); } catch (Throwable e) { Logger.appendToLog("Install failed:"); Logger.appendToLog(Log.getStackTraceString(e)); @@ -287,7 +281,7 @@ public class JavaGUILauncherActivity extends BaseActivity implements View.OnTouc Toast.LENGTH_SHORT).show(); } - public int launchJavaRuntime(Runtime runtime, File modFile, String javaArgs) { + public void launchJavaRuntime(Runtime runtime, File modFile, String javaArgs) { JREUtils.redirectAndPrintJRELog(); try { List javaArgList = new ArrayList<>(); @@ -313,18 +307,17 @@ public class JavaGUILauncherActivity extends BaseActivity implements View.OnTouc Logger.appendToLog("Info: Java arguments: " + Arrays.toString(javaArgList.toArray(new String[0]))); - return JREUtils.launchJavaVM(this, runtime,null,javaArgList, LauncherPreferences.PREF_CUSTOM_JAVA_ARGS); + JREUtils.launchJavaVM(this, runtime,null,javaArgList, LauncherPreferences.PREF_CUSTOM_JAVA_ARGS); } catch (Throwable th) { Tools.showError(this, th, true); - return -1; } } - private int doCustomInstall(Runtime runtime, File modFile, String javaArgs) { + private void doCustomInstall(Runtime runtime, File modFile, String javaArgs) { mSkipDetectMod = true; - return launchJavaRuntime(runtime, modFile, javaArgs); + launchJavaRuntime(runtime, modFile, javaArgs); } public void toggleKeyboard(View view) { diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java index ee1e31ef0..8daf0f419 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/LauncherActivity.java @@ -23,7 +23,7 @@ import androidx.fragment.app.FragmentManager; import com.kdt.mcgui.ProgressLayout; import com.kdt.mcgui.mcAccountSpinner; -import net.kdt.pojavlaunch.contextexecutor.ContextExecutor; +import net.kdt.pojavlaunch.lifecycle.ContextExecutor; import net.kdt.pojavlaunch.contracts.OpenDocumentWithExtension; import net.kdt.pojavlaunch.extra.ExtraConstants; import net.kdt.pojavlaunch.extra.ExtraCore; @@ -40,7 +40,7 @@ import net.kdt.pojavlaunch.progresskeeper.TaskCountListener; import net.kdt.pojavlaunch.services.ProgressServiceKeeper; import net.kdt.pojavlaunch.tasks.AsyncMinecraftDownloader; import net.kdt.pojavlaunch.tasks.AsyncVersionList; -import net.kdt.pojavlaunch.tasks.ContextAwareDoneListener; +import net.kdt.pojavlaunch.lifecycle.ContextAwareDoneListener; import net.kdt.pojavlaunch.utils.NotificationUtils; import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles; import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java index 9bc6021fb..5ff479f4e 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MainActivity.java @@ -13,8 +13,10 @@ import android.app.Activity; import android.app.AlertDialog; import android.content.ClipData; import android.content.ClipboardManager; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Color; @@ -22,6 +24,7 @@ import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.IBinder; import android.provider.DocumentsContract; import android.util.Log; import android.view.KeyEvent; @@ -36,10 +39,12 @@ import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; import androidx.drawerlayout.widget.DrawerLayout; import com.kdt.LoggerView; +import net.kdt.pojavlaunch.lifecycle.ContextExecutor; import net.kdt.pojavlaunch.customcontrols.ControlButtonMenuListener; import net.kdt.pojavlaunch.customcontrols.ControlData; import net.kdt.pojavlaunch.customcontrols.ControlDrawerData; @@ -62,7 +67,7 @@ import org.lwjgl.glfw.CallbackBridge; import java.io.File; import java.io.IOException; -public class MainActivity extends BaseActivity implements ControlButtonMenuListener, EditorExitable { +public class MainActivity extends BaseActivity implements ControlButtonMenuListener, EditorExitable, ServiceConnection { public static volatile ClipboardManager GLOBAL_CLIPBOARD; public static final String INTENT_MINECRAFT_VERSION = "intent_version"; @@ -84,13 +89,17 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe private AdapterView.OnItemClickListener gameActionClickListener; public ArrayAdapter ingameControlsEditorArrayAdapter; public AdapterView.OnItemClickListener ingameControlsEditorListener; + private GameService.LocalBinder mServiceBinder; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); minecraftProfile = LauncherProfiles.getCurrentProfile(); MCOptionUtils.load(Tools.getGameDirPath(minecraftProfile).getAbsolutePath()); - GameService.startService(this); + + Intent gameServiceIntent = new Intent(this, GameService.class); + // Start the service a bit early + ContextCompat.startForegroundService(this, gameServiceIntent); initLayout(R.layout.activity_basemain); CallbackBridge.addGrabListener(touchpad); CallbackBridge.addGrabListener(minecraftGLView); @@ -122,6 +131,12 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe MCOptionUtils.MCOptionListener optionListener = MCOptionUtils::getMcScale; MCOptionUtils.addMCOptionListener(optionListener); mControlLayout.setModifiable(false); + + // Set the activity for the executor. Must do this here, or else Tools.showErrorRemote() may not + // execute the correct method + ContextExecutor.setActivity(this); + //Now, attach to the service. The game will only start when this happens, to make sure that we know the right state. + bindService(gameServiceIntent, this, 0); } protected void initLayout(int resId) { @@ -191,11 +206,9 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe runCraft(finalVersion, mVersionInfo); }catch (Throwable e){ - Tools.showError(getApplicationContext(), e, true); + Tools.showErrorRemote(e); } }); - - minecraftGLView.start(); } catch (Throwable e) { Tools.showError(this, e, true); } @@ -275,6 +288,7 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe super.onDestroy(); CallbackBridge.removeGrabListener(touchpad); CallbackBridge.removeGrabListener(minecraftGLView); + ContextExecutor.clearActivity(); } @Override @@ -336,6 +350,8 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe int requiredJavaVersion = 8; if(version.javaVersion != null) requiredJavaVersion = version.javaVersion.majorVersion; Tools.launchMinecraft(this, minecraftAccount, minecraftProfile, versionId, requiredJavaVersion); + //Note that we actually stall in the above function, even if the game crashes. But let's be safe. + Tools.runOnUiThread(()-> mServiceBinder.isActive = false); } private void printLauncherInfo(String gameVersion, String javaArguments) { @@ -609,4 +625,17 @@ public class MainActivity extends BaseActivity implements ControlButtonMenuListe navDrawer.setOnItemClickListener(gameActionClickListener); isInEditor = false; } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + GameService.LocalBinder localBinder = (GameService.LocalBinder) service; + mServiceBinder = localBinder; + minecraftGLView.start(localBinder.isActive); + localBinder.isActive = true; + } + + @Override + public void onServiceDisconnected(ComponentName name) { + + } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MinecraftGLSurface.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MinecraftGLSurface.java index 8a556b16a..177265ca5 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MinecraftGLSurface.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/MinecraftGLSurface.java @@ -144,14 +144,17 @@ public class MinecraftGLSurface extends View implements GrabListener { MCOptionUtils.addMCOptionListener(mGuiScaleListener); } - /** Initialize the view and all its settings */ - public void start(){ + /** Initialize the view and all its settings + * @param isAlreadyRunning set to true to tell the view that the game is already running + * (only updates the window without calling the start listener) + */ + public void start(boolean isAlreadyRunning){ if(LauncherPreferences.PREF_USE_ALTERNATE_SURFACE){ SurfaceView surfaceView = new SurfaceView(getContext()); mSurface = surfaceView; surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { - private boolean isCalled = false; + private boolean isCalled = isAlreadyRunning; @Override public void surfaceCreated(@NonNull SurfaceHolder holder) { if(isCalled) { @@ -180,7 +183,7 @@ public class MinecraftGLSurface extends View implements GrabListener { mSurface = textureView; textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { - private boolean isCalled = false; + private boolean isCalled = isAlreadyRunning; @Override public void onSurfaceTextureAvailable(@NonNull SurfaceTexture surface, int width, int height) { Surface tSurface = new Surface(surface); diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavApplication.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavApplication.java index 09a22a2ba..71822ba1a 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavApplication.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/PojavApplication.java @@ -18,7 +18,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import net.kdt.pojavlaunch.contextexecutor.ContextExecutor; +import net.kdt.pojavlaunch.lifecycle.ContextExecutor; import net.kdt.pojavlaunch.tasks.AsyncAssetManager; import net.kdt.pojavlaunch.utils.*; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ShowErrorActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ShowErrorActivity.java index 88de61926..e620ae03c 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ShowErrorActivity.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ShowErrorActivity.java @@ -9,7 +9,7 @@ import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import net.kdt.pojavlaunch.contextexecutor.ContextExecutorTask; +import net.kdt.pojavlaunch.lifecycle.ContextExecutorTask; import net.kdt.pojavlaunch.utils.NotificationUtils; import java.io.Serializable; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java index fb818f1c3..29b25c326 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java @@ -8,7 +8,6 @@ import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_NOTCH_SIZE; import android.app.Activity; import android.app.ActivityManager; -import android.app.AlertDialog; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.ProgressDialog; @@ -39,6 +38,8 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.NotificationManagerCompat; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; @@ -47,8 +48,9 @@ import androidx.fragment.app.FragmentTransaction; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import net.kdt.pojavlaunch.contextexecutor.ContextExecutor; -import net.kdt.pojavlaunch.contextexecutor.ContextExecutorTask; +import net.kdt.pojavlaunch.lifecycle.ContextExecutor; +import net.kdt.pojavlaunch.lifecycle.ContextExecutorTask; +import net.kdt.pojavlaunch.lifecycle.LifecycleAwareAlertDialog; import net.kdt.pojavlaunch.multirt.MultiRTUtils; import net.kdt.pojavlaunch.multirt.Runtime; import net.kdt.pojavlaunch.plugins.FFmpegPlugin; @@ -161,21 +163,18 @@ public final class Tools { NATIVE_LIB_DIR = ctx.getApplicationInfo().nativeLibraryDir; } - - public static void launchMinecraft(final Activity activity, MinecraftAccount minecraftAccount, + public static void launchMinecraft(final AppCompatActivity activity, MinecraftAccount minecraftAccount, MinecraftProfile minecraftProfile, String versionId, int versionJavaRequirement) throws Throwable { int freeDeviceMemory = getFreeDeviceMemory(activity); if(LauncherPreferences.PREF_RAM_ALLOCATION > freeDeviceMemory) { - Object memoryErrorLock = new Object(); - activity.runOnUiThread(() -> { - androidx.appcompat.app.AlertDialog.Builder b = new androidx.appcompat.app.AlertDialog.Builder(activity) - .setMessage(activity.getString(R.string.memory_warning_msg, freeDeviceMemory ,LauncherPreferences.PREF_RAM_ALLOCATION)) - .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {synchronized(memoryErrorLock){memoryErrorLock.notifyAll();}}) - .setOnCancelListener((i) -> {synchronized(memoryErrorLock){memoryErrorLock.notifyAll();}}); - b.show(); - }); - synchronized (memoryErrorLock) { - memoryErrorLock.wait(); + LifecycleAwareAlertDialog.DialogCreator dialogCreator = (dialog, builder) -> + builder.setMessage(activity.getString(R.string.memory_warning_msg, freeDeviceMemory, LauncherPreferences.PREF_RAM_ALLOCATION)) + .setPositiveButton(android.R.string.ok, (d, w)->{}); + + if(LifecycleAwareAlertDialog.haltOnDialog(activity.getLifecycle(), activity, dialogCreator)) { + return; // If the dialog's lifecycle has ended, return without + // actually launching the game, thus giving us the opportunity + // to start after the activity is shown again } } Runtime runtime = MultiRTUtils.forceReread(Tools.pickRuntime(minecraftProfile, versionJavaRequirement)); @@ -216,6 +215,8 @@ public final class Tools { if(Tools.isValidString(minecraftProfile.javaArgs)) args = minecraftProfile.javaArgs; FFmpegPlugin.discover(activity); JREUtils.launchJavaVM(activity, runtime, gamedir, javaArgList, args); + // If we returned, this means that the JVM exit dialog has been shown and we don't need to be active anymore. + // We never return otherwise. The process will be killed anyway, and thus we will become inactive } public static File getGameDirPath(@NonNull MinecraftProfile minecraftProfile){ diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/ContextAwareDoneListener.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/lifecycle/ContextAwareDoneListener.java similarity index 92% rename from app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/ContextAwareDoneListener.java rename to app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/lifecycle/ContextAwareDoneListener.java index 588ee1d1f..d894ee97e 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/tasks/ContextAwareDoneListener.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/lifecycle/ContextAwareDoneListener.java @@ -1,4 +1,4 @@ -package net.kdt.pojavlaunch.tasks; +package net.kdt.pojavlaunch.lifecycle; import static net.kdt.pojavlaunch.MainActivity.INTENT_MINECRAFT_VERSION; @@ -9,9 +9,10 @@ import android.content.Intent; import net.kdt.pojavlaunch.MainActivity; import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.Tools; -import net.kdt.pojavlaunch.contextexecutor.ContextExecutor; -import net.kdt.pojavlaunch.contextexecutor.ContextExecutorTask; +import net.kdt.pojavlaunch.lifecycle.ContextExecutor; +import net.kdt.pojavlaunch.lifecycle.ContextExecutorTask; import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; +import net.kdt.pojavlaunch.tasks.AsyncMinecraftDownloader; import net.kdt.pojavlaunch.utils.NotificationUtils; public class ContextAwareDoneListener implements AsyncMinecraftDownloader.DoneListener, ContextExecutorTask { diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutor.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/lifecycle/ContextExecutor.java similarity index 98% rename from app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutor.java rename to app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/lifecycle/ContextExecutor.java index 0df6618a1..41a2673df 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutor.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/lifecycle/ContextExecutor.java @@ -1,4 +1,4 @@ -package net.kdt.pojavlaunch.contextexecutor; +package net.kdt.pojavlaunch.lifecycle; import android.app.Activity; import android.app.Application; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutorTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/lifecycle/ContextExecutorTask.java similarity index 95% rename from app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutorTask.java rename to app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/lifecycle/ContextExecutorTask.java index 9d8b1d3c3..a4a0ba95a 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutorTask.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/lifecycle/ContextExecutorTask.java @@ -1,4 +1,4 @@ -package net.kdt.pojavlaunch.contextexecutor; +package net.kdt.pojavlaunch.lifecycle; import android.app.Activity; import android.content.Context; diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/lifecycle/LifecycleAwareAlertDialog.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/lifecycle/LifecycleAwareAlertDialog.java new file mode 100644 index 000000000..6a74611fc --- /dev/null +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/lifecycle/LifecycleAwareAlertDialog.java @@ -0,0 +1,125 @@ +package net.kdt.pojavlaunch.lifecycle; + +import android.content.Context; +import android.content.DialogInterface; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleEventObserver; +import androidx.lifecycle.LifecycleOwner; + +import net.kdt.pojavlaunch.Tools; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A class that implements a form of lifecycle awareness for AlertDialog + */ +public abstract class LifecycleAwareAlertDialog implements LifecycleEventObserver { + private Lifecycle mLifecycle; + private AlertDialog mDialog; + private boolean mLifecycleEnded = false; + + /** + * Show the lifecycle-aware dialog. + * Note that the DialogCreator may not be always invoked. + * @param lifecycle the lifecycle to follow + * @param context the context for the dialog + * @param dialogCreator an interface used to create the dialog. + * Note that any dismiss listeners added to the dialog must be wrapped + * with wrapDismissListener(). + */ + public void show(Lifecycle lifecycle, Context context, DialogCreator dialogCreator) { + this.mLifecycleEnded = false; + this.mLifecycle = lifecycle; + if(mLifecycle.getCurrentState().equals(Lifecycle.State.DESTROYED)) { + this.mLifecycleEnded = true; + dialogHidden(mLifecycleEnded); + return; + } + AlertDialog.Builder builder = new AlertDialog.Builder(context); + // Install the default cancel/dismiss handling + builder.setOnDismissListener(wrapDismissListener(null)); + dialogCreator.createDialog(this, builder); + mLifecycle.addObserver(this); + mDialog = builder.show(); + } + + /** + * Invoked when the dialog gets hidden either by cancel()/dismiss(), or if a lifecycle event + * happens. + * @param lifecycleEnded if the dialog was hidden due to a lifecycle event + */ + abstract protected void dialogHidden(boolean lifecycleEnded); + + protected void dispatchDialogHidden() { + new Exception().printStackTrace(); + dialogHidden(mLifecycleEnded); + mLifecycle.removeObserver(this); + } + + public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) { + if(event.equals(Lifecycle.Event.ON_DESTROY)) { + mDialog.dismiss(); + mLifecycleEnded = true; + } + } + + /** + * Wrap an OnDismissListener for use with this LifecycleAwareAlertDialog. Pass null to only invoke the + * default dialog hidden handling. + * @param listener your listener + * @return the wrapped listener + */ + public DialogInterface.OnDismissListener wrapDismissListener(DialogInterface.OnCancelListener listener) { + return dialog -> { + dispatchDialogHidden(); + if(listener != null) listener.onCancel(dialog); + }; + } + + public interface DialogCreator { + /** + * This methods is called when the LifecycleAwareAlertDialog needs to set up its dialog. + * @param alertDialog an instance of LifecycleAwareAlertDialog for wrapping listeners + * @param dialogBuilder the AlertDialog builder + */ + void createDialog(LifecycleAwareAlertDialog alertDialog, AlertDialog.Builder dialogBuilder); + } + + /** + * Show a dialog and halt the current thread until the dialog gets closed either due to user action or a lifecycle event. + * @param lifecycle the Lifecycle object that this dialog will track to automatically close upon destruction + * @param context the context used to show the dialog + * @param dialogCreator a DialogCreator that creates the dialog + * @return true if the dialog was automatically dismissed due to a lifecycle event. This may happen + * before the dialog creator is used, so make sure to to handle the return value of the function. + * false otherwise + * @throws InterruptedException if the thread was interrupted while waiting for the dialog + */ + + public static boolean haltOnDialog(Lifecycle lifecycle, Context context, DialogCreator dialogCreator) throws InterruptedException { + Object waitLock = new Object(); + AtomicBoolean hasLifecycleEnded = new AtomicBoolean(false); + // This runnable is moved here in order to reduce bracket/lambda hell + Runnable showDialogRunnable = () -> { + LifecycleAwareAlertDialog lifecycleAwareDialog = new LifecycleAwareAlertDialog() { + @Override + protected void dialogHidden(boolean lifecycleEnded) { + hasLifecycleEnded.set(lifecycleEnded); + synchronized(waitLock){waitLock.notifyAll();} + } + }; + lifecycleAwareDialog.show(lifecycle, context, dialogCreator); + }; + synchronized (waitLock) { + Tools.runOnUiThread(showDialogRunnable); + // the wait() method makes the thread wait on the end of the synchronized block. + // so we put it here to make sure that the thread won't get notified before wait() + // is called + waitLock.wait(); + } + return hasLifecycleEnded.get(); + } +} diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/MirrorTamperedException.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/MirrorTamperedException.java index 1c464e618..22691528c 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/MirrorTamperedException.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/mirrors/MirrorTamperedException.java @@ -7,7 +7,7 @@ import android.text.Html; import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.ShowErrorActivity; -import net.kdt.pojavlaunch.contextexecutor.ContextExecutorTask; +import net.kdt.pojavlaunch.lifecycle.ContextExecutorTask; import net.kdt.pojavlaunch.prefs.LauncherPreferences; public class MirrorTamperedException extends Exception implements ContextExecutorTask { diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/GameService.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/GameService.java index 9329e05a5..d259490f0 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/GameService.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/GameService.java @@ -2,15 +2,14 @@ package net.kdt.pojavlaunch.services; import android.app.PendingIntent; import android.app.Service; -import android.content.Context; import android.content.Intent; +import android.os.Binder; import android.os.Build; import android.os.IBinder; import android.os.Process; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; -import androidx.core.content.ContextCompat; import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.Tools; @@ -20,10 +19,7 @@ import java.lang.ref.WeakReference; public class GameService extends Service { private static final WeakReference sGameService = new WeakReference<>(null); - public static void startService(Context context) { - Intent intent = new Intent(context, GameService.class); - ContextCompat.startForegroundService(context, intent); - } + private final LocalBinder mLocalBinder = new LocalBinder(); @Override public void onCreate() { @@ -47,7 +43,7 @@ public class GameService extends Service { .addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.notification_terminate), pendingKillIntent) .setSmallIcon(R.drawable.notif_icon) .setNotificationSilent(); - startForeground(NotificationUtils.NOTIFICATION_ID_GAME_SERVICE, notificationBuilder.build()); + startForeground(NotificationUtils.NOTIFICATION_ID_GAME_SERVICE, notificationBuilder.build()); return START_NOT_STICKY; // non-sticky so android wont try restarting the game after the user uses the "Quit" button } @@ -61,6 +57,10 @@ public class GameService extends Service { @Nullable @Override public IBinder onBind(Intent intent) { - return null; + return mLocalBinder; + } + + public static class LocalBinder extends Binder { + public boolean isActive; } } diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java index 7bba4901b..795f010e6 100644 --- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java +++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/utils/JREUtils.java @@ -16,12 +16,15 @@ import android.system.*; import android.util.*; import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; + import com.oracle.dalvik.*; import java.io.*; import java.util.*; import net.kdt.pojavlaunch.*; import net.kdt.pojavlaunch.extra.ExtraConstants; import net.kdt.pojavlaunch.extra.ExtraCore; +import net.kdt.pojavlaunch.lifecycle.LifecycleAwareAlertDialog; import net.kdt.pojavlaunch.multirt.MultiRTUtils; import net.kdt.pojavlaunch.multirt.Runtime; import net.kdt.pojavlaunch.plugins.FFmpegPlugin; @@ -265,7 +268,7 @@ public class JREUtils { // return ldLibraryPath; } - public static int launchJavaVM(final Activity activity, final Runtime runtime, File gameDirectory, final List JVMArgs, final String userArgsString) throws Throwable { + public static void launchJavaVM(final AppCompatActivity activity, final Runtime runtime, File gameDirectory, final List JVMArgs, final String userArgsString) throws Throwable { String runtimeHome = MultiRTUtils.getRuntimeHome(runtime.name).getAbsolutePath(); JREUtils.relocateLibPath(runtime, runtimeHome); @@ -303,18 +306,13 @@ public class JREUtils { final int exitCode = VMLauncher.launchJVM(userArgs.toArray(new String[0])); Logger.appendToLog("Java Exit code: " + exitCode); if (exitCode != 0) { - activity.runOnUiThread(() -> { - AlertDialog.Builder dialog = new AlertDialog.Builder(activity); - dialog.setMessage(activity.getString(R.string.mcn_exit_title, exitCode)); + LifecycleAwareAlertDialog.DialogCreator dialogCreator = (dialog, builder)-> + builder.setMessage(activity.getString(R.string.mcn_exit_title, exitCode)) + .setPositiveButton(R.string.main_share_logs, (dialogInterface, which)-> shareLog(activity)); - dialog.setPositiveButton(R.string.main_share_logs, (p1, p2) -> { - shareLog(activity); - MainActivity.fullyExit(); - }); - dialog.show(); - }); + LifecycleAwareAlertDialog.haltOnDialog(activity.getLifecycle(), activity, dialogCreator); } - return exitCode; + MainActivity.fullyExit(); } /**