react-native-notify-kit 10.7.0 → 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
- * Re-posts the foreground service notification if the given notification ID matches the active
76
- * foreground service. On Android 14+, users can dismiss ongoing foreground service notifications
77
- * for most service types; this method restores the notification so the user remains aware of the
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 the notification was re-posted, false otherwise
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, "Re-posting foreground service notification dismissed by user");
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(notificationModel.getId())) {
266
- boolean shouldPostNotificationAgain = true;
267
- // find if we need to start the service again if the type was changed
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 foregroundServiceType =
270
- notificationModel.getAndroid().getForegroundServiceType();
271
- if (foregroundServiceType == ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST) {
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
- && foregroundServiceType == ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) {
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 (foregroundServiceType != mCurrentForegroundServiceType) {
281
- Trace.beginSection("notifee:startForeground");
282
- try {
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 (shouldPostNotificationAgain) {
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(hashCode, notification);
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
- intent.putExtra("notification", buildNotification());
480
- intent.putExtra("notificationBundle", buildNotificationBundle(id));
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
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { AndroidStyle } from '../types/NotificationAndroid';
6
6
  import { isAndroid, isIOS } from '../utils';
7
+ import { isValidTimestamp } from '../validators/validate';
7
8
  const PREFIX = '[react-native-notify-kit]';
8
9
  const RESERVED_DATA_KEYS = new Set(['notifee_options', 'notifee_data']);
9
10
  const VALID_INTERRUPTION_LEVELS = new Set(['active', 'critical', 'passive', 'timeSensitive']);
@@ -92,6 +93,12 @@ function buildAndroidConfig(raw, config) {
92
93
  android.largeIcon = raw.largeIcon;
93
94
  if (typeof raw?.color === 'string')
94
95
  android.color = raw.color;
96
+ // Timestamp fields — preserve only values accepted by the canonical Android validator
97
+ if (typeof raw?.showTimestamp === 'boolean')
98
+ android.showTimestamp = raw.showTimestamp;
99
+ if (typeof raw?.timestamp === 'number' && isValidTimestamp(raw.timestamp)) {
100
+ android.timestamp = raw.timestamp;
101
+ }
95
102
  // Actions array — pass through (trust server validation)
96
103
  if (Array.isArray(raw?.actions)) {
97
104
  android.actions = raw.actions;
@@ -1 +1 @@
1
- {"version":3,"file":"reconstructNotification.js","sourceRoot":"","sources":["../../src/fcm/reconstructNotification.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"reconstructNotification.js","sourceRoot":"","sources":["../../src/fcm/reconstructNotification.ts"],"names":[],"mappings":"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"}
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "10.7.0";
1
+ export declare const version = "10.7.2";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated by genversion.
2
- export const version = '10.7.0';
2
+ export const version = '10.7.2';
3
3
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-notify-kit",
3
- "version": "10.7.0",
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/server/README.md CHANGED
@@ -24,6 +24,8 @@ const message = buildNotifyKitPayload({
24
24
  android: {
25
25
  channelId: 'orders',
26
26
  smallIcon: 'ic_notification',
27
+ showTimestamp: true,
28
+ timestamp: Date.now(),
27
29
  pressAction: { id: 'open-order' },
28
30
  },
29
31
  ios: {
@@ -44,6 +46,8 @@ await admin.messaging().send(message);
44
46
 
45
47
  `options.ttl` is expressed in seconds. Pass the message directly to Firebase Admin without an adapter or a manual unit conversion.
46
48
 
49
+ Android `showTimestamp` and `timestamp` stay inside the `_v: 1` `notifee_options` blob and are reconstructed client-side using the existing Android notification semantics. They are not Firebase Admin `android.notification` fields; the Android message remains data-only.
50
+
47
51
  ## API
48
52
 
49
53
  ### `buildNotifyKitPayload(input: NotifyKitPayloadInput): NotifyKitPayloadOutput`
@@ -7,6 +7,7 @@ import { Notification } from '../types/Notification';
7
7
  import { NotificationAndroid, AndroidStyle } from '../types/NotificationAndroid';
8
8
  import { NotificationIOS } from '../types/NotificationIOS';
9
9
  import { isAndroid, isIOS } from '../utils';
10
+ import { isValidTimestamp } from '../validators/validate';
10
11
  import type { FcmConfig, FcmRemoteMessage } from './types';
11
12
  import type { ParsedPayload } from './parseFcmPayload';
12
13
 
@@ -126,6 +127,12 @@ function buildAndroidConfig(
126
127
  if (typeof raw?.largeIcon === 'string') android.largeIcon = raw.largeIcon;
127
128
  if (typeof raw?.color === 'string') android.color = raw.color;
128
129
 
130
+ // Timestamp fields — preserve only values accepted by the canonical Android validator
131
+ if (typeof raw?.showTimestamp === 'boolean') android.showTimestamp = raw.showTimestamp;
132
+ if (typeof raw?.timestamp === 'number' && isValidTimestamp(raw.timestamp)) {
133
+ android.timestamp = raw.timestamp;
134
+ }
135
+
129
136
  // Actions array — pass through (trust server validation)
130
137
  if (Array.isArray(raw?.actions)) {
131
138
  android.actions = raw.actions as NotificationAndroid['actions'];
@@ -35,6 +35,8 @@ export type NotifyKitAndroidConfig = {
35
35
  pressAction?: NotifyKitPressAction;
36
36
  actions?: NotifyKitAndroidAction[];
37
37
  style?: NotifyKitAndroidStyle;
38
+ showTimestamp?: boolean;
39
+ timestamp?: number;
38
40
  };
39
41
 
40
42
  export type NotifyKitIosAttachment = {
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '10.7.0';
2
+ export const version = '10.7.2';