react-native-notify-kit 9.3.0 → 9.5.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.
- package/README.md +68 -2
- package/android/build.gradle +5 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/androidTest/java/app/notifee/core/DoScheduledWorkOrderingTest.java +234 -0
- package/android/src/androidTest/java/app/notifee/core/RebootRecoveryTest.java +204 -0
- package/android/src/androidTest/java/app/notifee/core/database/TimingWorkDataRepository.java +56 -0
- package/android/src/androidTest/java/app/notifee/core/database/WorkDataRepositoryRaceTest.java +221 -0
- package/android/src/androidTest/res/drawable/test_icon.xml +14 -0
- package/android/src/main/baseline-prof.txt +382 -0
- package/android/src/main/java/app/notifee/core/ForegroundService.java +269 -80
- package/android/src/main/java/app/notifee/core/InitProvider.java +48 -5
- package/android/src/main/java/app/notifee/core/Logger.java +5 -0
- package/android/src/main/java/app/notifee/core/NotifeeAlarmManager.java +160 -79
- package/android/src/main/java/app/notifee/core/NotificationManager.java +370 -288
- package/android/src/main/java/app/notifee/core/WarmupHelper.java +84 -0
- package/android/src/main/java/app/notifee/core/database/WorkDataRepository.java +38 -34
- package/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java +17 -1
- package/android/src/main/kotlin/io/invertase/notifee/NotifeeApiModule.kt +30 -0
- package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +182 -0
- package/android/src/test/java/app/notifee/core/database/WorkDataRepositoryFutureContractTest.java +279 -0
- package/dist/NotifeeApiModule.d.ts +1 -0
- package/dist/NotifeeApiModule.js +6 -0
- package/dist/NotifeeApiModule.js.map +1 -1
- package/dist/specs/NativeNotifeeModule.d.ts +1 -0
- package/dist/specs/NativeNotifeeModule.js.map +1 -1
- package/dist/types/Module.d.ts +19 -0
- package/dist/types/NotificationAndroid.d.ts +58 -0
- package/dist/types/NotificationAndroid.js +43 -0
- package/dist/types/NotificationAndroid.js.map +1 -1
- package/dist/utils/validate.d.ts +1 -0
- package/dist/utils/validate.js +10 -4
- package/dist/utils/validate.js.map +1 -1
- package/dist/validators/validateAndroidChannel.js +3 -3
- package/dist/validators/validateAndroidChannel.js.map +1 -1
- package/dist/validators/validateAndroidNotification.js +36 -12
- package/dist/validators/validateAndroidNotification.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/ios/NotifeeCore/NotifeeCore+NSNotificationCenter.m +5 -1
- package/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m +5 -5
- package/ios/NotifeeCore/NotifeeCore.m +28 -9
- package/ios/NotifeeCore/NotifeeCoreDelegateHolder.m +5 -1
- package/ios/NotifeeCore/NotifeeCoreExtensionHelper.m +7 -2
- package/ios/RNNotifee/NotifeeApiModule.mm +31 -10
- package/package.json +1 -1
- package/src/NotifeeApiModule.ts +7 -0
- package/src/specs/NativeNotifeeModule.ts +1 -0
- package/src/types/Module.ts +20 -0
- package/src/types/NotificationAndroid.ts +62 -0
- package/src/utils/validate.ts +12 -4
- package/src/validators/validateAndroidChannel.ts +3 -3
- package/src/validators/validateAndroidNotification.ts +43 -10
- package/src/version.ts +1 -1
|
@@ -26,6 +26,7 @@ import android.content.Context;
|
|
|
26
26
|
import android.content.Intent;
|
|
27
27
|
import android.os.Build;
|
|
28
28
|
import android.os.Bundle;
|
|
29
|
+
import androidx.annotation.NonNull;
|
|
29
30
|
import androidx.annotation.Nullable;
|
|
30
31
|
import androidx.core.app.AlarmManagerCompat;
|
|
31
32
|
import app.notifee.core.database.WorkDataEntity;
|
|
@@ -40,10 +41,14 @@ import com.google.common.util.concurrent.Futures;
|
|
|
40
41
|
import com.google.common.util.concurrent.ListenableFuture;
|
|
41
42
|
import com.google.common.util.concurrent.ListeningExecutorService;
|
|
42
43
|
import com.google.common.util.concurrent.MoreExecutors;
|
|
44
|
+
import java.util.ArrayList;
|
|
43
45
|
import java.util.Arrays;
|
|
44
46
|
import java.util.List;
|
|
45
47
|
import java.util.concurrent.ExecutorService;
|
|
46
48
|
import java.util.concurrent.Executors;
|
|
49
|
+
import java.util.concurrent.ScheduledExecutorService;
|
|
50
|
+
import java.util.concurrent.TimeUnit;
|
|
51
|
+
import java.util.concurrent.TimeoutException;
|
|
47
52
|
|
|
48
53
|
class NotifeeAlarmManager {
|
|
49
54
|
private static final String TAG = "NotifeeAlarmManager";
|
|
@@ -52,6 +57,59 @@ class NotifeeAlarmManager {
|
|
|
52
57
|
private static final ListeningExecutorService alarmManagerListeningExecutor =
|
|
53
58
|
MoreExecutors.listeningDecorator(alarmManagerExecutor);
|
|
54
59
|
|
|
60
|
+
// Scheduler used only as the timeout clock for Futures.withTimeout on Room
|
|
61
|
+
// writes happening inside BroadcastReceiver.goAsync() scopes. Broadcasts have
|
|
62
|
+
// ~10s before Android kills the process, so we cap the Room wait at 8s and
|
|
63
|
+
// call pendingResult.finish() anyway on timeout to avoid ANRs.
|
|
64
|
+
private static final ScheduledExecutorService TIMEOUT_SCHEDULER =
|
|
65
|
+
Executors.newSingleThreadScheduledExecutor();
|
|
66
|
+
private static final long RECEIVER_WRITE_TIMEOUT_SECONDS = 8;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Wraps {@code future} in a {@link Futures#withTimeout} safety net and arranges for {@code
|
|
70
|
+
* pendingResult.finish()} to be called exactly once — on success, on failure, or on timeout. Use
|
|
71
|
+
* from a {@link BroadcastReceiver#goAsync} scope where the receiver must tell Android "I'm done"
|
|
72
|
+
* within ~10s or the process is killed. Timeouts are logged at {@code WARN} with the supplied
|
|
73
|
+
* {@code logContext}; real failures at {@code ERROR}.
|
|
74
|
+
*/
|
|
75
|
+
private static void finishReceiverWhenDone(
|
|
76
|
+
@NonNull ListenableFuture<?> future,
|
|
77
|
+
@Nullable BroadcastReceiver.PendingResult pendingResult,
|
|
78
|
+
@NonNull String logContext) {
|
|
79
|
+
ListenableFuture<?> bounded =
|
|
80
|
+
Futures.withTimeout(
|
|
81
|
+
future, RECEIVER_WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS, TIMEOUT_SCHEDULER);
|
|
82
|
+
Futures.addCallback(
|
|
83
|
+
bounded,
|
|
84
|
+
new FutureCallback<Object>() {
|
|
85
|
+
@Override
|
|
86
|
+
public void onSuccess(Object result) {
|
|
87
|
+
if (pendingResult != null) {
|
|
88
|
+
pendingResult.finish();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@Override
|
|
93
|
+
public void onFailure(@NonNull Throwable t) {
|
|
94
|
+
if (t instanceof TimeoutException) {
|
|
95
|
+
Logger.w(
|
|
96
|
+
TAG,
|
|
97
|
+
"Room write for "
|
|
98
|
+
+ logContext
|
|
99
|
+
+ " did not complete within "
|
|
100
|
+
+ RECEIVER_WRITE_TIMEOUT_SECONDS
|
|
101
|
+
+ "s; finishing BroadcastReceiver anyway to avoid ANR");
|
|
102
|
+
} else {
|
|
103
|
+
Logger.e(TAG, "Failure in " + logContext, new Exception(t));
|
|
104
|
+
}
|
|
105
|
+
if (pendingResult != null) {
|
|
106
|
+
pendingResult.finish();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
alarmManagerListeningExecutor);
|
|
111
|
+
}
|
|
112
|
+
|
|
55
113
|
static void displayScheduledNotification(
|
|
56
114
|
Bundle alarmManagerNotification, @Nullable BroadcastReceiver.PendingResult pendingResult) {
|
|
57
115
|
if (alarmManagerNotification == null) {
|
|
@@ -71,72 +129,60 @@ class NotifeeAlarmManager {
|
|
|
71
129
|
|
|
72
130
|
WorkDataRepository workDataRepository = new WorkDataRepository(getApplicationContext());
|
|
73
131
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
true));
|
|
117
|
-
} else {
|
|
118
|
-
// not repeating, delete database entry if work is a one-time request
|
|
119
|
-
WorkDataRepository.getInstance(getApplicationContext())
|
|
120
|
-
.deleteById(id);
|
|
121
|
-
}
|
|
122
|
-
return Futures.immediateFuture(null);
|
|
123
|
-
},
|
|
124
|
-
alarmManagerExecutor);
|
|
125
|
-
},
|
|
126
|
-
alarmManagerExecutor)
|
|
127
|
-
.addOnCompleteListener(
|
|
128
|
-
(e, result) -> {
|
|
129
|
-
try {
|
|
130
|
-
if (e != null) {
|
|
131
|
-
Logger.e(TAG, "Failed to display notification", e);
|
|
132
|
-
}
|
|
133
|
-
} finally {
|
|
134
|
-
if (pendingResult != null) {
|
|
135
|
-
pendingResult.finish();
|
|
132
|
+
// Chain: read → display → persist (update for repeat / delete for one-shot).
|
|
133
|
+
// The final Room write is awaited before pendingResult.finish() so the
|
|
134
|
+
// BroadcastReceiver only tells Android "I'm done" once the next-fire anchor
|
|
135
|
+
// (or the deletion) has actually landed in Room. Without this, process death
|
|
136
|
+
// between finish() and the enqueued write could lose the updated timestamp
|
|
137
|
+
// and cause the same alarm to fire again on the next reboot. See #549 audit
|
|
138
|
+
// callers #6 and #7.
|
|
139
|
+
ListenableFuture<Void> displayAndPersistFuture =
|
|
140
|
+
Futures.transformAsync(
|
|
141
|
+
workDataRepository.getWorkDataById(id),
|
|
142
|
+
workDataEntity -> {
|
|
143
|
+
if (workDataEntity == null
|
|
144
|
+
|| workDataEntity.getNotification() == null
|
|
145
|
+
|| workDataEntity.getTrigger() == null) {
|
|
146
|
+
Logger.w(
|
|
147
|
+
TAG, "Attempted to handle doScheduledWork but no notification data was found.");
|
|
148
|
+
return Futures.immediateFuture(null);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
Bundle triggerBundle = ObjectUtils.bytesToBundle(workDataEntity.getTrigger());
|
|
152
|
+
Bundle notificationBundle =
|
|
153
|
+
ObjectUtils.bytesToBundle(workDataEntity.getNotification());
|
|
154
|
+
NotificationModel notificationModel =
|
|
155
|
+
NotificationModel.fromBundle(notificationBundle);
|
|
156
|
+
|
|
157
|
+
return Futures.transformAsync(
|
|
158
|
+
NotificationManager.displayNotification(notificationModel, triggerBundle),
|
|
159
|
+
voidDisplayedNotification -> {
|
|
160
|
+
if (triggerBundle.containsKey("repeatFrequency")
|
|
161
|
+
&& ObjectUtils.getInt(triggerBundle.get("repeatFrequency")) != -1) {
|
|
162
|
+
TimestampTriggerModel trigger =
|
|
163
|
+
TimestampTriggerModel.fromBundle(triggerBundle);
|
|
164
|
+
// scheduleTimestampTriggerNotification() calls setNextTimestamp()
|
|
165
|
+
// internally, so we must NOT call it here to avoid double-advancing
|
|
166
|
+
scheduleTimestampTriggerNotification(notificationModel, trigger);
|
|
167
|
+
return WorkDataRepository.getInstance(getApplicationContext())
|
|
168
|
+
.update(
|
|
169
|
+
new WorkDataEntity(
|
|
170
|
+
id,
|
|
171
|
+
workDataEntity.getNotification(),
|
|
172
|
+
ObjectUtils.bundleToBytes(trigger.toBundle()),
|
|
173
|
+
true));
|
|
136
174
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
175
|
+
// not repeating, delete database entry if work is a one-time request
|
|
176
|
+
return WorkDataRepository.getInstance(getApplicationContext()).deleteById(id);
|
|
177
|
+
},
|
|
178
|
+
alarmManagerExecutor);
|
|
179
|
+
},
|
|
180
|
+
alarmManagerExecutor);
|
|
181
|
+
|
|
182
|
+
// Awaits the Room write before pendingResult.finish(), bounded by the 8s
|
|
183
|
+
// ANR safety timeout. Handles success, failure, and timeout uniformly.
|
|
184
|
+
finishReceiverWhenDone(
|
|
185
|
+
displayAndPersistFuture, pendingResult, "displayScheduledNotification[" + id + "]");
|
|
140
186
|
}
|
|
141
187
|
|
|
142
188
|
public static PendingIntent getAlarmManagerIntentForNotification(String notificationId) {
|
|
@@ -283,10 +329,18 @@ class NotifeeAlarmManager {
|
|
|
283
329
|
alarmManagerListeningExecutor);
|
|
284
330
|
}
|
|
285
331
|
|
|
286
|
-
|
|
287
|
-
|
|
332
|
+
/**
|
|
333
|
+
* On reboot, reschedule one trigger notification created via alarm manager.
|
|
334
|
+
*
|
|
335
|
+
* <p>Returns a future that completes when Room has persisted the updated next-fire anchor. The
|
|
336
|
+
* caller MUST await this future before calling {@code pendingResult.finish()} — otherwise Android
|
|
337
|
+
* may kill the boot receiver's process before Room has drained, and the next reboot will
|
|
338
|
+
* reschedule from the stale anchor. This bug is NOT in upstream invertase/notifee#549 and was
|
|
339
|
+
* surfaced only by the pre-fix-549-audit.md read-only caller audit (Caller #8).
|
|
340
|
+
*/
|
|
341
|
+
ListenableFuture<Void> rescheduleNotification(WorkDataEntity workDataEntity) {
|
|
288
342
|
if (workDataEntity.getNotification() == null || workDataEntity.getTrigger() == null) {
|
|
289
|
-
return;
|
|
343
|
+
return Futures.immediateFuture(null);
|
|
290
344
|
}
|
|
291
345
|
|
|
292
346
|
byte[] notificationBytes = workDataEntity.getNotification();
|
|
@@ -302,22 +356,23 @@ class NotifeeAlarmManager {
|
|
|
302
356
|
case 0:
|
|
303
357
|
TimestampTriggerModel trigger = TimestampTriggerModel.fromBundle(triggerBundle);
|
|
304
358
|
if (!trigger.getWithAlarmManager()) {
|
|
305
|
-
return;
|
|
359
|
+
return Futures.immediateFuture(null);
|
|
306
360
|
}
|
|
307
361
|
|
|
308
362
|
scheduleTimestampTriggerNotification(notificationModel, trigger);
|
|
309
|
-
// Persist updated timestamp so next reboot starts from the correct anchor
|
|
310
|
-
WorkDataRepository.getInstance(getApplicationContext())
|
|
363
|
+
// Persist updated timestamp so next reboot starts from the correct anchor.
|
|
364
|
+
return WorkDataRepository.getInstance(getApplicationContext())
|
|
311
365
|
.update(
|
|
312
366
|
new WorkDataEntity(
|
|
313
367
|
workDataEntity.getId(),
|
|
314
368
|
workDataEntity.getNotification(),
|
|
315
369
|
ObjectUtils.bundleToBytes(trigger.toBundle()),
|
|
316
370
|
workDataEntity.getWithAlarmManager()));
|
|
317
|
-
break;
|
|
318
371
|
case 1:
|
|
319
372
|
// TODO: support interval triggers with alarm manager
|
|
320
|
-
|
|
373
|
+
return Futures.immediateFuture(null);
|
|
374
|
+
default:
|
|
375
|
+
return Futures.immediateFuture(null);
|
|
321
376
|
}
|
|
322
377
|
}
|
|
323
378
|
|
|
@@ -328,19 +383,45 @@ class NotifeeAlarmManager {
|
|
|
328
383
|
new FutureCallback<List<WorkDataEntity>>() {
|
|
329
384
|
@Override
|
|
330
385
|
public void onSuccess(List<WorkDataEntity> workDataEntities) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
386
|
+
Logger.d(
|
|
387
|
+
TAG,
|
|
388
|
+
"Reschedule starting for "
|
|
389
|
+
+ (workDataEntities != null ? workDataEntities.size() : 0)
|
|
390
|
+
+ " recurring alarms");
|
|
391
|
+
|
|
392
|
+
if (workDataEntities == null || workDataEntities.isEmpty()) {
|
|
336
393
|
if (pendingResult != null) {
|
|
337
394
|
pendingResult.finish();
|
|
338
395
|
}
|
|
396
|
+
return;
|
|
339
397
|
}
|
|
398
|
+
|
|
399
|
+
List<ListenableFuture<Void>> updateFutures = new ArrayList<>(workDataEntities.size());
|
|
400
|
+
for (WorkDataEntity workDataEntity : workDataEntities) {
|
|
401
|
+
try {
|
|
402
|
+
updateFutures.add(rescheduleNotification(workDataEntity));
|
|
403
|
+
} catch (Throwable t) {
|
|
404
|
+
// A single bad entity must not prevent the rest of the batch
|
|
405
|
+
// from being rescheduled — log and continue.
|
|
406
|
+
Logger.w(
|
|
407
|
+
TAG,
|
|
408
|
+
"Failed to reschedule entity "
|
|
409
|
+
+ workDataEntity.getId()
|
|
410
|
+
+ ": "
|
|
411
|
+
+ t.getMessage());
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Awaits all per-entity update futures before finishing the boot
|
|
416
|
+
// receiver, bounded by the 8s ANR safety timeout. Any not-yet-
|
|
417
|
+
// persisted next-fire anchors left behind on timeout will catch up
|
|
418
|
+
// on the next alarm fire.
|
|
419
|
+
ListenableFuture<List<Void>> combined = Futures.allAsList(updateFutures);
|
|
420
|
+
finishReceiverWhenDone(combined, pendingResult, "rescheduleNotifications");
|
|
340
421
|
}
|
|
341
422
|
|
|
342
423
|
@Override
|
|
343
|
-
public void onFailure(Throwable t) {
|
|
424
|
+
public void onFailure(@NonNull Throwable t) {
|
|
344
425
|
Logger.e(TAG, "Failed to reschedule notifications", new Exception(t));
|
|
345
426
|
if (pendingResult != null) {
|
|
346
427
|
pendingResult.finish();
|