From 1f7a5f626d51cc698c74fa239ef37b22dbe096cc Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Thu, 29 Jun 2023 18:37:27 +0200 Subject: [PATCH] Show notifications from workers (#3760) Fix a crash where workers, in some conditions, should show a notification. These are sent to a dedicated channel with no importance. Convert NotificationWorker to a CoroutineWorker and remove its use of `runBlocking`. Fixes #3754 --- app/lint-baseline.xml | 140 +++++++++--------- .../keylesspalace/tusky/TuskyApplication.kt | 3 + .../notifications/NotificationFetcher.kt | 17 +-- .../notifications/NotificationHelper.java | 52 ++++++- .../tusky/worker/NotificationWorker.kt | 17 ++- .../tusky/worker/PruneCacheWorker.kt | 9 ++ app/src/main/res/values/strings.xml | 4 + 7 files changed, 158 insertions(+), 84 deletions(-) diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml index e01146f5..bf4a0466 100644 --- a/app/lint-baseline.xml +++ b/app/lint-baseline.xml @@ -80,7 +80,7 @@ errorLine2=" ~~~~~~~"> @@ -791,22 +791,22 @@ + errorLine1=" <item quantity="one">برهم‌کنشی جدید</item>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + errorLine1=" <item quantity="one">توصیف محتوا برای کم‌بینایان (کران ۱ نویسه)</item>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -839,7 +839,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -850,7 +850,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -861,7 +861,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -1532,7 +1532,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> @@ -1543,7 +1543,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> @@ -1653,7 +1653,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2500,7 +2500,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2511,7 +2511,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2522,7 +2522,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> @@ -2533,7 +2533,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2544,7 +2544,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -2555,7 +2555,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -2566,7 +2566,7 @@ errorLine2=" ~~~~~~~~~~~~~~"> @@ -2577,7 +2577,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2588,7 +2588,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2599,7 +2599,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> @@ -2610,7 +2610,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2621,7 +2621,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> @@ -2632,7 +2632,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2643,7 +2643,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2654,7 +2654,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -2665,7 +2665,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2676,7 +2676,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2687,7 +2687,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2698,7 +2698,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> @@ -2709,7 +2709,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2720,7 +2720,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2731,7 +2731,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2742,7 +2742,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -2940,7 +2940,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -2951,7 +2951,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -3116,7 +3116,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~"> @@ -3127,7 +3127,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3138,7 +3138,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3226,7 +3226,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3237,7 +3237,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3248,7 +3248,7 @@ errorLine2=" ~~~~~~~~~"> @@ -3259,7 +3259,7 @@ errorLine2=" ~~~~~~~~~"> @@ -4425,7 +4425,7 @@ errorLine2=" ~~~~~~~~~"> @@ -4436,7 +4436,7 @@ errorLine2=" ~~~~~~~"> @@ -4447,7 +4447,7 @@ errorLine2=" ~~~~~~~"> @@ -5096,7 +5096,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> @@ -6071,7 +6071,7 @@ errorLine2=" ~~~~~~~~"> @@ -6082,7 +6082,7 @@ errorLine2=" ~~~~~~~~"> @@ -6115,7 +6115,7 @@ errorLine2=" ~~~~~~~~"> @@ -6126,7 +6126,7 @@ errorLine2=" ~~~~~~~~"> @@ -6225,7 +6225,7 @@ errorLine2=" ~~~~~~~~"> @@ -6236,7 +6236,7 @@ errorLine2=" ~~~~~~~~"> @@ -6247,7 +6247,7 @@ errorLine2=" ~~~~~~~~"> @@ -6258,7 +6258,7 @@ errorLine2=" ~~~~~~~~"> @@ -6896,7 +6896,7 @@ errorLine2=" ~~~~~~~~"> @@ -6907,7 +6907,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -6918,7 +6918,7 @@ errorLine2=" ~~~~~~~"> @@ -6929,7 +6929,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -6940,7 +6940,7 @@ errorLine2=" ~~~~~~~~~~~~"> @@ -6951,7 +6951,7 @@ errorLine2=" ~~~~~~~~~~~~~"> @@ -6962,7 +6962,7 @@ errorLine2=" ~~~~~~~"> @@ -6973,7 +6973,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -6984,7 +6984,7 @@ errorLine2=" ~~~~~~~~~~~~~"> @@ -6995,7 +6995,7 @@ errorLine2=" ~~~~~~~"> @@ -7006,7 +7006,7 @@ errorLine2=" ~~~~~~~"> @@ -7017,7 +7017,7 @@ errorLine2=" ~~~~~~~~~~~~~~~~~~~"> @@ -7028,7 +7028,7 @@ errorLine2=" ~~~~~~~~~~~~~"> diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt index 3107cea8..e7c64699 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -23,6 +23,7 @@ import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import autodispose2.AutoDisposePlugins +import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.di.AppInjector import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.SCHEMA_VERSION @@ -95,6 +96,8 @@ class TuskyApplication : Application(), HasAndroidInjector { Log.w("RxJava", "undeliverable exception", it) } + NotificationHelper.createWorkerNotificationChannel(this) + WorkManager.initialize( this, androidx.work.Configuration.Builder() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt index 89f4222b..633ca08f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt @@ -11,9 +11,10 @@ import com.keylesspalace.tusky.entity.Marker import com.keylesspalace.tusky.entity.Notification import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.isLessThan -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.delay import javax.inject.Inject import kotlin.math.min +import kotlin.time.Duration.Companion.milliseconds /** * Fetch Mastodon notifications and show Android notifications, with summaries, for them. @@ -29,19 +30,17 @@ class NotificationFetcher @Inject constructor( private val accountManager: AccountManager, private val context: Context ) { - fun fetchAndShow() { + suspend fun fetchAndShow() { for (account in accountManager.getAllAccountsOrderedByActive()) { if (account.notificationsEnabled) { try { val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager // Create sorted list of new notifications - val notifications = runBlocking { // OK, because in a worker thread - fetchNewNotifications(account) - .filter { filterNotification(notificationManager, account, it) } - .sortedWith(compareBy({ it.id.length }, { it.id })) // oldest notifications first - .toMutableList() - } + val notifications = fetchNewNotifications(account) + .filter { filterNotification(notificationManager, account, it) } + .sortedWith(compareBy({ it.id.length }, { it.id })) // oldest notifications first + .toMutableList() // There's a maximum limit on the number of notifications an Android app // can display. If the total number of notifications (current notifications, @@ -82,7 +81,7 @@ class NotificationFetcher @Inject constructor( // Android will rate limit / drop notifications if they're posted too // quickly. There is no indication to the user that this happened. // See https://github.com/tuskyapp/Tusky/pull/3626#discussion_r1192963664 - Thread.sleep(1000) + delay(1000.milliseconds) } NotificationHelper.updateSummaryNotifications( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java index 5e7f2ece..6a42e897 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java @@ -37,6 +37,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import androidx.core.app.NotificationCompat; import androidx.core.app.RemoteInput; import androidx.core.app.TaskStackBuilder; @@ -77,7 +78,12 @@ import java.util.concurrent.TimeUnit; public class NotificationHelper { - private static int notificationId = 0; + /** ID of notification shown when fetching notifications */ + public static final int NOTIFICATION_ID_FETCH_NOTIFICATION = 0; + /** ID of notification shown when pruning the cache */ + public static final int NOTIFICATION_ID_PRUNE_CACHE = 1; + /** Dynamic notification IDs start here */ + private static int notificationId = NOTIFICATION_ID_PRUNE_CACHE + 1; /** * constants used in Intents @@ -121,6 +127,7 @@ public class NotificationHelper { public static final String CHANNEL_SIGN_UP = "CHANNEL_SIGN_UP"; public static final String CHANNEL_UPDATES = "CHANNEL_UPDATES"; public static final String CHANNEL_REPORT = "CHANNEL_REPORT"; + public static final String CHANNEL_BACKGROUND_TASKS = "CHANNEL_BACKGROUND_TASKS"; /** * WorkManager Tag @@ -472,6 +479,49 @@ public class NotificationHelper { pendingIntentFlags(false)); } + /** + * Creates a notification channel for notifications for background work that should not + * disturb the user. + * + * @param context context + */ + public static void createWorkerNotificationChannel(@NonNull Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + NotificationChannel channel = new NotificationChannel( + CHANNEL_BACKGROUND_TASKS, + context.getString(R.string.notification_listenable_worker_name), + NotificationManager.IMPORTANCE_NONE + ); + + channel.setDescription(context.getString(R.string.notification_listenable_worker_description)); + channel.enableLights(false); + channel.enableVibration(false); + channel.setShowBadge(false); + + notificationManager.createNotificationChannel(channel); + } + + /** + * Creates a notification for a background worker. + * + * @param context context + * @param titleResource String resource to use as the notification's title + * @return the notification + */ + @NonNull + public static android.app.Notification createWorkerNotification(@NonNull Context context, @StringRes int titleResource) { + String title = context.getString(titleResource); + return new NotificationCompat.Builder(context, CHANNEL_BACKGROUND_TASKS) + .setContentTitle(title) + .setTicker(title) + .setSmallIcon(R.drawable.ic_notify) + .setOngoing(true) + .build(); + } + public static void createNotificationChannelsForAccount(@NonNull AccountEntity account, @NonNull Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt b/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt index 84fabd4a..cc99b78b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt +++ b/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt @@ -17,10 +17,15 @@ package com.keylesspalace.tusky.worker +import android.app.Notification import android.content.Context -import androidx.work.Worker +import androidx.work.CoroutineWorker +import androidx.work.ForegroundInfo import androidx.work.WorkerParameters +import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.notifications.NotificationFetcher +import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.components.notifications.NotificationHelper.NOTIFICATION_ID_FETCH_NOTIFICATION import javax.inject.Inject /** Fetch and show new notifications. */ @@ -28,16 +33,20 @@ class NotificationWorker( appContext: Context, params: WorkerParameters, private val notificationsFetcher: NotificationFetcher -) : Worker(appContext, params) { - override fun doWork(): Result { +) : CoroutineWorker(appContext, params) { + val notification: Notification = NotificationHelper.createWorkerNotification(applicationContext, R.string.notification_notification_worker) + + override suspend fun doWork(): Result { notificationsFetcher.fetchAndShow() return Result.success() } + override suspend fun getForegroundInfo() = ForegroundInfo(NOTIFICATION_ID_FETCH_NOTIFICATION, notification) + class Factory @Inject constructor( private val notificationsFetcher: NotificationFetcher ) : ChildWorkerFactory { - override fun createWorker(appContext: Context, params: WorkerParameters): Worker { + override fun createWorker(appContext: Context, params: WorkerParameters): CoroutineWorker { return NotificationWorker(appContext, params, notificationsFetcher) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt b/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt index c0ebdb79..5a65a2ef 100644 --- a/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt +++ b/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt @@ -17,11 +17,16 @@ package com.keylesspalace.tusky.worker +import android.app.Notification import android.content.Context import android.util.Log import androidx.work.CoroutineWorker +import androidx.work.ForegroundInfo import androidx.work.ListenableWorker import androidx.work.WorkerParameters +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.components.notifications.NotificationHelper.NOTIFICATION_ID_PRUNE_CACHE import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import javax.inject.Inject @@ -33,6 +38,8 @@ class PruneCacheWorker( private val appDatabase: AppDatabase, private val accountManager: AccountManager ) : CoroutineWorker(appContext, workerParams) { + val notification: Notification = NotificationHelper.createWorkerNotification(applicationContext, R.string.notification_prune_cache) + override suspend fun doWork(): Result { for (account in accountManager.accounts) { Log.d(TAG, "Pruning database using account ID: ${account.id}") @@ -41,6 +48,8 @@ class PruneCacheWorker( return Result.success() } + override suspend fun getForegroundInfo() = ForegroundInfo(NOTIFICATION_ID_PRUNE_CACHE, notification) + companion object { private const val TAG = "PruneCacheWorker" private const val MAX_STATUSES_IN_CACHE = 1000 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 26af20b9..9fbb6fda 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -371,6 +371,8 @@ Notifications when posts you\'ve interacted with are edited Reports Notifications about moderation reports + Background activity + Notifications when Tusky is working in the background Unknown %s mentioned you @@ -381,6 +383,8 @@ %d new interaction %d new interactions + Fetching notifications… + Cache maintenance… Locked Account