react-native-notify-kit 9.4.0 → 9.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/README.md +200 -5
  2. package/android/build.gradle +1 -0
  3. package/android/src/androidTest/java/app/notifee/core/DoScheduledWorkOrderingTest.java +234 -0
  4. package/android/src/androidTest/java/app/notifee/core/RebootRecoveryTest.java +569 -0
  5. package/android/src/androidTest/java/app/notifee/core/database/TimingWorkDataRepository.java +56 -0
  6. package/android/src/androidTest/java/app/notifee/core/database/WorkDataRepositoryRaceTest.java +221 -0
  7. package/android/src/androidTest/res/drawable/test_icon.xml +14 -0
  8. package/android/src/main/java/app/notifee/core/AlarmPermissionBroadcastReceiver.java +17 -6
  9. package/android/src/main/java/app/notifee/core/InitProvider.java +109 -2
  10. package/android/src/main/java/app/notifee/core/NotifeeAlarmManager.java +385 -93
  11. package/android/src/main/java/app/notifee/core/NotificationAlarmReceiver.java +16 -5
  12. package/android/src/main/java/app/notifee/core/NotificationManager.java +153 -96
  13. package/android/src/main/java/app/notifee/core/Preferences.java +9 -0
  14. package/android/src/main/java/app/notifee/core/RebootBroadcastReceiver.java +20 -5
  15. package/android/src/main/java/app/notifee/core/database/WorkDataRepository.java +38 -34
  16. package/android/src/main/kotlin/io/invertase/notifee/HeadlessTask.kt +3 -2
  17. package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +251 -4
  18. package/android/src/test/java/app/notifee/core/InitProviderBootCheckTest.java +81 -0
  19. package/android/src/test/java/app/notifee/core/NotifeeAlarmManagerHandleStaleTest.java +242 -0
  20. package/android/src/test/java/app/notifee/core/NotifeeAlarmManagerSetAlarmClockTest.java +264 -0
  21. package/android/src/test/java/app/notifee/core/database/WorkDataRepositoryFutureContractTest.java +279 -0
  22. package/android/src/test/java/app/notifee/core/model/TimestampTriggerModelTest.java +199 -3
  23. package/android/src/test/java/io/invertase/notifee/HeadlessTaskConfigTest.kt +34 -0
  24. package/dist/version.d.ts +1 -1
  25. package/dist/version.js +1 -1
  26. package/package.json +1 -1
  27. package/src/version.ts +1 -1
@@ -543,15 +543,13 @@ class NotificationManager {
543
543
  task -> {
544
544
  if (notificationType == NOTIFICATION_TYPE_TRIGGER
545
545
  || notificationType == NOTIFICATION_TYPE_ALL) {
546
- return new ExtendedListenableFuture<Void>(
547
- NotifeeAlarmManager.cancelAllNotifications())
548
- .addOnCompleteListener(
549
- (e, result) -> {
550
- if (e == null) {
551
- WorkDataRepository.getInstance(getApplicationContext()).deleteAll();
552
- }
553
- },
554
- LISTENING_CACHED_THREAD_POOL);
546
+ // Chain the Room delete onto the alarm-manager cancel so the outer
547
+ // future — and therefore the JS Promise — only completes after Room
548
+ // has drained. Fixes upstream invertase/notifee#549.
549
+ return Futures.transformAsync(
550
+ NotifeeAlarmManager.cancelAllNotifications(),
551
+ ignored -> WorkDataRepository.getInstance(getApplicationContext()).deleteAll(),
552
+ LISTENING_CACHED_THREAD_POOL);
555
553
  }
556
554
  return Futures.immediateFuture(null);
557
555
  },
@@ -613,9 +611,11 @@ class NotificationManager {
613
611
  }))
614
612
  .continueWith(
615
613
  task -> {
616
- // delete all from database
614
+ // Chain the Room delete so the outer future — and therefore the JS
615
+ // Promise — only completes after Room has drained. Fixes upstream
616
+ // invertase/notifee#549 for the per-id cancel path.
617
617
  if (notificationType != NOTIFICATION_TYPE_DISPLAYED) {
618
- WorkDataRepository.getInstance(getApplicationContext()).deleteByIds(ids);
618
+ return WorkDataRepository.getInstance(getApplicationContext()).deleteByIds(ids);
619
619
  }
620
620
  return Futures.immediateFuture(null);
621
621
  },
@@ -691,56 +691,75 @@ class NotificationManager {
691
691
 
692
692
  static ListenableFuture<Void> createTriggerNotification(
693
693
  NotificationModel notificationModel, Bundle triggerBundle) {
694
- return LISTENING_CACHED_THREAD_POOL.submit(
695
- () -> {
696
- int triggerType = ObjectUtils.getInt(triggerBundle.get("type"));
697
- switch (triggerType) {
698
- case 0:
699
- createTimestampTriggerNotification(notificationModel, triggerBundle);
700
- break;
701
- case 1:
702
- createIntervalTriggerNotification(notificationModel, triggerBundle);
703
- break;
704
- }
694
+ int triggerType = ObjectUtils.getInt(triggerBundle.get("type"));
695
+ ListenableFuture<Void> scheduleFuture;
696
+ switch (triggerType) {
697
+ case 0:
698
+ scheduleFuture = createTimestampTriggerNotification(notificationModel, triggerBundle);
699
+ break;
700
+ case 1:
701
+ scheduleFuture = createIntervalTriggerNotification(notificationModel, triggerBundle);
702
+ break;
703
+ default:
704
+ scheduleFuture = Futures.immediateFuture(null);
705
+ break;
706
+ }
705
707
 
708
+ return Futures.transform(
709
+ scheduleFuture,
710
+ unused -> {
706
711
  EventBus.post(
707
712
  new NotificationEvent(
708
713
  NotificationEvent.TYPE_TRIGGER_NOTIFICATION_CREATED, notificationModel));
709
-
710
714
  return null;
711
- });
715
+ },
716
+ LISTENING_CACHED_THREAD_POOL);
712
717
  }
713
718
 
714
- static void createIntervalTriggerNotification(
719
+ // Returns a future that completes only after the Room insert has persisted AND
720
+ // WorkManager has enqueued the periodic work. Chaining Room-first guarantees the
721
+ // worker reads the row on its first fire even if the process is killed between
722
+ // the JS Promise resolving and WorkManager scheduling. Fixes upstream
723
+ // invertase/notifee#549 for the interval-trigger path.
724
+ static ListenableFuture<Void> createIntervalTriggerNotification(
715
725
  NotificationModel notificationModel, Bundle triggerBundle) {
716
726
  IntervalTriggerModel trigger = IntervalTriggerModel.fromBundle(triggerBundle);
717
727
  String uniqueWorkName = "trigger:" + notificationModel.getId();
718
- WorkManager workManager = WorkManager.getInstance(getApplicationContext());
719
-
720
- Data.Builder workDataBuilder =
721
- new Data.Builder()
722
- .putString(Worker.KEY_WORK_TYPE, Worker.WORK_TYPE_NOTIFICATION_TRIGGER)
723
- .putString(Worker.KEY_WORK_REQUEST, Worker.WORK_REQUEST_PERIODIC)
724
- .putString("id", notificationModel.getId());
725
-
726
- WorkDataRepository.getInstance(getApplicationContext())
727
- .insertTriggerNotification(notificationModel, triggerBundle, false);
728
-
729
- long interval = trigger.getInterval();
730
-
731
- PeriodicWorkRequest.Builder workRequestBuilder;
732
- workRequestBuilder =
733
- new PeriodicWorkRequest.Builder(Worker.class, interval, trigger.getTimeUnit())
734
- .setInitialDelay(interval, trigger.getTimeUnit());
735
-
736
- workRequestBuilder.addTag(Worker.WORK_TYPE_NOTIFICATION_TRIGGER);
737
- workRequestBuilder.addTag(uniqueWorkName);
738
- workRequestBuilder.setInputData(workDataBuilder.build());
739
- workManager.enqueueUniquePeriodicWork(
740
- uniqueWorkName, ExistingPeriodicWorkPolicy.UPDATE, workRequestBuilder.build());
728
+ Context context = getApplicationContext();
729
+
730
+ ListenableFuture<Void> insertFuture =
731
+ WorkDataRepository.insertTriggerNotification(
732
+ context, notificationModel, triggerBundle, false);
733
+
734
+ return Futures.transform(
735
+ insertFuture,
736
+ unused -> {
737
+ WorkManager workManager = WorkManager.getInstance(context);
738
+ Data.Builder workDataBuilder =
739
+ new Data.Builder()
740
+ .putString(Worker.KEY_WORK_TYPE, Worker.WORK_TYPE_NOTIFICATION_TRIGGER)
741
+ .putString(Worker.KEY_WORK_REQUEST, Worker.WORK_REQUEST_PERIODIC)
742
+ .putString("id", notificationModel.getId());
743
+
744
+ long interval = trigger.getInterval();
745
+ PeriodicWorkRequest.Builder workRequestBuilder =
746
+ new PeriodicWorkRequest.Builder(Worker.class, interval, trigger.getTimeUnit())
747
+ .setInitialDelay(interval, trigger.getTimeUnit());
748
+ workRequestBuilder.addTag(Worker.WORK_TYPE_NOTIFICATION_TRIGGER);
749
+ workRequestBuilder.addTag(uniqueWorkName);
750
+ workRequestBuilder.setInputData(workDataBuilder.build());
751
+ workManager.enqueueUniquePeriodicWork(
752
+ uniqueWorkName, ExistingPeriodicWorkPolicy.UPDATE, workRequestBuilder.build());
753
+ return null;
754
+ },
755
+ LISTENING_CACHED_THREAD_POOL);
741
756
  }
742
757
 
743
- static void createTimestampTriggerNotification(
758
+ // Returns a future that completes only after the Room insert has persisted AND
759
+ // the AlarmManager / WorkManager schedule call has run. Same rationale as the
760
+ // interval path above. Fixes upstream invertase/notifee#549 for the timestamp
761
+ // trigger path (AlarmManager by default since 9.1.12).
762
+ static ListenableFuture<Void> createTimestampTriggerNotification(
744
763
  NotificationModel notificationModel, Bundle triggerBundle) {
745
764
  TimestampTriggerModel trigger = TimestampTriggerModel.fromBundle(triggerBundle);
746
765
 
@@ -749,52 +768,56 @@ class NotificationManager {
749
768
  long delay = trigger.getDelay();
750
769
  int interval = trigger.getInterval();
751
770
 
752
- // Save in DB
753
771
  Data.Builder workDataBuilder =
754
772
  new Data.Builder()
755
773
  .putString(Worker.KEY_WORK_TYPE, Worker.WORK_TYPE_NOTIFICATION_TRIGGER)
756
774
  .putString("id", notificationModel.getId());
757
775
 
758
776
  Boolean withAlarmManager = trigger.getWithAlarmManager();
777
+ Context context = getApplicationContext();
778
+
779
+ ListenableFuture<Void> insertFuture =
780
+ WorkDataRepository.insertTriggerNotification(
781
+ context, notificationModel, triggerBundle, withAlarmManager);
782
+
783
+ return Futures.transform(
784
+ insertFuture,
785
+ unused -> {
786
+ if (withAlarmManager) {
787
+ NotifeeAlarmManager.scheduleTimestampTriggerNotification(notificationModel, trigger);
788
+ return null;
789
+ }
759
790
 
760
- WorkDataRepository.getInstance(getApplicationContext())
761
- .insertTriggerNotification(notificationModel, triggerBundle, withAlarmManager);
762
-
763
- // Schedule notification with alarm manager
764
- if (withAlarmManager) {
765
- NotifeeAlarmManager.scheduleTimestampTriggerNotification(notificationModel, trigger);
766
- return;
767
- }
768
-
769
- // Continue to schedule trigger notification with WorkManager
770
- WorkManager workManager = WorkManager.getInstance(getApplicationContext());
771
-
772
- // WorkManager - One time trigger
773
- if (interval == -1) {
774
- OneTimeWorkRequest.Builder workRequestBuilder = new OneTimeWorkRequest.Builder(Worker.class);
775
- workRequestBuilder.addTag(Worker.WORK_TYPE_NOTIFICATION_TRIGGER);
776
- workRequestBuilder.addTag(uniqueWorkName);
777
- workDataBuilder.putString(Worker.KEY_WORK_REQUEST, Worker.WORK_REQUEST_ONE_TIME);
778
- workRequestBuilder.setInputData(workDataBuilder.build());
779
- workRequestBuilder.setInitialDelay(delay, TimeUnit.SECONDS);
780
- workManager.enqueueUniqueWork(
781
- uniqueWorkName, ExistingWorkPolicy.REPLACE, workRequestBuilder.build());
782
- } else {
783
- // WorkManager - repeat trigger
784
- PeriodicWorkRequest.Builder workRequestBuilder;
785
-
786
- workRequestBuilder =
787
- new PeriodicWorkRequest.Builder(
788
- Worker.class, trigger.getInterval(), trigger.getTimeUnit());
789
-
790
- workRequestBuilder.addTag(Worker.WORK_TYPE_NOTIFICATION_TRIGGER);
791
- workRequestBuilder.addTag(uniqueWorkName);
792
- workRequestBuilder.setInitialDelay(delay, TimeUnit.SECONDS);
793
- workDataBuilder.putString(Worker.KEY_WORK_REQUEST, Worker.WORK_REQUEST_PERIODIC);
794
- workRequestBuilder.setInputData(workDataBuilder.build());
795
- workManager.enqueueUniquePeriodicWork(
796
- uniqueWorkName, ExistingPeriodicWorkPolicy.UPDATE, workRequestBuilder.build());
797
- }
791
+ WorkManager workManager = WorkManager.getInstance(context);
792
+
793
+ // WorkManager - One time trigger
794
+ if (interval == -1) {
795
+ OneTimeWorkRequest.Builder workRequestBuilder =
796
+ new OneTimeWorkRequest.Builder(Worker.class);
797
+ workRequestBuilder.addTag(Worker.WORK_TYPE_NOTIFICATION_TRIGGER);
798
+ workRequestBuilder.addTag(uniqueWorkName);
799
+ workDataBuilder.putString(Worker.KEY_WORK_REQUEST, Worker.WORK_REQUEST_ONE_TIME);
800
+ workRequestBuilder.setInputData(workDataBuilder.build());
801
+ workRequestBuilder.setInitialDelay(delay, TimeUnit.SECONDS);
802
+ workManager.enqueueUniqueWork(
803
+ uniqueWorkName, ExistingWorkPolicy.REPLACE, workRequestBuilder.build());
804
+ } else {
805
+ // WorkManager - repeat trigger
806
+ PeriodicWorkRequest.Builder workRequestBuilder =
807
+ new PeriodicWorkRequest.Builder(
808
+ Worker.class, trigger.getInterval(), trigger.getTimeUnit());
809
+
810
+ workRequestBuilder.addTag(Worker.WORK_TYPE_NOTIFICATION_TRIGGER);
811
+ workRequestBuilder.addTag(uniqueWorkName);
812
+ workRequestBuilder.setInitialDelay(delay, TimeUnit.SECONDS);
813
+ workDataBuilder.putString(Worker.KEY_WORK_REQUEST, Worker.WORK_REQUEST_PERIODIC);
814
+ workRequestBuilder.setInputData(workDataBuilder.build());
815
+ workManager.enqueueUniquePeriodicWork(
816
+ uniqueWorkName, ExistingPeriodicWorkPolicy.UPDATE, workRequestBuilder.build());
817
+ }
818
+ return null;
819
+ },
820
+ LISTENING_CACHED_THREAD_POOL);
798
821
  }
799
822
 
800
823
  static ListenableFuture<List<Bundle>> getDisplayedNotifications() {
@@ -982,17 +1005,51 @@ class NotificationManager {
982
1005
  new ExtendedListenableFuture<>(result)
983
1006
  .addOnCompleteListener(
984
1007
  (e2, _unused) -> {
985
- completer.set(Result.success());
986
1008
  if (e2 != null) {
987
1009
  Logger.e(TAG, "Failed to display notification", e2);
1010
+ completer.set(Result.success());
1011
+ return;
1012
+ }
1013
+ String workerRequestType = data.getString(Worker.KEY_WORK_REQUEST);
1014
+ if (workerRequestType != null
1015
+ && workerRequestType.equals(Worker.WORK_REQUEST_ONE_TIME)) {
1016
+ // DO NOT reorder — completer.set must only run after the
1017
+ // delete future completes, otherwise WorkManager may start
1018
+ // a new work instance that reads the stale row. Previously
1019
+ // completer.set fired before the delete was enqueued, leaving
1020
+ // a zombie row that reboot recovery would resurrect as a
1021
+ // ghost alarm. See #549 audit Part B, Caller #5.
1022
+ //
1023
+ // Note: CallbackToFutureAdapter.Completer.set() returns
1024
+ // false on double-set, it does NOT throw. This is an
1025
+ // androidx contract. Do not refactor under the assumption
1026
+ // that double-set is dangerous.
1027
+ Futures.addCallback(
1028
+ WorkDataRepository.getInstance(getApplicationContext())
1029
+ .deleteById(id),
1030
+ new FutureCallback<Void>() {
1031
+ @Override
1032
+ public void onSuccess(Void unused) {
1033
+ completer.set(Result.success());
1034
+ }
1035
+
1036
+ @Override
1037
+ public void onFailure(@NonNull Throwable t) {
1038
+ // Notification was already displayed; a failed
1039
+ // delete leaves an orphan row that the next
1040
+ // cancelAll or app restart will clean up. Still
1041
+ // report success so WorkManager doesn't retry
1042
+ // the already-displayed notification.
1043
+ Logger.e(
1044
+ TAG,
1045
+ "Failed to delete one-time trigger row after" + " display",
1046
+ new Exception(t));
1047
+ completer.set(Result.success());
1048
+ }
1049
+ },
1050
+ LISTENING_CACHED_THREAD_POOL);
988
1051
  } else {
989
- String workerRequestType = data.getString(Worker.KEY_WORK_REQUEST);
990
- if (workerRequestType != null
991
- && workerRequestType.equals(Worker.WORK_REQUEST_ONE_TIME)) {
992
- // delete database entry if work is a one-time request
993
- WorkDataRepository.getInstance(getApplicationContext())
994
- .deleteById(id);
995
- }
1052
+ completer.set(Result.success());
996
1053
  }
997
1054
  },
998
1055
  LISTENING_CACHED_THREAD_POOL);
@@ -22,6 +22,15 @@ import android.content.SharedPreferences;
22
22
 
23
23
  class Preferences {
24
24
  private static final String PREFERENCES_FILE = "app.notifee.core";
25
+
26
+ /**
27
+ * SharedPreferences key storing the value of {@code Settings.Global.BOOT_COUNT} observed on the
28
+ * most recent successful app init. Compared against the current {@code BOOT_COUNT} to detect a
29
+ * reboot since the last app run — the core mechanism of the OEM cold-start recovery added for
30
+ * upstream invertase/notifee#734.
31
+ */
32
+ static final String LAST_KNOWN_BOOT_COUNT_KEY = "notifee_last_known_boot_count";
33
+
25
34
  private static Preferences sharedInstance = new Preferences();
26
35
  private SharedPreferences preferences;
27
36
 
@@ -20,20 +20,35 @@ package app.notifee.core;
20
20
  import android.content.BroadcastReceiver;
21
21
  import android.content.Context;
22
22
  import android.content.Intent;
23
- import android.util.Log;
24
23
 
25
24
  /*
26
25
  * This is invoked when the phone restarts to ensure that all notifications created by the alarm manager
27
26
  * are rescheduled correctly, as Android removes all scheduled alarms when the phone shuts down.
28
27
  */
29
28
  public class RebootBroadcastReceiver extends BroadcastReceiver {
29
+ private static final String TAG = "RebootReceiver";
30
+
30
31
  @Override
31
32
  public void onReceive(Context context, Intent intent) {
32
33
  PendingResult pendingResult = goAsync();
33
- Log.i("RebootReceiver", "Received reboot event");
34
- if (ContextHolder.getApplicationContext() == null) {
35
- ContextHolder.setApplicationContext(context.getApplicationContext());
34
+ // Tracks whether the synchronous section successfully handed off to the
35
+ // async reschedule path. If not, the finally block must call finish() to
36
+ // avoid leaving the broadcast unterminated — Android will otherwise kill
37
+ // the process after ~10s and race subsequent reboot broadcasts.
38
+ boolean asyncHandoffSucceeded = false;
39
+ try {
40
+ Logger.i(TAG, "Received reboot event");
41
+ if (ContextHolder.getApplicationContext() == null) {
42
+ ContextHolder.setApplicationContext(context.getApplicationContext());
43
+ }
44
+ new NotifeeAlarmManager().rescheduleNotifications(pendingResult);
45
+ asyncHandoffSucceeded = true;
46
+ } catch (Throwable t) {
47
+ Logger.e(TAG, "Failed to reschedule notifications after reboot", t);
48
+ } finally {
49
+ if (!asyncHandoffSucceeded && pendingResult != null) {
50
+ pendingResult.finish();
51
+ }
36
52
  }
37
- new NotifeeAlarmManager().rescheduleNotifications(pendingResult);
38
53
  }
39
54
  }
@@ -20,13 +20,16 @@ package app.notifee.core.database;
20
20
  import android.content.Context;
21
21
  import android.os.Bundle;
22
22
  import androidx.annotation.NonNull;
23
+ import androidx.annotation.VisibleForTesting;
23
24
  import app.notifee.core.model.NotificationModel;
24
25
  import app.notifee.core.utility.ObjectUtils;
25
26
  import com.google.common.util.concurrent.ListenableFuture;
27
+ import com.google.common.util.concurrent.ListeningExecutorService;
26
28
  import java.util.List;
27
29
 
28
30
  public class WorkDataRepository {
29
- private WorkDataDao mWorkDataDao;
31
+ private final WorkDataDao mWorkDataDao;
32
+ private final ListeningExecutorService mExecutor;
30
33
  private static WorkDataRepository mInstance;
31
34
 
32
35
  public static @NonNull WorkDataRepository getInstance(@NonNull Context context) {
@@ -42,52 +45,56 @@ public class WorkDataRepository {
42
45
  public WorkDataRepository(Context context) {
43
46
  NotifeeCoreDatabase db = NotifeeCoreDatabase.getDatabase(context);
44
47
  mWorkDataDao = db.workDao();
48
+ mExecutor = NotifeeCoreDatabase.databaseWriteListeningExecutor;
45
49
  }
46
50
 
47
- public void insert(WorkDataEntity workData) {
48
- NotifeeCoreDatabase.databaseWriteListeningExecutor.execute(
49
- () -> {
50
- mWorkDataDao.insert(workData);
51
- });
51
+ @VisibleForTesting
52
+ WorkDataRepository(@NonNull WorkDataDao dao, @NonNull ListeningExecutorService executor) {
53
+ mWorkDataDao = dao;
54
+ mExecutor = executor;
55
+ }
56
+
57
+ // Submit a DAO write to the listening executor and return a future that
58
+ // completes when the DAO call returns (success or exception). Using the
59
+ // submit(Runnable, result) overload avoids the Callable<Object> inference
60
+ // that would otherwise require a cast at every call site.
61
+ private @NonNull ListenableFuture<Void> submitWrite(@NonNull Runnable work) {
62
+ return mExecutor.submit(work, (Void) null);
63
+ }
64
+
65
+ public @NonNull ListenableFuture<Void> insert(WorkDataEntity workData) {
66
+ return submitWrite(() -> mWorkDataDao.insert(workData));
52
67
  }
53
68
 
54
69
  public ListenableFuture<WorkDataEntity> getWorkDataById(String id) {
55
- return NotifeeCoreDatabase.databaseWriteListeningExecutor.submit(
56
- () -> mWorkDataDao.getWorkDataById(id));
70
+ return mExecutor.submit(() -> mWorkDataDao.getWorkDataById(id));
57
71
  }
58
72
 
59
73
  public ListenableFuture<List<WorkDataEntity>> getAllWithAlarmManager(Boolean withAlarmManager) {
60
- return NotifeeCoreDatabase.databaseWriteListeningExecutor.submit(
61
- () -> mWorkDataDao.getAllWithAlarmManager(withAlarmManager));
74
+ return mExecutor.submit(() -> mWorkDataDao.getAllWithAlarmManager(withAlarmManager));
62
75
  }
63
76
 
64
77
  public ListenableFuture<List<WorkDataEntity>> getAll() {
65
- return NotifeeCoreDatabase.databaseWriteListeningExecutor.submit(() -> mWorkDataDao.getAll());
78
+ return mExecutor.submit(() -> mWorkDataDao.getAll());
66
79
  }
67
80
 
68
- public void deleteById(String id) {
69
- NotifeeCoreDatabase.databaseWriteListeningExecutor.execute(
70
- () -> {
71
- mWorkDataDao.deleteById(id);
72
- });
81
+ public @NonNull ListenableFuture<Void> deleteById(String id) {
82
+ return submitWrite(() -> mWorkDataDao.deleteById(id));
73
83
  }
74
84
 
75
- public void deleteByIds(List<String> ids) {
76
- NotifeeCoreDatabase.databaseWriteListeningExecutor.execute(
77
- () -> {
78
- mWorkDataDao.deleteByIds(ids);
79
- });
85
+ public @NonNull ListenableFuture<Void> deleteByIds(List<String> ids) {
86
+ return submitWrite(() -> mWorkDataDao.deleteByIds(ids));
80
87
  }
81
88
 
82
- public void deleteAll() {
83
- NotifeeCoreDatabase.databaseWriteListeningExecutor.execute(
84
- () -> {
85
- mWorkDataDao.deleteAll();
86
- });
89
+ public @NonNull ListenableFuture<Void> deleteAll() {
90
+ return submitWrite(() -> mWorkDataDao.deleteAll());
87
91
  }
88
92
 
89
- public static void insertTriggerNotification(
90
- NotificationModel notificationModel, Bundle triggerBundle, Boolean withAlarmManager) {
93
+ public static @NonNull ListenableFuture<Void> insertTriggerNotification(
94
+ @NonNull Context context,
95
+ NotificationModel notificationModel,
96
+ Bundle triggerBundle,
97
+ Boolean withAlarmManager) {
91
98
  WorkDataEntity workData =
92
99
  new WorkDataEntity(
93
100
  notificationModel.getId(),
@@ -95,13 +102,10 @@ public class WorkDataRepository {
95
102
  ObjectUtils.bundleToBytes(triggerBundle),
96
103
  withAlarmManager);
97
104
 
98
- mInstance.insert(workData);
105
+ return getInstance(context).insert(workData);
99
106
  }
100
107
 
101
- public void update(WorkDataEntity workData) {
102
- NotifeeCoreDatabase.databaseWriteListeningExecutor.execute(
103
- () -> {
104
- mWorkDataDao.update(workData);
105
- });
108
+ public @NonNull ListenableFuture<Void> update(WorkDataEntity workData) {
109
+ return submitWrite(() -> mWorkDataDao.update(workData));
106
110
  }
107
111
  }
@@ -204,8 +204,9 @@ class HeadlessTask {
204
204
  private val params: WritableMap
205
205
 
206
206
  init {
207
- params.putInt("taskId", taskId)
208
- this.params = params
207
+ val copied = params.copy()
208
+ copied.putInt("taskId", taskId)
209
+ this.params = copied
209
210
  }
210
211
 
211
212
  val taskConfig: HeadlessJsTaskConfig