diff --git a/app_pojavlauncher/src/main/AndroidManifest.xml b/app_pojavlauncher/src/main/AndroidManifest.xml
index c08be997c..4147e1f3a 100644
--- a/app_pojavlauncher/src/main/AndroidManifest.xml
+++ b/app_pojavlauncher/src/main/AndroidManifest.xml
@@ -78,6 +78,9 @@
android:name=".FatalErrorActivity"
android:configChanges="keyboardHidden|orientation|screenSize|keyboard|navigation"
android:theme="@style/Theme.AppCompat.DayNight.Dialog" />
+
{
boolean storagePermAllowed = (Build.VERSION.SDK_INT < 23 || Build.VERSION.SDK_INT >= 29 ||
ActivityCompat.checkSelfPermission(PojavApplication.this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) && Tools.checkStorageRoot(PojavApplication.this);
@@ -78,8 +80,14 @@ public class PojavApplication extends Application {
startActivity(ferrorIntent);
}
}
-
- @Override
+
+ @Override
+ public void onTerminate() {
+ super.onTerminate();
+ ContextExecutor.clearApplication();
+ }
+
+ @Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(LocaleUtils.setLocale(base));
}
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ShowErrorActivity.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ShowErrorActivity.java
new file mode 100644
index 000000000..ec64ee7fb
--- /dev/null
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/ShowErrorActivity.java
@@ -0,0 +1,75 @@
+package net.kdt.pojavlaunch;
+
+import android.app.Activity;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.core.app.NotificationCompat;
+
+import net.kdt.pojavlaunch.contextexecutor.ContextExecutorTask;
+import net.kdt.pojavlaunch.value.NotificationConstants;
+
+import java.io.Serializable;
+
+public class ShowErrorActivity extends Activity {
+
+ private static final String ERROR_ACTIVITY_REMOTE_TASK = "remoteTask";
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Intent intent = getIntent();
+ if(intent == null) {
+ finish();
+ return;
+ }
+ RemoteErrorTask remoteErrorTask = (RemoteErrorTask) intent.getSerializableExtra(ERROR_ACTIVITY_REMOTE_TASK);
+ if(remoteErrorTask == null) {
+ finish();
+ return;
+ }
+ remoteErrorTask.executeWithActivity(this);
+ }
+
+
+ public static class RemoteErrorTask implements ContextExecutorTask, Serializable {
+ private final Throwable mThrowable;
+ private final String mRolledMsg;
+
+ public RemoteErrorTask(Throwable mThrowable, String mRolledMsg) {
+ this.mThrowable = mThrowable;
+ this.mRolledMsg = mRolledMsg;
+ }
+ @Override
+ public void executeWithActivity(Activity activity) {
+ Tools.showError(activity, mRolledMsg, mThrowable);
+ }
+
+ @Override
+ public void executeWithApplication(Context context) {
+ sendNotification(context, this);
+ }
+ }
+ private static void sendNotification(Context context, RemoteErrorTask remoteErrorTask) {
+
+ Intent showErrorIntent = new Intent(context, ShowErrorActivity.class);
+ showErrorIntent.putExtra(ERROR_ACTIVITY_REMOTE_TASK, remoteErrorTask);
+
+ PendingIntent pendingIntent = PendingIntent.getActivity(context, NotificationConstants.PENDINGINTENT_CODE_SHOW_ERROR, showErrorIntent,
+ Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0);
+
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, context.getString(R.string.notif_channel_id))
+ .setContentTitle(context.getString(R.string.notif_error_occured))
+ .setContentText(context.getString(R.string.notif_error_occured_desc))
+ .setSmallIcon(R.drawable.notif_icon)
+ .setContentIntent(pendingIntent);
+ notificationManager.notify(NotificationConstants.NOTIFICATION_ID_SHOW_ERROR, notificationBuilder.build());
+ }
+
+}
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 f2b8f94d9..c5cf7c335 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java
@@ -46,6 +46,7 @@ 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.multirt.MultiRTUtils;
import net.kdt.pojavlaunch.multirt.Runtime;
import net.kdt.pojavlaunch.plugins.FFmpegPlugin;
@@ -595,6 +596,26 @@ public final class Tools {
}
}
+ public static void showErrorRemote(Throwable e) {
+ showErrorRemote(null, e);
+ }
+ public static void showErrorRemote(Context context, int rolledMessage, Throwable e) {
+ showErrorRemote(context.getString(rolledMessage), e);
+ }
+ public static void showErrorRemote(String rolledMessage, Throwable e) {
+ // I WILL embrace layer violations because Android's concept of layers is STUPID
+ // We live in the same process anyway, why make it any more harder with this needless
+ // abstraction?
+ // Also, to @TorchDragon in r/AndroidDev discord: if Android is not for general computing,
+ // and all apps need so much babysitting, why did they put an SoC equivalent to
+ // my main PC in power?
+
+ // Add your Context-related rage here
+ ContextExecutor.execute(new ShowErrorActivity.RemoteErrorTask(e, rolledMessage));
+ }
+
+
+
public static void dialogOnUiThread(final Activity activity, final CharSequence title, final CharSequence message) {
activity.runOnUiThread(()->dialog(activity, title, message));
}
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutor.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutor.java
new file mode 100644
index 000000000..7b17f5cc0
--- /dev/null
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutor.java
@@ -0,0 +1,75 @@
+package net.kdt.pojavlaunch.contextexecutor;
+
+import android.app.Activity;
+import android.app.Application;
+
+import net.kdt.pojavlaunch.Tools;
+
+import java.lang.ref.WeakReference;
+
+public class ContextExecutor {
+ private static WeakReference sApplication;
+ private static WeakReference sActivity;
+
+
+ /**
+ * Schedules a ContextExecutorTask to be executed. For more info on tasks, please read
+ * ContextExecutorTask.java
+ * @param contextExecutorTask the task to be executed
+ */
+ public static void execute(ContextExecutorTask contextExecutorTask) {
+ Tools.runOnUiThread(()->executeOnUiThread(contextExecutorTask));
+ }
+
+ private static void executeOnUiThread(ContextExecutorTask contextExecutorTask) {
+ Activity activity = getWeakReference(sActivity);
+ if(activity != null) {
+ contextExecutorTask.executeWithActivity(activity);
+ return;
+ }
+ Application application = getWeakReference(sApplication);
+ if(application != null) {
+ contextExecutorTask.executeWithApplication(application);
+ }else {
+ throw new RuntimeException("ContextExecutor.execute() called before Application.onCreate!");
+ }
+ }
+
+ /**
+ * Set the Activity that this ContextExecutor will use for executing tasks
+ * @param activity the activity to be used
+ */
+ public static void setActivity(Activity activity) {
+ sActivity = new WeakReference<>(activity);
+ }
+
+ /**
+ * Clear the Activity previously set, so thet ContextExecutor won't use it to execute tasks.
+ */
+ public static void clearActivity() {
+ if(sActivity != null)
+ sActivity.clear();
+ }
+
+ /**
+ * Set the Application that will be used to execute tasks if the Activity won't be available.
+ * @param application the application to use as the fallback
+ */
+ public static void setApplication(Application application) {
+ sApplication = new WeakReference<>(application);
+ }
+
+ /**
+ * Clear the Application previously set, so that ContextExecutor will notify the user of a critical error
+ * that is executing code after the application is ended by the system.
+ */
+ public static void clearApplication() {
+ if(sApplication != null)
+ sApplication.clear();
+ }
+
+ private static T getWeakReference(WeakReference weakReference) {
+ if(weakReference == null) return null;
+ return weakReference.get();
+ }
+}
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutorTask.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutorTask.java
new file mode 100644
index 000000000..9d8b1d3c3
--- /dev/null
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/contextexecutor/ContextExecutorTask.java
@@ -0,0 +1,25 @@
+package net.kdt.pojavlaunch.contextexecutor;
+
+import android.app.Activity;
+import android.content.Context;
+
+/**
+ * A ContextExecutorTask is a task that can dynamically change its behaviour, based on the context
+ * used for its execution. This can be used to implement for ex. error/finish notifications from
+ * background threads that may live with the Service after the activity that started them died.
+ */
+public interface ContextExecutorTask {
+ /**
+ * ContextExecutor will execute this function first if a foreground Activity that was attached to the
+ * ContextExecutor is available.
+ * @param activity the activity
+ */
+ void executeWithActivity(Activity activity);
+
+ /**
+ * ContextExecutor will execute this function if a foreground Activity is not available, but the app
+ * is still running.
+ * @param context the application context
+ */
+ void executeWithApplication(Context context);
+}
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java
index 7f6b20b1c..141468af8 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/ModpackApi.java
@@ -57,7 +57,7 @@ public interface ModpackApi {
if (loaderInfo == null) return;
loaderInfo.getDownloadTask(new NotificationDownloadListener(context, loaderInfo)).run();
}catch (IOException e) {
- // TODO: pass on the IOException to a relevant handler
+ Tools.showErrorRemote(context, R.string.modpack_install_download_failed, e);
}
});
}
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/NotificationDownloadListener.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/NotificationDownloadListener.java
index 34d12ad1a..98f4be4e6 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/NotificationDownloadListener.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/modloaders/modpacks/api/NotificationDownloadListener.java
@@ -13,6 +13,7 @@ import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener;
import net.kdt.pojavlaunch.modloaders.modpacks.ModloaderInstallTracker;
+import net.kdt.pojavlaunch.value.NotificationConstants;
import java.io.File;
@@ -46,21 +47,21 @@ public class NotificationDownloadListener implements ModloaderDownloadListener {
@Override
public void onDownloadError(Exception e) {
- Tools.runOnUiThread(()->sendEmptyNotification(R.string.modpack_install_notification_download_failed));
+ Tools.showErrorRemote(mContext, R.string.modpack_install_modloader_download_failed, e);
}
private void sendIntentNotification(Intent intent, int contentText) {
PendingIntent pendingInstallIntent =
- PendingIntent.getActivity(mContext, 0,
+ PendingIntent.getActivity(mContext, NotificationConstants.PENDINGINTENT_CODE_DOWNLOAD_SERVICE,
intent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0);
mNotificationBuilder.setContentText(mContext.getText(contentText));
mNotificationBuilder.setContentIntent(pendingInstallIntent);
- mNotificationManager.notify(3, mNotificationBuilder.build());
+ mNotificationManager.notify(NotificationConstants.NOTIFICATION_ID_DOWNLOAD_LISTENER, mNotificationBuilder.build());
}
private void sendEmptyNotification(int contentText) {
mNotificationBuilder.setContentText(mContext.getText(contentText));
- mNotificationManager.notify(3, mNotificationBuilder.build());
+ mNotificationManager.notify(NotificationConstants.NOTIFICATION_ID_DOWNLOAD_LISTENER, mNotificationBuilder.build());
}
}
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 00babeca1..a88416961 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
@@ -14,6 +14,7 @@ import androidx.core.content.ContextCompat;
import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
+import net.kdt.pojavlaunch.value.NotificationConstants;
import java.lang.ref.WeakReference;
@@ -38,14 +39,15 @@ public class GameService extends Service {
}
Intent killIntent = new Intent(getApplicationContext(), GameService.class);
killIntent.putExtra("kill", true);
- PendingIntent pendingKillIntent = PendingIntent.getService(this, 0, killIntent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0);
+ PendingIntent pendingKillIntent = PendingIntent.getService(this, NotificationConstants.PENDINGINTENT_CODE_KILL_GAME_SERVICE
+ , killIntent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0);
NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, "channel_id")
.setContentTitle(getString(R.string.lazy_service_default_title))
.setContentText(getString(R.string.notification_game_runs))
.addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.notification_terminate), pendingKillIntent)
.setSmallIcon(R.drawable.notif_icon)
.setNotificationSilent();
- startForeground(2, notificationBuilder.build());
+ startForeground(NotificationConstants.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
}
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java
index b5fc6396b..2ffa71eb9 100644
--- a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/services/ProgressService.java
@@ -19,6 +19,7 @@ import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
import net.kdt.pojavlaunch.progresskeeper.TaskCountListener;
+import net.kdt.pojavlaunch.value.NotificationConstants;
/**
* Lazy service which allows the process not to get killed.
@@ -42,7 +43,8 @@ public class ProgressService extends Service implements TaskCountListener {
notificationManagerCompat = NotificationManagerCompat.from(getApplicationContext());
Intent killIntent = new Intent(getApplicationContext(), ProgressService.class);
killIntent.putExtra("kill", true);
- PendingIntent pendingKillIntent = PendingIntent.getService(this, 0, killIntent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0);
+ PendingIntent pendingKillIntent = PendingIntent.getService(this, NotificationConstants.PENDINGINTENT_CODE_KILL_PROGRESS_SERVICE
+ , killIntent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0);
mNotificationBuilder = new NotificationCompat.Builder(this, "channel_id")
.setContentTitle(getString(R.string.lazy_service_default_title))
.addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.notification_terminate), pendingKillIntent)
@@ -62,7 +64,7 @@ public class ProgressService extends Service implements TaskCountListener {
}
Log.d("ProgressService", "Started!");
mNotificationBuilder.setContentText(getString(R.string.progresslayout_tasks_in_progress, ProgressKeeper.getTaskCount()));
- startForeground(1, mNotificationBuilder.build());
+ startForeground(NotificationConstants.NOTIFICATION_ID_PROGRESS_SERVICE, mNotificationBuilder.build());
if(ProgressKeeper.getTaskCount() < 1) stopSelf();
else ProgressKeeper.addTaskCountListener(this, false);
diff --git a/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/NotificationConstants.java b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/NotificationConstants.java
new file mode 100644
index 000000000..02c23596e
--- /dev/null
+++ b/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/value/NotificationConstants.java
@@ -0,0 +1,12 @@
+package net.kdt.pojavlaunch.value;
+
+public class NotificationConstants {
+ public static final int NOTIFICATION_ID_PROGRESS_SERVICE = 1;
+ public static final int NOTIFICATION_ID_GAME_SERVICE = 2;
+ public static final int NOTIFICATION_ID_DOWNLOAD_LISTENER = 3;
+ public static final int NOTIFICATION_ID_SHOW_ERROR = 4;
+ public static final int PENDINGINTENT_CODE_KILL_PROGRESS_SERVICE = 1;
+ public static final int PENDINGINTENT_CODE_KILL_GAME_SERVICE = 2;
+ public static final int PENDINGINTENT_CODE_DOWNLOAD_SERVICE = 3;
+ public static final int PENDINGINTENT_CODE_SHOW_ERROR = 4;
+}
diff --git a/app_pojavlauncher/src/main/res/values/strings.xml b/app_pojavlauncher/src/main/res/values/strings.xml
index f9b821395..f13fbecd9 100644
--- a/app_pojavlauncher/src/main/res/values/strings.xml
+++ b/app_pojavlauncher/src/main/res/values/strings.xml
@@ -423,5 +423,9 @@
Pojav Modpack Installer
Click here to finish modpack installation
Failed to download mod loader information
- Failed to download the mod loader files
+ Failed to download the mod loader files
+ Failed to download modpack files
+
+ An error has occurred
+ Click to see more details