Feat[notifs]: features for remote error reporting, unified notification constants

TODO: also use it for Minecraft downloads
This commit is contained in:
artdeell 2023-08-22 21:48:07 +03:00 committed by ArtDev
parent 2980afdd05
commit f1e88e2068
13 changed files with 243 additions and 12 deletions

View File

@ -78,6 +78,9 @@
android:name=".FatalErrorActivity" android:name=".FatalErrorActivity"
android:configChanges="keyboardHidden|orientation|screenSize|keyboard|navigation" android:configChanges="keyboardHidden|orientation|screenSize|keyboard|navigation"
android:theme="@style/Theme.AppCompat.DayNight.Dialog" /> android:theme="@style/Theme.AppCompat.DayNight.Dialog" />
<activity android:name=".ShowErrorActivity"
android:configChanges="keyboardHidden|orientation|screenSize|keyboard|navigation"
android:theme="@style/Theme.AppCompat.DayNight.Dialog" />
<activity <activity
android:name=".ExitActivity" android:name=".ExitActivity"
android:configChanges="keyboardHidden|orientation|screenSize|keyboard|navigation" android:configChanges="keyboardHidden|orientation|screenSize|keyboard|navigation"

View File

@ -24,6 +24,7 @@ import androidx.fragment.app.FragmentManager;
import com.kdt.mcgui.ProgressLayout; import com.kdt.mcgui.ProgressLayout;
import com.kdt.mcgui.mcAccountSpinner; import com.kdt.mcgui.mcAccountSpinner;
import net.kdt.pojavlaunch.contextexecutor.ContextExecutor;
import net.kdt.pojavlaunch.fragments.MainMenuFragment; import net.kdt.pojavlaunch.fragments.MainMenuFragment;
import net.kdt.pojavlaunch.fragments.MicrosoftLoginFragment; import net.kdt.pojavlaunch.fragments.MicrosoftLoginFragment;
import net.kdt.pojavlaunch.extra.ExtraConstants; import net.kdt.pojavlaunch.extra.ExtraConstants;
@ -183,12 +184,14 @@ public class LauncherActivity extends BaseActivity {
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
ContextExecutor.setActivity(this);
mInstallTracker.attach(); mInstallTracker.attach();
} }
@Override @Override
protected void onPause() { protected void onPause() {
super.onPause(); super.onPause();
ContextExecutor.clearActivity();
mInstallTracker.detach(); mInstallTracker.detach();
} }

View File

@ -18,6 +18,7 @@ import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import net.kdt.pojavlaunch.contextexecutor.ContextExecutor;
import net.kdt.pojavlaunch.tasks.AsyncAssetManager; import net.kdt.pojavlaunch.tasks.AsyncAssetManager;
import net.kdt.pojavlaunch.utils.*; import net.kdt.pojavlaunch.utils.*;
@ -27,6 +28,7 @@ public class PojavApplication extends Application {
@Override @Override
public void onCreate() { public void onCreate() {
ContextExecutor.setApplication(this);
Thread.setDefaultUncaughtExceptionHandler((thread, th) -> { Thread.setDefaultUncaughtExceptionHandler((thread, th) -> {
boolean storagePermAllowed = (Build.VERSION.SDK_INT < 23 || Build.VERSION.SDK_INT >= 29 || 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); 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); startActivity(ferrorIntent);
} }
} }
@Override @Override
public void onTerminate() {
super.onTerminate();
ContextExecutor.clearApplication();
}
@Override
protected void attachBaseContext(Context base) { protected void attachBaseContext(Context base) {
super.attachBaseContext(LocaleUtils.setLocale(base)); super.attachBaseContext(LocaleUtils.setLocale(base));
} }

View File

@ -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());
}
}

View File

@ -46,6 +46,7 @@ import androidx.fragment.app.FragmentTransaction;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import net.kdt.pojavlaunch.contextexecutor.ContextExecutor;
import net.kdt.pojavlaunch.multirt.MultiRTUtils; import net.kdt.pojavlaunch.multirt.MultiRTUtils;
import net.kdt.pojavlaunch.multirt.Runtime; import net.kdt.pojavlaunch.multirt.Runtime;
import net.kdt.pojavlaunch.plugins.FFmpegPlugin; 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) { public static void dialogOnUiThread(final Activity activity, final CharSequence title, final CharSequence message) {
activity.runOnUiThread(()->dialog(activity, title, message)); activity.runOnUiThread(()->dialog(activity, title, message));
} }

View File

@ -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<Application> sApplication;
private static WeakReference<Activity> 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> T getWeakReference(WeakReference<T> weakReference) {
if(weakReference == null) return null;
return weakReference.get();
}
}

View File

@ -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);
}

View File

@ -57,7 +57,7 @@ public interface ModpackApi {
if (loaderInfo == null) return; if (loaderInfo == null) return;
loaderInfo.getDownloadTask(new NotificationDownloadListener(context, loaderInfo)).run(); loaderInfo.getDownloadTask(new NotificationDownloadListener(context, loaderInfo)).run();
}catch (IOException e) { }catch (IOException e) {
// TODO: pass on the IOException to a relevant handler Tools.showErrorRemote(context, R.string.modpack_install_download_failed, e);
} }
}); });
} }

View File

@ -13,6 +13,7 @@ import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools; import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener; import net.kdt.pojavlaunch.modloaders.ModloaderDownloadListener;
import net.kdt.pojavlaunch.modloaders.modpacks.ModloaderInstallTracker; import net.kdt.pojavlaunch.modloaders.modpacks.ModloaderInstallTracker;
import net.kdt.pojavlaunch.value.NotificationConstants;
import java.io.File; import java.io.File;
@ -46,21 +47,21 @@ public class NotificationDownloadListener implements ModloaderDownloadListener {
@Override @Override
public void onDownloadError(Exception e) { 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) { private void sendIntentNotification(Intent intent, int contentText) {
PendingIntent pendingInstallIntent = PendingIntent pendingInstallIntent =
PendingIntent.getActivity(mContext, 0, PendingIntent.getActivity(mContext, NotificationConstants.PENDINGINTENT_CODE_DOWNLOAD_SERVICE,
intent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0); intent, Build.VERSION.SDK_INT >=23 ? PendingIntent.FLAG_IMMUTABLE : 0);
mNotificationBuilder.setContentText(mContext.getText(contentText)); mNotificationBuilder.setContentText(mContext.getText(contentText));
mNotificationBuilder.setContentIntent(pendingInstallIntent); mNotificationBuilder.setContentIntent(pendingInstallIntent);
mNotificationManager.notify(3, mNotificationBuilder.build()); mNotificationManager.notify(NotificationConstants.NOTIFICATION_ID_DOWNLOAD_LISTENER, mNotificationBuilder.build());
} }
private void sendEmptyNotification(int contentText) { private void sendEmptyNotification(int contentText) {
mNotificationBuilder.setContentText(mContext.getText(contentText)); mNotificationBuilder.setContentText(mContext.getText(contentText));
mNotificationManager.notify(3, mNotificationBuilder.build()); mNotificationManager.notify(NotificationConstants.NOTIFICATION_ID_DOWNLOAD_LISTENER, mNotificationBuilder.build());
} }
} }

View File

@ -14,6 +14,7 @@ import androidx.core.content.ContextCompat;
import net.kdt.pojavlaunch.R; import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools; import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.value.NotificationConstants;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
@ -38,14 +39,15 @@ public class GameService extends Service {
} }
Intent killIntent = new Intent(getApplicationContext(), GameService.class); Intent killIntent = new Intent(getApplicationContext(), GameService.class);
killIntent.putExtra("kill", true); 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") NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, "channel_id")
.setContentTitle(getString(R.string.lazy_service_default_title)) .setContentTitle(getString(R.string.lazy_service_default_title))
.setContentText(getString(R.string.notification_game_runs)) .setContentText(getString(R.string.notification_game_runs))
.addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.notification_terminate), pendingKillIntent) .addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.notification_terminate), pendingKillIntent)
.setSmallIcon(R.drawable.notif_icon) .setSmallIcon(R.drawable.notif_icon)
.setNotificationSilent(); .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 return START_NOT_STICKY; // non-sticky so android wont try restarting the game after the user uses the "Quit" button
} }

View File

@ -19,6 +19,7 @@ import net.kdt.pojavlaunch.R;
import net.kdt.pojavlaunch.Tools; import net.kdt.pojavlaunch.Tools;
import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper; import net.kdt.pojavlaunch.progresskeeper.ProgressKeeper;
import net.kdt.pojavlaunch.progresskeeper.TaskCountListener; import net.kdt.pojavlaunch.progresskeeper.TaskCountListener;
import net.kdt.pojavlaunch.value.NotificationConstants;
/** /**
* Lazy service which allows the process not to get killed. * 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()); notificationManagerCompat = NotificationManagerCompat.from(getApplicationContext());
Intent killIntent = new Intent(getApplicationContext(), ProgressService.class); Intent killIntent = new Intent(getApplicationContext(), ProgressService.class);
killIntent.putExtra("kill", true); 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") mNotificationBuilder = new NotificationCompat.Builder(this, "channel_id")
.setContentTitle(getString(R.string.lazy_service_default_title)) .setContentTitle(getString(R.string.lazy_service_default_title))
.addAction(android.R.drawable.ic_menu_close_clear_cancel, getString(R.string.notification_terminate), pendingKillIntent) .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!"); Log.d("ProgressService", "Started!");
mNotificationBuilder.setContentText(getString(R.string.progresslayout_tasks_in_progress, ProgressKeeper.getTaskCount())); 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(); if(ProgressKeeper.getTaskCount() < 1) stopSelf();
else ProgressKeeper.addTaskCountListener(this, false); else ProgressKeeper.addTaskCountListener(this, false);

View File

@ -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;
}

View File

@ -423,5 +423,9 @@
<string name="modpack_install_notification_title">Pojav Modpack Installer</string> <string name="modpack_install_notification_title">Pojav Modpack Installer</string>
<string name="modpack_install_notification_success">Click here to finish modpack installation</string> <string name="modpack_install_notification_success">Click here to finish modpack installation</string>
<string name="modpack_install_notification_data_not_available">Failed to download mod loader information</string> <string name="modpack_install_notification_data_not_available">Failed to download mod loader information</string>
<string name="modpack_install_notification_download_failed">Failed to download the mod loader files</string> <string name="modpack_install_modloader_download_failed">Failed to download the mod loader files</string>
<string name="modpack_install_download_failed">Failed to download modpack files</string>
<string name="notif_error_occured">An error has occurred</string>
<string name="notif_error_occured_desc">Click to see more details</string>
</resources> </resources>