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