Browse Source

feat(android): add WorkManager deduplication for notification workers

Implement unique work names to prevent duplicate WorkManager tasks
from being enqueued when multiple notifications are scheduled for
the same time or when the receiver is triggered multiple times.

Changes:
- DailyNotificationReceiver: Use enqueueUniqueWork with unique names
  ("display_{id}", "dismiss_{id}") and ExistingWorkPolicy.KEEP/REPLACE
- DailyNotificationFetcher: Use unique work names based on scheduled
  time rounded to minutes ("fetch_{minutes}") with ExistingWorkPolicy.REPLACE

This resolves the issue where ~25+ concurrent workers were being
enqueued for the same notification, leading to race conditions and
resource waste. Now only one worker processes each notification/fetch
at a time.

Verified in logcat: Worker count reduced from 25+ to 1 per notification.
master
Matthew Raymer 1 day ago
parent
commit
83a0c1530d
  1. 17
      android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationFetcher.java
  2. 42
      android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java

17
android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationFetcher.java

@ -14,6 +14,7 @@ import android.content.Context;
import android.util.Log;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
@ -95,6 +96,12 @@ public class DailyNotificationFetcher {
.putInt("retry_count", 0)
.build();
// Create unique work name based on scheduled time to prevent duplicate fetches
// Use scheduled time rounded to nearest minute to handle multiple notifications
// scheduled close together
long scheduledTimeMinutes = scheduledTime / (60 * 1000);
String workName = "fetch_" + scheduledTimeMinutes;
// Create one-time work request
OneTimeWorkRequest fetchWork = new OneTimeWorkRequest.Builder(
DailyNotificationFetchWorker.class)
@ -103,11 +110,17 @@ public class DailyNotificationFetcher {
.setInitialDelay(delayMs, TimeUnit.MILLISECONDS)
.build();
// Enqueue the work
workManager.enqueue(fetchWork);
// Use unique work name with REPLACE policy (newer fetch replaces older)
// This prevents duplicate fetch workers for the same scheduled time
workManager.enqueueUniqueWork(
workName,
ExistingWorkPolicy.REPLACE,
fetchWork
);
Log.i(TAG, "DN|WORK_ENQUEUED work_id=" + fetchWork.getId().toString() +
" fetch_at=" + fetchTime +
" work_name=" + workName +
" delay_ms=" + delayMs +
" delay_minutes=" + (delayMs / 60000.0));
Log.i(TAG, "Background fetch scheduled successfully");

42
android/plugin/src/main/java/com/timesafari/dailynotification/DailyNotificationReceiver.java

@ -21,6 +21,7 @@ import android.util.Log;
import androidx.core.app.NotificationCompat;
import androidx.work.Data;
import androidx.work.ExistingWorkPolicy;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
@ -89,13 +90,22 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
}
/**
* Enqueue notification processing work to WorkManager
* Enqueue notification processing work to WorkManager with deduplication
*
* Uses unique work name based on notification ID to prevent duplicate
* work items from being enqueued for the same notification. WorkManager's
* enqueueUniqueWork automatically prevents duplicates when using the same
* work name.
*
* @param context Application context
* @param notificationId ID of notification to process
*/
private void enqueueNotificationWork(Context context, String notificationId) {
try {
// Create unique work name based on notification ID to prevent duplicates
// WorkManager will automatically skip if work with this name already exists
String workName = "display_" + notificationId;
Data inputData = new Data.Builder()
.putString("notification_id", notificationId)
.putString("action", "display")
@ -106,8 +116,16 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
.addTag("daily_notification_display")
.build();
WorkManager.getInstance(context).enqueue(workRequest);
Log.d(TAG, "DN|WORK_ENQUEUE display=" + notificationId);
// Use unique work name with KEEP policy (don't replace if exists)
// This prevents duplicate work items from being enqueued even if
// the receiver is triggered multiple times for the same notification
WorkManager.getInstance(context).enqueueUniqueWork(
workName,
ExistingWorkPolicy.KEEP,
workRequest
);
Log.d(TAG, "DN|WORK_ENQUEUE display=" + notificationId + " work_name=" + workName);
} catch (Exception e) {
Log.e(TAG, "DN|WORK_ENQUEUE_ERR display=" + notificationId + " err=" + e.getMessage(), e);
@ -115,13 +133,19 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
}
/**
* Enqueue notification dismissal work to WorkManager
* Enqueue notification dismissal work to WorkManager with deduplication
*
* Uses unique work name based on notification ID to prevent duplicate
* dismissal work items.
*
* @param context Application context
* @param notificationId ID of notification to dismiss
*/
private void enqueueDismissalWork(Context context, String notificationId) {
try {
// Create unique work name based on notification ID to prevent duplicates
String workName = "dismiss_" + notificationId;
Data inputData = new Data.Builder()
.putString("notification_id", notificationId)
.putString("action", "dismiss")
@ -132,8 +156,14 @@ public class DailyNotificationReceiver extends BroadcastReceiver {
.addTag("daily_notification_dismiss")
.build();
WorkManager.getInstance(context).enqueue(workRequest);
Log.d(TAG, "DN|WORK_ENQUEUE dismiss=" + notificationId);
// Use unique work name with REPLACE policy (allow new dismissal to replace pending)
WorkManager.getInstance(context).enqueueUniqueWork(
workName,
ExistingWorkPolicy.REPLACE,
workRequest
);
Log.d(TAG, "DN|WORK_ENQUEUE dismiss=" + notificationId + " work_name=" + workName);
} catch (Exception e) {
Log.e(TAG, "DN|WORK_ENQUEUE_ERR dismiss=" + notificationId + " err=" + e.getMessage(), e);

Loading…
Cancel
Save