react-native-notify-kit 10.7.1 → 10.7.2
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.
|
@@ -63,6 +63,8 @@ public class ForegroundService extends Service {
|
|
|
63
63
|
*/
|
|
64
64
|
private volatile boolean mStartForegroundCalled = false;
|
|
65
65
|
|
|
66
|
+
// While active, mCurrent* tracks the latest state submitted by a platform call that returned
|
|
67
|
+
// normally. Android does not acknowledge that the notification was displayed.
|
|
66
68
|
public static String mCurrentNotificationId = null;
|
|
67
69
|
|
|
68
70
|
public static int mCurrentForegroundServiceType = -1;
|
|
@@ -72,12 +74,11 @@ public class ForegroundService extends Service {
|
|
|
72
74
|
private static int mCurrentHashCode = 0;
|
|
73
75
|
|
|
74
76
|
/**
|
|
75
|
-
*
|
|
76
|
-
* foreground service. On Android 14+, users can dismiss ongoing foreground service
|
|
77
|
-
* for most service types
|
|
78
|
-
* running service.
|
|
77
|
+
* Submits the latest foreground service notification again if the given notification ID matches
|
|
78
|
+
* the active foreground service. On Android 14+, users can dismiss ongoing foreground service
|
|
79
|
+
* notifications for most service types.
|
|
79
80
|
*
|
|
80
|
-
* @return true if
|
|
81
|
+
* @return true if a re-post was submitted without a synchronous exception, false otherwise
|
|
81
82
|
*/
|
|
82
83
|
@SuppressLint("MissingPermission")
|
|
83
84
|
static boolean repostIfActive(String notificationId) {
|
|
@@ -87,7 +88,7 @@ public class ForegroundService extends Service {
|
|
|
87
88
|
|| mCurrentNotification == null) {
|
|
88
89
|
return false;
|
|
89
90
|
}
|
|
90
|
-
Logger.w(TAG, "
|
|
91
|
+
Logger.w(TAG, "Submitting dismissed foreground service notification for re-post");
|
|
91
92
|
NotificationManagerCompat.from(ContextHolder.getApplicationContext())
|
|
92
93
|
.notify(mCurrentHashCode, mCurrentNotification);
|
|
93
94
|
return true;
|
|
@@ -193,17 +194,13 @@ public class ForegroundService extends Service {
|
|
|
193
194
|
|
|
194
195
|
if (notification != null && bundle != null) {
|
|
195
196
|
NotificationModel notificationModel = NotificationModel.fromBundle(bundle);
|
|
197
|
+
String notificationId = notificationModel.getId();
|
|
196
198
|
|
|
197
199
|
Object pendingEvent = null;
|
|
198
200
|
boolean noneEarlyReturn = false;
|
|
199
201
|
|
|
200
202
|
synchronized (sLock) {
|
|
201
203
|
if (mCurrentNotificationId == null) {
|
|
202
|
-
mCurrentNotificationId = notificationModel.getId();
|
|
203
|
-
mCurrentNotificationBundle = bundle;
|
|
204
|
-
mCurrentNotification = notification;
|
|
205
|
-
mCurrentHashCode = hashCode;
|
|
206
|
-
|
|
207
204
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
208
205
|
int foregroundServiceType =
|
|
209
206
|
notificationModel.getAndroid().getForegroundServiceType();
|
|
@@ -216,12 +213,6 @@ public class ForegroundService extends Service {
|
|
|
216
213
|
TAG,
|
|
217
214
|
"Resolved foreground service type is NONE on API 34+; aborting"
|
|
218
215
|
+ " startForeground to avoid InvalidForegroundServiceTypeException.");
|
|
219
|
-
// Reset stale state while still holding the lock.
|
|
220
|
-
mCurrentNotificationId = null;
|
|
221
|
-
mCurrentForegroundServiceType = -1;
|
|
222
|
-
mCurrentNotificationBundle = null;
|
|
223
|
-
mCurrentNotification = null;
|
|
224
|
-
mCurrentHashCode = 0;
|
|
225
216
|
// Set flag to call the helper AFTER releasing sLock — it performs
|
|
226
217
|
// Binder IPC (PackageManager + startForeground) and must not hold sLock.
|
|
227
218
|
noneEarlyReturn = true;
|
|
@@ -246,6 +237,10 @@ public class ForegroundService extends Service {
|
|
|
246
237
|
}
|
|
247
238
|
|
|
248
239
|
if (!noneEarlyReturn) {
|
|
240
|
+
mCurrentNotificationId = notificationId;
|
|
241
|
+
mCurrentNotificationBundle = bundle;
|
|
242
|
+
mCurrentNotification = notification;
|
|
243
|
+
mCurrentHashCode = hashCode;
|
|
249
244
|
// On headless task complete
|
|
250
245
|
final MethodCallResult<Void> methodCallResult =
|
|
251
246
|
(e, aVoid) -> {
|
|
@@ -262,37 +257,50 @@ public class ForegroundService extends Service {
|
|
|
262
257
|
pendingEvent = new ForegroundServiceEvent(notificationModel, methodCallResult);
|
|
263
258
|
}
|
|
264
259
|
} else {
|
|
265
|
-
if (mCurrentNotificationId.equals(
|
|
266
|
-
|
|
267
|
-
//
|
|
260
|
+
if (mCurrentNotificationId.equals(notificationId)) {
|
|
261
|
+
// Before API 31, updating the service's registered notification requires
|
|
262
|
+
// startForeground(). API 31+ processes a same-ID notify() against the foreground
|
|
263
|
+
// service record. Keep that path for an unchanged type: repeating
|
|
264
|
+
// startForeground() can extend a shortService timeout on API 34+.
|
|
265
|
+
boolean shouldStartForeground = Build.VERSION.SDK_INT < Build.VERSION_CODES.S;
|
|
266
|
+
int foregroundServiceType = mCurrentForegroundServiceType;
|
|
268
267
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
269
|
-
int
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
foregroundServiceType = resolveManifestServiceType();
|
|
268
|
+
int requestedType = notificationModel.getAndroid().getForegroundServiceType();
|
|
269
|
+
if (requestedType == ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST) {
|
|
270
|
+
requestedType = resolveManifestServiceType();
|
|
273
271
|
}
|
|
274
272
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
|
275
|
-
&&
|
|
273
|
+
&& requestedType == ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) {
|
|
276
274
|
Logger.e(
|
|
277
275
|
TAG,
|
|
278
276
|
"Resolved foreground service type is NONE on API 34+; skipping type"
|
|
279
277
|
+ " change.");
|
|
280
|
-
} else if (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
startForeground(hashCode, notification, foregroundServiceType);
|
|
284
|
-
} finally {
|
|
285
|
-
Trace.endSection();
|
|
286
|
-
}
|
|
287
|
-
mStartForegroundCalled = true;
|
|
288
|
-
mCurrentForegroundServiceType = foregroundServiceType;
|
|
289
|
-
shouldPostNotificationAgain = false;
|
|
278
|
+
} else if (requestedType != mCurrentForegroundServiceType) {
|
|
279
|
+
foregroundServiceType = requestedType;
|
|
280
|
+
shouldStartForeground = true;
|
|
290
281
|
}
|
|
291
282
|
}
|
|
292
|
-
if (
|
|
283
|
+
if (shouldStartForeground) {
|
|
284
|
+
Trace.beginSection("notifee:startForeground");
|
|
285
|
+
try {
|
|
286
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
287
|
+
startForeground(mCurrentHashCode, notification, foregroundServiceType);
|
|
288
|
+
} else {
|
|
289
|
+
startForeground(mCurrentHashCode, notification);
|
|
290
|
+
}
|
|
291
|
+
} finally {
|
|
292
|
+
Trace.endSection();
|
|
293
|
+
}
|
|
294
|
+
mStartForegroundCalled = true;
|
|
295
|
+
} else {
|
|
293
296
|
NotificationManagerCompat.from(ContextHolder.getApplicationContext())
|
|
294
|
-
.notify(
|
|
297
|
+
.notify(mCurrentHashCode, notification);
|
|
295
298
|
}
|
|
299
|
+
// The platform call returned normally, so cache the latest submitted state for
|
|
300
|
+
// re-post and timeout. A normal return does not acknowledge display.
|
|
301
|
+
mCurrentForegroundServiceType = foregroundServiceType;
|
|
302
|
+
mCurrentNotification = notification;
|
|
303
|
+
mCurrentNotificationBundle = bundle;
|
|
296
304
|
} else {
|
|
297
305
|
pendingEvent =
|
|
298
306
|
new NotificationEvent(
|
|
@@ -3,6 +3,7 @@ package app.notifee.core;
|
|
|
3
3
|
import static org.junit.Assert.assertEquals;
|
|
4
4
|
import static org.junit.Assert.assertNotNull;
|
|
5
5
|
import static org.junit.Assert.assertNull;
|
|
6
|
+
import static org.junit.Assert.assertThrows;
|
|
6
7
|
import static org.junit.Assert.assertTrue;
|
|
7
8
|
import static org.junit.Assert.fail;
|
|
8
9
|
|
|
@@ -17,6 +18,7 @@ import android.content.pm.PackageManager;
|
|
|
17
18
|
import android.content.pm.ServiceInfo;
|
|
18
19
|
import android.os.Bundle;
|
|
19
20
|
import androidx.core.app.NotificationCompat;
|
|
21
|
+
import app.notifee.core.event.ForegroundServiceEvent;
|
|
20
22
|
import app.notifee.core.event.NotificationEvent;
|
|
21
23
|
import java.lang.reflect.Field;
|
|
22
24
|
import java.lang.reflect.Method;
|
|
@@ -35,6 +37,7 @@ import org.robolectric.android.controller.ServiceController;
|
|
|
35
37
|
import org.robolectric.annotation.Config;
|
|
36
38
|
import org.robolectric.annotation.Implementation;
|
|
37
39
|
import org.robolectric.annotation.Implements;
|
|
40
|
+
import org.robolectric.shadows.ShadowNotificationManager;
|
|
38
41
|
import org.robolectric.shadows.ShadowService;
|
|
39
42
|
|
|
40
43
|
@RunWith(RobolectricTestRunner.class)
|
|
@@ -328,6 +331,473 @@ public class ForegroundServiceTest {
|
|
|
328
331
|
assertEquals(id.hashCode(), hashField.getInt(null));
|
|
329
332
|
}
|
|
330
333
|
|
|
334
|
+
@Test
|
|
335
|
+
@Config(sdk = 28)
|
|
336
|
+
public void onStartCommand_sameIdUpdate_refreshesRegisteredNotificationAndCachedState()
|
|
337
|
+
throws Exception {
|
|
338
|
+
createChannel();
|
|
339
|
+
ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
|
|
340
|
+
String id = "live";
|
|
341
|
+
FgsRunnerCapture runnerCapture = new FgsRunnerCapture();
|
|
342
|
+
EventBus.register(runnerCapture);
|
|
343
|
+
try {
|
|
344
|
+
service.onStartCommand(buildStartIntent(id, id.hashCode(), "A"), 0, 1);
|
|
345
|
+
service.onStartCommand(buildStartIntent(id, id.hashCode(), "B"), 0, 2);
|
|
346
|
+
|
|
347
|
+
Notification registered =
|
|
348
|
+
org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification();
|
|
349
|
+
NotificationManager manager =
|
|
350
|
+
(NotificationManager)
|
|
351
|
+
RuntimeEnvironment.getApplication().getSystemService(Context.NOTIFICATION_SERVICE);
|
|
352
|
+
Notification visible =
|
|
353
|
+
org.robolectric.Shadows.shadowOf(manager).getNotification(id.hashCode());
|
|
354
|
+
assertEquals("B", visible.extras.getString(Notification.EXTRA_TITLE));
|
|
355
|
+
assertEquals(1, org.robolectric.Shadows.shadowOf(manager).size());
|
|
356
|
+
assertEquals("B", registered.extras.getString(Notification.EXTRA_TITLE));
|
|
357
|
+
assertTrue((registered.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0);
|
|
358
|
+
assertEquals(
|
|
359
|
+
id.hashCode(),
|
|
360
|
+
org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
|
|
361
|
+
assertEquals(
|
|
362
|
+
"B",
|
|
363
|
+
((Notification) getPrivateStatic("mCurrentNotification"))
|
|
364
|
+
.extras.getString(Notification.EXTRA_TITLE));
|
|
365
|
+
assertEquals(
|
|
366
|
+
"B", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
|
|
367
|
+
assertEquals(id, ForegroundService.mCurrentNotificationId);
|
|
368
|
+
assertEquals(id.hashCode(), getPrivateStatic("mCurrentHashCode"));
|
|
369
|
+
assertEquals(1, runnerCapture.events.size());
|
|
370
|
+
} finally {
|
|
371
|
+
EventBus.unregister(runnerCapture);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
@Test
|
|
376
|
+
@Config(sdk = 29)
|
|
377
|
+
public void onStartCommand_sameIdUpdateApi29_preservesExplicitNoneTypeAndRegistration()
|
|
378
|
+
throws Exception {
|
|
379
|
+
createChannel();
|
|
380
|
+
ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
|
|
381
|
+
String id = "live";
|
|
382
|
+
|
|
383
|
+
service.onStartCommand(
|
|
384
|
+
buildStartIntent(id, id.hashCode(), "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE), 0, 1);
|
|
385
|
+
service.onStartCommand(
|
|
386
|
+
buildStartIntent(id, id.hashCode(), "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE), 0, 2);
|
|
387
|
+
|
|
388
|
+
assertEquals(
|
|
389
|
+
"B",
|
|
390
|
+
org.robolectric.Shadows.shadowOf(service)
|
|
391
|
+
.getLastForegroundNotification()
|
|
392
|
+
.extras
|
|
393
|
+
.getString(Notification.EXTRA_TITLE));
|
|
394
|
+
assertEquals(ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE, getLastForegroundServiceType(service));
|
|
395
|
+
assertEquals(
|
|
396
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE, ForegroundService.mCurrentForegroundServiceType);
|
|
397
|
+
assertEquals("B", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
@Test
|
|
401
|
+
@Config(sdk = 30)
|
|
402
|
+
public void onStartCommand_sameIdUpdateApi30_keepsTypedForegroundService() throws Exception {
|
|
403
|
+
createChannel();
|
|
404
|
+
ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
|
|
405
|
+
String id = "live";
|
|
406
|
+
service.onStartCommand(
|
|
407
|
+
buildStartIntent(id, id.hashCode(), "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
|
|
408
|
+
0,
|
|
409
|
+
1);
|
|
410
|
+
service.onStartCommand(
|
|
411
|
+
buildStartIntent(id, id.hashCode(), "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
|
|
412
|
+
0,
|
|
413
|
+
2);
|
|
414
|
+
|
|
415
|
+
assertEquals(
|
|
416
|
+
"B",
|
|
417
|
+
org.robolectric.Shadows.shadowOf(service)
|
|
418
|
+
.getLastForegroundNotification()
|
|
419
|
+
.extras
|
|
420
|
+
.getString(Notification.EXTRA_TITLE));
|
|
421
|
+
assertEquals(
|
|
422
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC, getLastForegroundServiceType(service));
|
|
423
|
+
assertEquals(
|
|
424
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
|
425
|
+
ForegroundService.mCurrentForegroundServiceType);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
@Test
|
|
429
|
+
@Config(sdk = 29)
|
|
430
|
+
public void onStartCommand_sameIdUpdateApi29_preservesManifestFallbackType() throws Exception {
|
|
431
|
+
createChannel();
|
|
432
|
+
ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
|
|
433
|
+
String id = "live";
|
|
434
|
+
service.onStartCommand(buildStartIntent(id, id.hashCode(), "A"), 0, 1);
|
|
435
|
+
service.onStartCommand(buildStartIntent(id, id.hashCode(), "B"), 0, 2);
|
|
436
|
+
|
|
437
|
+
assertEquals(
|
|
438
|
+
"B",
|
|
439
|
+
org.robolectric.Shadows.shadowOf(service)
|
|
440
|
+
.getLastForegroundNotification()
|
|
441
|
+
.extras
|
|
442
|
+
.getString(Notification.EXTRA_TITLE));
|
|
443
|
+
assertEquals(
|
|
444
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST, getLastForegroundServiceType(service));
|
|
445
|
+
assertEquals(
|
|
446
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST,
|
|
447
|
+
ForegroundService.mCurrentForegroundServiceType);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
@Test
|
|
451
|
+
@Config(sdk = 34)
|
|
452
|
+
public void onStartCommand_sameIdTypeChange_usesLatestTypedNotificationAndCache()
|
|
453
|
+
throws Exception {
|
|
454
|
+
createChannel();
|
|
455
|
+
declareForegroundServiceTypes(
|
|
456
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
|
457
|
+
| ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
|
458
|
+
ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
|
|
459
|
+
String id = "live";
|
|
460
|
+
|
|
461
|
+
service.onStartCommand(
|
|
462
|
+
buildStartIntent(id, id.hashCode(), "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
|
|
463
|
+
0,
|
|
464
|
+
1);
|
|
465
|
+
service.onStartCommand(
|
|
466
|
+
buildStartIntent(
|
|
467
|
+
id, id.hashCode(), "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK),
|
|
468
|
+
0,
|
|
469
|
+
2);
|
|
470
|
+
|
|
471
|
+
assertEquals(
|
|
472
|
+
"B",
|
|
473
|
+
org.robolectric.Shadows.shadowOf(service)
|
|
474
|
+
.getLastForegroundNotification()
|
|
475
|
+
.extras
|
|
476
|
+
.getString(Notification.EXTRA_TITLE));
|
|
477
|
+
assertEquals(
|
|
478
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK, getLastForegroundServiceType(service));
|
|
479
|
+
assertEquals(
|
|
480
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK,
|
|
481
|
+
ForegroundService.mCurrentForegroundServiceType);
|
|
482
|
+
assertEquals(
|
|
483
|
+
"B",
|
|
484
|
+
((Notification) getPrivateStatic("mCurrentNotification"))
|
|
485
|
+
.extras.getString(Notification.EXTRA_TITLE));
|
|
486
|
+
assertEquals("B", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
@Test
|
|
490
|
+
@Config(sdk = 33)
|
|
491
|
+
public void receiverDeleteAfterSameIdUpdate_repostsLatestNotificationWithoutDismissEvent()
|
|
492
|
+
throws Exception {
|
|
493
|
+
createChannel();
|
|
494
|
+
ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
|
|
495
|
+
String id = "live";
|
|
496
|
+
service.onStartCommand(buildStartIntent(id, id.hashCode(), "A"), 0, 1);
|
|
497
|
+
service.onStartCommand(buildStartIntent(id, id.hashCode(), "B"), 0, 2);
|
|
498
|
+
|
|
499
|
+
NotificationManager manager =
|
|
500
|
+
(NotificationManager)
|
|
501
|
+
RuntimeEnvironment.getApplication().getSystemService(Context.NOTIFICATION_SERVICE);
|
|
502
|
+
manager.cancel(id.hashCode());
|
|
503
|
+
ReceiverService receiver = Robolectric.buildService(ReceiverService.class).create().get();
|
|
504
|
+
Intent delete = new Intent();
|
|
505
|
+
delete.setAction(ReceiverService.DELETE_INTENT);
|
|
506
|
+
delete.putExtra("notification", buildNotificationBundle(id));
|
|
507
|
+
FgsEventCapture capture = new FgsEventCapture();
|
|
508
|
+
EventBus.register(capture);
|
|
509
|
+
try {
|
|
510
|
+
receiver.onStartCommand(delete, 0, 3);
|
|
511
|
+
Notification reposted =
|
|
512
|
+
org.robolectric.Shadows.shadowOf(manager).getNotification(id.hashCode());
|
|
513
|
+
assertNotNull(reposted);
|
|
514
|
+
assertEquals("B", reposted.extras.getString(Notification.EXTRA_TITLE));
|
|
515
|
+
assertEquals(1, org.robolectric.Shadows.shadowOf(manager).size());
|
|
516
|
+
assertEquals(0, capture.events.size());
|
|
517
|
+
} finally {
|
|
518
|
+
EventBus.unregister(capture);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
@Test
|
|
523
|
+
@Config(sdk = 34)
|
|
524
|
+
public void onTimeout_afterShortServiceSameIdUpdate_emitsLatestBundleWithoutRestart()
|
|
525
|
+
throws Exception {
|
|
526
|
+
createChannel();
|
|
527
|
+
declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
|
|
528
|
+
ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
|
|
529
|
+
String id = "live";
|
|
530
|
+
service.onStartCommand(
|
|
531
|
+
buildStartIntent(id, id.hashCode(), "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
|
|
532
|
+
0,
|
|
533
|
+
1);
|
|
534
|
+
service.onStartCommand(
|
|
535
|
+
buildStartIntent(id, id.hashCode(), "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
|
|
536
|
+
0,
|
|
537
|
+
2);
|
|
538
|
+
|
|
539
|
+
NotificationManager manager =
|
|
540
|
+
(NotificationManager)
|
|
541
|
+
RuntimeEnvironment.getApplication().getSystemService(Context.NOTIFICATION_SERVICE);
|
|
542
|
+
assertEquals(
|
|
543
|
+
"B",
|
|
544
|
+
org.robolectric.Shadows.shadowOf(manager)
|
|
545
|
+
.getNotification(id.hashCode())
|
|
546
|
+
.extras
|
|
547
|
+
.getString(Notification.EXTRA_TITLE));
|
|
548
|
+
// Robolectric does not model Android 12+ notify-to-FGS synchronization. Its service shadow
|
|
549
|
+
// remains at A here, which also guards against resetting shortService's timer via a repeat
|
|
550
|
+
// startForeground call.
|
|
551
|
+
assertEquals(
|
|
552
|
+
"A",
|
|
553
|
+
org.robolectric.Shadows.shadowOf(service)
|
|
554
|
+
.getLastForegroundNotification()
|
|
555
|
+
.extras
|
|
556
|
+
.getString(Notification.EXTRA_TITLE));
|
|
557
|
+
FgsEventCapture capture = new FgsEventCapture();
|
|
558
|
+
EventBus.register(capture);
|
|
559
|
+
try {
|
|
560
|
+
service.onTimeout(42);
|
|
561
|
+
assertEquals(1, capture.events.size());
|
|
562
|
+
assertEquals(NotificationEvent.TYPE_FG_TIMEOUT, capture.events.get(0).getType());
|
|
563
|
+
assertEquals("B", capture.events.get(0).getNotification().getTitle());
|
|
564
|
+
} finally {
|
|
565
|
+
EventBus.unregister(capture);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
@Test
|
|
570
|
+
@Config(sdk = 33)
|
|
571
|
+
public void onStartCommand_differentId_emitsAlreadyExistsAndKeepsActiveState() throws Exception {
|
|
572
|
+
createChannel();
|
|
573
|
+
ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
|
|
574
|
+
String id = "live";
|
|
575
|
+
service.onStartCommand(buildStartIntent(id, id.hashCode(), "A"), 0, 1);
|
|
576
|
+
FgsEventCapture capture = new FgsEventCapture();
|
|
577
|
+
EventBus.register(capture);
|
|
578
|
+
try {
|
|
579
|
+
service.onStartCommand(buildStartIntent("other", "other".hashCode(), "B"), 0, 2);
|
|
580
|
+
assertEquals(1, capture.events.size());
|
|
581
|
+
assertEquals(NotificationEvent.TYPE_FG_ALREADY_EXIST, capture.events.get(0).getType());
|
|
582
|
+
assertEquals(id, ForegroundService.mCurrentNotificationId);
|
|
583
|
+
assertEquals(
|
|
584
|
+
"A", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
|
|
585
|
+
NotificationManager manager =
|
|
586
|
+
(NotificationManager)
|
|
587
|
+
RuntimeEnvironment.getApplication().getSystemService(Context.NOTIFICATION_SERVICE);
|
|
588
|
+
assertEquals(1, org.robolectric.Shadows.shadowOf(manager).size());
|
|
589
|
+
} finally {
|
|
590
|
+
EventBus.unregister(capture);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
@Test
|
|
595
|
+
@Config(sdk = 28, shadows = RejectBStartForegroundShadowService.class)
|
|
596
|
+
public void onStartCommand_sameTypeStartForegroundFailure_keepsPreviousCache() throws Exception {
|
|
597
|
+
createChannel();
|
|
598
|
+
ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
|
|
599
|
+
String id = "live";
|
|
600
|
+
service.onStartCommand(buildStartIntent(id, id.hashCode(), "A"), 0, 1);
|
|
601
|
+
|
|
602
|
+
SecurityException failure =
|
|
603
|
+
assertThrows(
|
|
604
|
+
SecurityException.class,
|
|
605
|
+
() -> service.onStartCommand(buildStartIntent(id, id.hashCode(), "B"), 0, 2));
|
|
606
|
+
assertEquals("posting B rejected", failure.getMessage());
|
|
607
|
+
|
|
608
|
+
assertEquals(id, ForegroundService.mCurrentNotificationId);
|
|
609
|
+
assertEquals(id.hashCode(), getPrivateStatic("mCurrentHashCode"));
|
|
610
|
+
assertEquals("A", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
|
|
611
|
+
assertEquals(
|
|
612
|
+
"A",
|
|
613
|
+
((Notification) getPrivateStatic("mCurrentNotification"))
|
|
614
|
+
.extras.getString(Notification.EXTRA_TITLE));
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
@Test
|
|
618
|
+
@Config(sdk = 28, shadows = RejectBStartForegroundShadowService.class)
|
|
619
|
+
public void onStartCommand_initialStartForegroundFailure_doesNotClaimActiveService()
|
|
620
|
+
throws Exception {
|
|
621
|
+
createChannel();
|
|
622
|
+
ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
|
|
623
|
+
String id = "live";
|
|
624
|
+
FgsRunnerCapture runnerCapture = new FgsRunnerCapture();
|
|
625
|
+
EventBus.register(runnerCapture);
|
|
626
|
+
try {
|
|
627
|
+
SecurityException failure =
|
|
628
|
+
assertThrows(
|
|
629
|
+
SecurityException.class,
|
|
630
|
+
() -> service.onStartCommand(buildStartIntent(id, id.hashCode(), "B"), 0, 1));
|
|
631
|
+
assertEquals("posting B rejected", failure.getMessage());
|
|
632
|
+
assertNull(ForegroundService.mCurrentNotificationId);
|
|
633
|
+
assertNull(getPrivateStatic("mCurrentNotification"));
|
|
634
|
+
assertNull(getPrivateStatic("mCurrentNotificationBundle"));
|
|
635
|
+
assertEquals(0, getPrivateStatic("mCurrentHashCode"));
|
|
636
|
+
assertEquals(0, runnerCapture.events.size());
|
|
637
|
+
|
|
638
|
+
service.onStartCommand(buildStartIntent(id, id.hashCode(), "A"), 0, 2);
|
|
639
|
+
assertEquals(1, runnerCapture.events.size());
|
|
640
|
+
assertEquals(
|
|
641
|
+
"A", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
|
|
642
|
+
} finally {
|
|
643
|
+
EventBus.unregister(runnerCapture);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
@Test
|
|
648
|
+
@Config(sdk = 34, shadows = RejectBStartForegroundShadowService.class)
|
|
649
|
+
public void onStartCommand_typeChangeStartForegroundFailure_keepsPreviousTypeAndCache()
|
|
650
|
+
throws Exception {
|
|
651
|
+
createChannel();
|
|
652
|
+
declareForegroundServiceTypes(
|
|
653
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
|
654
|
+
| ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
|
655
|
+
ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
|
|
656
|
+
String id = "live";
|
|
657
|
+
service.onStartCommand(
|
|
658
|
+
buildStartIntent(id, id.hashCode(), "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
|
|
659
|
+
0,
|
|
660
|
+
1);
|
|
661
|
+
|
|
662
|
+
SecurityException failure =
|
|
663
|
+
assertThrows(
|
|
664
|
+
SecurityException.class,
|
|
665
|
+
() ->
|
|
666
|
+
service.onStartCommand(
|
|
667
|
+
buildStartIntent(
|
|
668
|
+
id, id.hashCode(), "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK),
|
|
669
|
+
0,
|
|
670
|
+
2));
|
|
671
|
+
assertEquals("posting B rejected", failure.getMessage());
|
|
672
|
+
|
|
673
|
+
assertEquals(
|
|
674
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
|
675
|
+
ForegroundService.mCurrentForegroundServiceType);
|
|
676
|
+
assertEquals("A", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
|
|
677
|
+
assertEquals(
|
|
678
|
+
"A",
|
|
679
|
+
((Notification) getPrivateStatic("mCurrentNotification"))
|
|
680
|
+
.extras.getString(Notification.EXTRA_TITLE));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
@Test
|
|
684
|
+
@Config(sdk = 33, shadows = RejectBNotificationShadowManager.class)
|
|
685
|
+
public void onStartCommand_notifyFailure_keepsPreviousCache() throws Exception {
|
|
686
|
+
createChannel();
|
|
687
|
+
ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
|
|
688
|
+
String id = "live";
|
|
689
|
+
service.onStartCommand(buildStartIntent(id, id.hashCode(), "A"), 0, 1);
|
|
690
|
+
|
|
691
|
+
SecurityException failure =
|
|
692
|
+
assertThrows(
|
|
693
|
+
SecurityException.class,
|
|
694
|
+
() -> service.onStartCommand(buildStartIntent(id, id.hashCode(), "B"), 0, 2));
|
|
695
|
+
assertEquals("posting B rejected", failure.getMessage());
|
|
696
|
+
|
|
697
|
+
assertEquals(id, ForegroundService.mCurrentNotificationId);
|
|
698
|
+
assertEquals(id.hashCode(), getPrivateStatic("mCurrentHashCode"));
|
|
699
|
+
assertEquals("A", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
|
|
700
|
+
assertEquals(
|
|
701
|
+
"A",
|
|
702
|
+
((Notification) getPrivateStatic("mCurrentNotification"))
|
|
703
|
+
.extras.getString(Notification.EXTRA_TITLE));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
@Test
|
|
707
|
+
@Config(sdk = 34, shadows = SilentlyRejectMissingChannelShadowManager.class)
|
|
708
|
+
public void onStartCommand_notifyReturnsNormallyWithoutPosting_keepsLatestSubmittedState()
|
|
709
|
+
throws Exception {
|
|
710
|
+
createChannel();
|
|
711
|
+
declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
|
|
712
|
+
ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
|
|
713
|
+
String id = "live";
|
|
714
|
+
service.onStartCommand(
|
|
715
|
+
buildStartIntent(id, id.hashCode(), "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
|
|
716
|
+
0,
|
|
717
|
+
1);
|
|
718
|
+
|
|
719
|
+
Intent update =
|
|
720
|
+
buildStartIntent(id, id.hashCode(), "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
|
|
721
|
+
String missingChannelId = "missing-channel";
|
|
722
|
+
Notification rejected =
|
|
723
|
+
new NotificationCompat.Builder(RuntimeEnvironment.getApplication(), missingChannelId)
|
|
724
|
+
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
|
725
|
+
.setContentTitle("B")
|
|
726
|
+
.setOnlyAlertOnce(true)
|
|
727
|
+
.build();
|
|
728
|
+
update.putExtra("notification", rejected);
|
|
729
|
+
update
|
|
730
|
+
.getBundleExtra("notificationBundle")
|
|
731
|
+
.getBundle("android")
|
|
732
|
+
.putString("channelId", missingChannelId);
|
|
733
|
+
|
|
734
|
+
NotificationManager manager =
|
|
735
|
+
(NotificationManager)
|
|
736
|
+
RuntimeEnvironment.getApplication().getSystemService(Context.NOTIFICATION_SERVICE);
|
|
737
|
+
assertNull(manager.getNotificationChannel(missingChannelId));
|
|
738
|
+
SilentlyRejectMissingChannelShadowManager.rejectedPosts = 0;
|
|
739
|
+
SilentlyRejectMissingChannelShadowManager.lastRejectedNotification = null;
|
|
740
|
+
service.onStartCommand(update, 0, 2);
|
|
741
|
+
|
|
742
|
+
// This shadow models the platform's normal-return, no-post outcome. Robolectric does not
|
|
743
|
+
// reproduce the separate Android foreground-service record update in this sequence.
|
|
744
|
+
assertEquals(1, SilentlyRejectMissingChannelShadowManager.rejectedPosts);
|
|
745
|
+
assertEquals(
|
|
746
|
+
"B",
|
|
747
|
+
SilentlyRejectMissingChannelShadowManager.lastRejectedNotification.extras.getString(
|
|
748
|
+
Notification.EXTRA_TITLE));
|
|
749
|
+
assertEquals(
|
|
750
|
+
"A",
|
|
751
|
+
org.robolectric.Shadows.shadowOf(manager)
|
|
752
|
+
.getNotification(id.hashCode())
|
|
753
|
+
.extras
|
|
754
|
+
.getString(Notification.EXTRA_TITLE));
|
|
755
|
+
// The simulated visible card remains A while NotifyKit's latest submitted state is B.
|
|
756
|
+
assertEquals("B", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
|
|
757
|
+
assertEquals(
|
|
758
|
+
"B",
|
|
759
|
+
((Notification) getPrivateStatic("mCurrentNotification"))
|
|
760
|
+
.extras.getString(Notification.EXTRA_TITLE));
|
|
761
|
+
assertEquals(
|
|
762
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
|
|
763
|
+
ForegroundService.mCurrentForegroundServiceType);
|
|
764
|
+
assertEquals(id, ForegroundService.mCurrentNotificationId);
|
|
765
|
+
assertEquals(id.hashCode(), getPrivateStatic("mCurrentHashCode"));
|
|
766
|
+
// The service shadow remains at A because the unchanged-type path did not call
|
|
767
|
+
// startForeground() again, which could extend a shortService timeout on Android.
|
|
768
|
+
assertEquals(
|
|
769
|
+
"A",
|
|
770
|
+
org.robolectric.Shadows.shadowOf(service)
|
|
771
|
+
.getLastForegroundNotification()
|
|
772
|
+
.extras
|
|
773
|
+
.getString(Notification.EXTRA_TITLE));
|
|
774
|
+
|
|
775
|
+
manager.cancel(id.hashCode());
|
|
776
|
+
ReceiverService receiver = Robolectric.buildService(ReceiverService.class).create().get();
|
|
777
|
+
Intent delete = new Intent();
|
|
778
|
+
delete.setAction(ReceiverService.DELETE_INTENT);
|
|
779
|
+
delete.putExtra("notification", buildNotificationBundle(id));
|
|
780
|
+
FgsEventCapture capture = new FgsEventCapture();
|
|
781
|
+
EventBus.register(capture);
|
|
782
|
+
try {
|
|
783
|
+
receiver.onStartCommand(delete, 0, 3);
|
|
784
|
+
assertEquals(2, SilentlyRejectMissingChannelShadowManager.rejectedPosts);
|
|
785
|
+
assertEquals(
|
|
786
|
+
"B",
|
|
787
|
+
SilentlyRejectMissingChannelShadowManager.lastRejectedNotification.extras.getString(
|
|
788
|
+
Notification.EXTRA_TITLE));
|
|
789
|
+
assertNull(org.robolectric.Shadows.shadowOf(manager).getNotification(id.hashCode()));
|
|
790
|
+
assertEquals(0, capture.events.size());
|
|
791
|
+
|
|
792
|
+
service.onTimeout(42);
|
|
793
|
+
assertEquals(1, capture.events.size());
|
|
794
|
+
assertEquals(NotificationEvent.TYPE_FG_TIMEOUT, capture.events.get(0).getType());
|
|
795
|
+
assertEquals("B", capture.events.get(0).getNotification().getTitle());
|
|
796
|
+
} finally {
|
|
797
|
+
EventBus.unregister(capture);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
331
801
|
/**
|
|
332
802
|
* Regression guard for the 9.1.13 {@code onTimeout(int)} fix (upstream invertase/notifee#703). On
|
|
333
803
|
* API 34, Android's single-argument {@code onTimeout} fires when a {@code shortService} FGS
|
|
@@ -473,11 +943,35 @@ public class ForegroundServiceTest {
|
|
|
473
943
|
}
|
|
474
944
|
|
|
475
945
|
private static Intent buildStartIntent(String id, int hashCode) {
|
|
946
|
+
return buildStartIntent(id, hashCode, "FGS test");
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
private static Intent buildStartIntent(String id, int hashCode, String title) {
|
|
476
950
|
Intent intent = new Intent();
|
|
477
951
|
intent.setAction(ForegroundService.START_FOREGROUND_SERVICE_ACTION);
|
|
478
952
|
intent.putExtra("hashCode", hashCode);
|
|
479
|
-
|
|
480
|
-
|
|
953
|
+
Notification notification =
|
|
954
|
+
new NotificationCompat.Builder(RuntimeEnvironment.getApplication(), TEST_CHANNEL_ID)
|
|
955
|
+
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
|
956
|
+
.setContentTitle(title)
|
|
957
|
+
.setOnlyAlertOnce(true)
|
|
958
|
+
.build();
|
|
959
|
+
intent.putExtra("notification", notification);
|
|
960
|
+
Bundle bundle = buildNotificationBundle(id);
|
|
961
|
+
bundle.putString("title", title);
|
|
962
|
+
intent.putExtra("notificationBundle", bundle);
|
|
963
|
+
return intent;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
private static Intent buildStartIntent(
|
|
967
|
+
String id, int hashCode, String title, int foregroundServiceType) {
|
|
968
|
+
Intent intent = buildStartIntent(id, hashCode, title);
|
|
969
|
+
Bundle bundle = intent.getBundleExtra("notificationBundle");
|
|
970
|
+
Bundle android = bundle.getBundle("android");
|
|
971
|
+
ArrayList<Integer> types = new ArrayList<>();
|
|
972
|
+
types.add(foregroundServiceType);
|
|
973
|
+
android.putIntegerArrayList("foregroundServiceTypes", types);
|
|
974
|
+
intent.putExtra("notificationBundle", bundle);
|
|
481
975
|
return intent;
|
|
482
976
|
}
|
|
483
977
|
|
|
@@ -538,6 +1032,67 @@ public class ForegroundServiceTest {
|
|
|
538
1032
|
field.set(null, value);
|
|
539
1033
|
}
|
|
540
1034
|
|
|
1035
|
+
private static Object getPrivateStatic(String name) throws Exception {
|
|
1036
|
+
Field field = ForegroundService.class.getDeclaredField(name);
|
|
1037
|
+
field.setAccessible(true);
|
|
1038
|
+
return field.get(null);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
public static class FgsRunnerCapture {
|
|
1042
|
+
final List<ForegroundServiceEvent> events = new ArrayList<>();
|
|
1043
|
+
|
|
1044
|
+
@Subscribe(threadMode = ThreadMode.POSTING)
|
|
1045
|
+
public void onForegroundServiceEvent(ForegroundServiceEvent event) {
|
|
1046
|
+
events.add(event);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
@Implements(Service.class)
|
|
1051
|
+
public static class RejectBStartForegroundShadowService extends ShadowService {
|
|
1052
|
+
@Implementation
|
|
1053
|
+
protected void startForeground(int id, Notification notification) {
|
|
1054
|
+
if ("B".equals(notification.extras.getString(Notification.EXTRA_TITLE))) {
|
|
1055
|
+
throw new SecurityException("posting B rejected");
|
|
1056
|
+
}
|
|
1057
|
+
super.startForeground(id, notification);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
@Implementation
|
|
1061
|
+
protected void startForeground(int id, Notification notification, int foregroundServiceType) {
|
|
1062
|
+
if ("B".equals(notification.extras.getString(Notification.EXTRA_TITLE))) {
|
|
1063
|
+
throw new SecurityException("posting B rejected");
|
|
1064
|
+
}
|
|
1065
|
+
super.startForeground(id, notification, foregroundServiceType);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
@Implements(NotificationManager.class)
|
|
1070
|
+
public static class RejectBNotificationShadowManager extends ShadowNotificationManager {
|
|
1071
|
+
@Implementation
|
|
1072
|
+
protected void notify(String tag, int id, Notification notification) {
|
|
1073
|
+
if ("B".equals(notification.extras.getString(Notification.EXTRA_TITLE))) {
|
|
1074
|
+
throw new SecurityException("posting B rejected");
|
|
1075
|
+
}
|
|
1076
|
+
super.notify(tag, id, notification);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
@Implements(NotificationManager.class)
|
|
1081
|
+
public static class SilentlyRejectMissingChannelShadowManager extends ShadowNotificationManager {
|
|
1082
|
+
static int rejectedPosts;
|
|
1083
|
+
static Notification lastRejectedNotification;
|
|
1084
|
+
|
|
1085
|
+
@Implementation
|
|
1086
|
+
protected void notify(String tag, int id, Notification notification) {
|
|
1087
|
+
if ("missing-channel".equals(notification.getChannelId())) {
|
|
1088
|
+
rejectedPosts++;
|
|
1089
|
+
lastRejectedNotification = notification;
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
super.notify(tag, id, notification);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
541
1096
|
/**
|
|
542
1097
|
* greenrobot EventBus subscriber that records every {@link NotificationEvent} posted during a
|
|
543
1098
|
* test. {@link ThreadMode#POSTING} fires the subscriber synchronously on the caller thread, so
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "10.7.
|
|
1
|
+
export declare const version = "10.7.2";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-notify-kit",
|
|
3
|
-
"version": "10.7.
|
|
3
|
+
"version": "10.7.2",
|
|
4
4
|
"author": "Marco Crupi",
|
|
5
5
|
"description": "Maintained Notifee-compatible React Native notifications library for Android, iOS, FCM Mode, rich notifications, foreground services, and Expo CNG development builds.",
|
|
6
6
|
"main": "dist/index.js",
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by genversion.
|
|
2
|
-
export const version = '10.7.
|
|
2
|
+
export const version = '10.7.2';
|