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
This commit is contained in:
parent
7fe4c9f317
commit
1f7a5f626d
7 changed files with 158 additions and 84 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -371,6 +371,8 @@
|
|||
<string name="notification_update_description">Notifications when posts you\'ve interacted with are edited</string>
|
||||
<string name="notification_report_name">Reports</string>
|
||||
<string name="notification_report_description">Notifications about moderation reports</string>
|
||||
<string name="notification_listenable_worker_name">Background activity</string>
|
||||
<string name="notification_listenable_worker_description">Notifications when Tusky is working in the background</string>
|
||||
<string name="notification_unknown_name">Unknown</string>
|
||||
|
||||
<string name="notification_mention_format">%s mentioned you</string>
|
||||
|
|
@ -381,6 +383,8 @@
|
|||
<item quantity="one">%d new interaction</item>
|
||||
<item quantity="other">%d new interactions</item>
|
||||
</plurals>
|
||||
<string name="notification_notification_worker">Fetching notifications…</string>
|
||||
<string name="notification_prune_cache">Cache maintenance…</string>
|
||||
|
||||
<string name="description_account_locked">Locked Account</string>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue