react-native-notify-kit 10.2.0 → 10.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +6 -6
  2. package/android/src/main/AndroidManifest.xml +0 -1
  3. package/android/src/main/java/app/notifee/core/ForegroundService.java +3 -1
  4. package/android/src/main/java/app/notifee/core/NotifeeAlarmManager.java +4 -2
  5. package/android/src/main/java/app/notifee/core/NotificationManager.java +43 -43
  6. package/android/src/main/java/app/notifee/core/ReceiverService.java +18 -13
  7. package/android/src/main/java/app/notifee/core/database/NotifeeCoreDatabase.java +1 -4
  8. package/android/src/main/java/app/notifee/core/model/ChannelModel.java +8 -13
  9. package/android/src/main/java/app/notifee/core/model/IntervalTriggerModel.java +2 -6
  10. package/android/src/main/java/app/notifee/core/model/NotificationAndroidActionModel.java +1 -2
  11. package/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java +30 -33
  12. package/android/src/main/java/app/notifee/core/model/NotificationAndroidPressActionModel.java +3 -9
  13. package/android/src/main/java/app/notifee/core/model/NotificationAndroidStyleModel.java +6 -4
  14. package/android/src/main/java/app/notifee/core/model/TimestampTriggerModel.java +7 -4
  15. package/android/src/main/java/app/notifee/core/utility/BundleValueReader.java +65 -0
  16. package/android/src/main/java/app/notifee/core/utility/IntentUtils.java +0 -28
  17. package/android/src/main/java/app/notifee/core/utility/ObjectUtils.java +1 -1
  18. package/android/src/main/java/app/notifee/core/utility/ParcelableCompatReader.java +64 -0
  19. package/android/src/main/java/app/notifee/core/utility/PowerManagerUtils.java +79 -41
  20. package/android/src/main/java/app/notifee/core/utility/ResourceUtils.java +3 -0
  21. package/android/src/main/kotlin/io/invertase/notifee/NotifeeApiModule.kt +1 -1
  22. package/android/src/main/kotlin/io/invertase/notifee/NotifeeInitProvider.kt +0 -26
  23. package/android/src/main/kotlin/io/invertase/notifee/NotifeeReactUtils.kt +1 -2
  24. package/android/src/test/java/app/notifee/core/NotifeeAlarmManagerCurrentBehaviorTest.java +446 -0
  25. package/android/src/test/java/app/notifee/core/NotificationManagerCreateTriggerNotificationCurrentBehaviorTest.java +203 -0
  26. package/android/src/test/java/app/notifee/core/NotificationManagerPressActionOptOutTest.java +180 -0
  27. package/android/src/test/java/app/notifee/core/NotificationReceiverHandlerInitialNotificationTest.java +157 -0
  28. package/android/src/test/java/app/notifee/core/ReceiverServiceInitialNotificationTest.java +169 -0
  29. package/android/src/test/java/app/notifee/core/model/ChannelModelTest.java +116 -0
  30. package/android/src/test/java/app/notifee/core/model/IntervalTriggerModelTest.java +71 -0
  31. package/android/src/test/java/app/notifee/core/model/NotificationAndroidModelTest.java +309 -0
  32. package/android/src/test/java/app/notifee/core/model/NotificationAndroidStyleModelTest.java +154 -0
  33. package/android/src/test/java/app/notifee/core/model/TimestampTriggerModelTest.java +296 -14
  34. package/android/src/test/java/app/notifee/core/utility/BundleValueReaderTest.java +131 -0
  35. package/android/src/test/java/app/notifee/core/utility/ObjectUtilsTest.java +142 -0
  36. package/android/src/test/java/app/notifee/core/utility/ParcelableCompatReaderTest.java +124 -0
  37. package/android/src/test/java/io/invertase/notifee/HeadlessTaskReactHostTest.java +136 -0
  38. package/android/src/test/java/io/invertase/notifee/NotifeeEventSubscriberRoutingTest.java +262 -0
  39. package/android/src/test/java/io/invertase/notifee/NotifeeReactUtilsDrawerTest.java +24 -0
  40. package/cli/dist/lib/patchPodfile.d.ts +3 -1
  41. package/cli/dist/lib/patchPodfile.js +125 -33
  42. package/cli/dist/lib/patchPodfile.js.map +1 -1
  43. package/dist/index.d.ts +2 -0
  44. package/dist/types/Module.d.ts +20 -6
  45. package/dist/types/Notification.d.ts +5 -2
  46. package/dist/types/Notification.js +5 -2
  47. package/dist/types/Notification.js.map +1 -1
  48. package/dist/types/NotificationAndroid.d.ts +4 -0
  49. package/dist/types/NotificationAndroid.js +4 -0
  50. package/dist/types/NotificationAndroid.js.map +1 -1
  51. package/dist/types/PowerManagerInfo.d.ts +9 -4
  52. package/dist/version.d.ts +1 -1
  53. package/dist/version.js +1 -1
  54. package/ios/NotifeeCore/NotifeeCore.m +101 -114
  55. package/package.json +1 -1
  56. package/src/index.ts +2 -0
  57. package/src/types/Module.ts +20 -6
  58. package/src/types/Notification.ts +5 -2
  59. package/src/types/NotificationAndroid.ts +4 -0
  60. package/src/types/PowerManagerInfo.ts +9 -4
  61. package/src/version.ts +1 -1
package/README.md CHANGED
@@ -421,11 +421,11 @@ As bugs are fixed, this table is updated. See [CHANGELOG.md](CHANGELOG.md) for f
421
421
 
422
422
  Some upstream Notifee issues are not bugs in the library itself but platform-level limitations imposed by Android's Doze mode and vendor power management — no library code can make `AlarmManager` deliver an alarm to, or a foreground service survive inside, an app the OEM has explicitly paused. For these, the fork provides **documented mitigations**: user-facing helper APIs, code-level self-healing where possible, and decision guides that steer consumers toward the Android primitive most resilient to the specific vendor policy.
423
423
 
424
- | Upstream issue | Symptom | Platform root cause | Fork mitigation |
425
- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
426
- | [invertase/notifee#410](https://github.com/invertase/notifee/issues/410) | Foreground service paused on screen lock (Samsung OneUI, ~6 seconds after screen off on battery) and killed immediately when the app is backgrounded (Xiaomi MIUI) | Vendor aggressive battery-saver and autostart policies suspend or terminate foreground services of apps not whitelisted in the OEM's protected-apps / autostart settings. Partially Doze-related on non-exempt `foregroundServiceType` values; mostly OEM-specific behavior catalogued at [dontkillmyapp.com](https://dontkillmyapp.com/). | **(1) Decision guide** — the [Timers: foreground service or `SET_ALARM_CLOCK`?](#timers-foreground-service-or-set_alarm_clock) section recommends the `SET_ALARM_CLOCK` trigger over a silent foreground service for rest, cooking, and recovery timer use cases. `setAlarmClock` is the same primitive the stock Clock app uses and is generally respected by vendor aggressive-kill policies. **(2) Foreground service use case matrix** — the [Foreground service use case guide](#foreground-service-use-case-guide) documents which `foregroundServiceType` values are Doze-CPU-exempt, which have type-specific timeouts, and the Google Play policy constraints that rule out misusing `mediaPlayback` for silent timers. **(3) `openPowerManagerSettings()` helper API** — deep-links the user to the correct vendor autostart / protected-apps screen on 16 manufacturers; whitelisting the app prevents both `BOOT_COMPLETED` suppression and background FGS kills. |
427
- | [invertase/notifee#734](https://github.com/invertase/notifee/issues/734) | Scheduled trigger notifications silently lost across a device reboot on OEM devices (Xiaomi MIUI, OnePlus, Huawei EMUI, Oppo ColorOS, Vivo FuntouchOS) | The vendor OS suppresses the `BOOT_COMPLETED` broadcast to apps the user has not manually whitelisted, so the library's `RebootBroadcastReceiver` never runs and persisted `AlarmManager` triggers are never re-armed after reboot. | **(1) `BOOT_COUNT` cold-start self-heal (code)** — on every app init, `InitProvider` compares `Settings.Global.BOOT_COUNT` against the last-known value in `SharedPreferences` and re-arms every persisted trigger on a background thread if a boot delta is detected, even when `BOOT_COMPLETED` was never delivered. Paired with a process-wide `AtomicBoolean` race guard in `NotifeeAlarmManager.rescheduleNotifications` that prevents double-advancement when the reboot receiver and the cold-start path race. **(2) `openPowerManagerSettings()` helper API** — the same vendor-settings deep-link used by #410, pointing the user at the autostart whitelist for defense in depth. |
428
- | [invertase/notifee#927](https://github.com/invertase/notifee/issues/927) | Custom sound passed via `displayNotification({ android: { sound, channelId }, ios: { sound } })` is ignored for **remote push notifications** (FCM/APNs) delivered while the app is in background or killed — the system default sound plays instead. Foreground delivery and **locally-scheduled notifications** (`displayNotification`, `createTriggerNotification`) are unaffected. | When a remote push arrives while the app is killed, the JavaScript layer never runs — the system tray item is drawn by the OS (Android system + Firebase SDK; iOS + APNs) before any Notifee code executes. On Android API 26+, the `NotificationChannel` sound is set once at channel creation and is immutable thereafter — `NotificationCompat.Builder.setSound()` is silently ignored when the builder has a `channelId`. On iOS, the Notification Service Extension only rewrites incoming push content when the payload contains a `notifee_options` key (see `NotifeeCoreExtensionHelper.m:43`); a plain APNs payload is delivered unmodified. | **Documentation only — the platform contract cannot be worked around at the library layer.** Recipes by platform: **(Android)** create the `NotificationChannel` with the desired sound at first-run (the channel sound is immutable; to change it the channel must be deleted and recreated under a new `channelId`), and configure `AndroidNotification.sound` in the FCM payload server-side so the system tray honors it for background pushes. As a heavier alternative, switch the backend to an FCM data-only payload and call `displayNotification()` from a headless task — the JS-side `android.sound` is then honored, but this trades simplicity for the cost of running JS on every push. **(iOS)** either set `aps.sound` directly in the APNs payload, or install the Notification Service Extension (see `docs/react-native/ios/remote-notification-support.mdx`) and ship the sound under `notifee_options.ios.sound` in the push payload. |
424
+ | Upstream issue | Symptom | Platform root cause | Fork mitigation |
425
+ | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
426
+ | [invertase/notifee#410](https://github.com/invertase/notifee/issues/410) | Foreground service paused on screen lock (Samsung OneUI, ~6 seconds after screen off on battery) and killed immediately when the app is backgrounded (Xiaomi MIUI) | Vendor aggressive battery-saver and autostart policies suspend or terminate foreground services of apps not whitelisted in the OEM's protected-apps / autostart settings. Partially Doze-related on non-exempt `foregroundServiceType` values; mostly OEM-specific behavior catalogued at [dontkillmyapp.com](https://dontkillmyapp.com/). | **(1) Decision guide** — the [Timers: foreground service or `SET_ALARM_CLOCK`?](#timers-foreground-service-or-set_alarm_clock) section recommends the `SET_ALARM_CLOCK` trigger over a silent foreground service for rest, cooking, and recovery timer use cases. `setAlarmClock` is the same primitive the stock Clock app uses and is generally respected by vendor aggressive-kill policies. **(2) Foreground service use case matrix** — the [Foreground service use case guide](#foreground-service-use-case-guide) documents which `foregroundServiceType` values are Doze-CPU-exempt, which have type-specific timeouts, and the Google Play policy constraints that rule out misusing `mediaPlayback` for silent timers. **(3) `openPowerManagerSettings()` helper API** — points users toward known vendor autostart / protected-apps screen candidates when available; these links are best-effort because firmware variants may move, block, or remove those settings activities. |
427
+ | [invertase/notifee#734](https://github.com/invertase/notifee/issues/734) | Scheduled trigger notifications silently lost across a device reboot on OEM devices (Xiaomi MIUI, OnePlus, Huawei EMUI, Oppo ColorOS, Vivo FuntouchOS) | The vendor OS suppresses the `BOOT_COMPLETED` broadcast to apps the user has not manually whitelisted, so the library's `RebootBroadcastReceiver` never runs and persisted `AlarmManager` triggers are never re-armed after reboot. | **(1) `BOOT_COUNT` cold-start self-heal (code)** — on every app init, `InitProvider` compares `Settings.Global.BOOT_COUNT` against the last-known value in `SharedPreferences` and re-arms every persisted trigger on a background thread if a boot delta is detected, even when `BOOT_COMPLETED` was never delivered. Paired with a process-wide `AtomicBoolean` race guard in `NotifeeAlarmManager.rescheduleNotifications` that prevents double-advancement when the reboot receiver and the cold-start path race. **(2) `openPowerManagerSettings()` helper API** — the same vendor-settings deep-link used by #410, pointing the user at the autostart whitelist for defense in depth. |
428
+ | [invertase/notifee#927](https://github.com/invertase/notifee/issues/927) | Custom sound passed via `displayNotification({ android: { sound, channelId }, ios: { sound } })` is ignored for **remote push notifications** (FCM/APNs) delivered while the app is in background or killed — the system default sound plays instead. Foreground delivery and **locally-scheduled notifications** (`displayNotification`, `createTriggerNotification`) are unaffected. | When a remote push arrives while the app is killed, the JavaScript layer never runs — the system tray item is drawn by the OS (Android system + Firebase SDK; iOS + APNs) before any Notifee code executes. On Android API 26+, the `NotificationChannel` sound is set once at channel creation and is immutable thereafter — `NotificationCompat.Builder.setSound()` is silently ignored when the builder has a `channelId`. On iOS, the Notification Service Extension only rewrites incoming push content when the payload contains a `notifee_options` key (see `NotifeeCoreExtensionHelper.m:43`); a plain APNs payload is delivered unmodified. | **Documentation only — the platform contract cannot be worked around at the library layer.** Recipes by platform: **(Android)** create the `NotificationChannel` with the desired sound at first-run (the channel sound is immutable; to change it the channel must be deleted and recreated under a new `channelId`), and configure `AndroidNotification.sound` in the FCM payload server-side so the system tray honors it for background pushes. As a heavier alternative, switch the backend to an FCM data-only payload and call `displayNotification()` from a headless task — the JS-side `android.sound` is then honored, but this trades simplicity for the cost of running JS on every push. **(iOS)** either set `aps.sound` directly in the APNs payload, or install the Notification Service Extension (see `docs/react-native/ios/remote-notification-support.mdx`) and ship the sound under `notifee_options.ios.sound` in the push payload. |
429
429
 
430
430
  Both mitigations are intentionally additive to the existing reboot-recovery and foreground-service code paths and do not replace the consumer's responsibility to prompt the user for battery-optimization exemption when the use case warrants it. For a complete vendor-by-vendor reference of autostart, battery-saver, and background-restriction behavior, see [dontkillmyapp.com](https://dontkillmyapp.com/).
431
431
 
@@ -526,7 +526,7 @@ The fork mitigates this with two layers that work together:
526
526
 
527
527
  **1. Automatic cold-start recovery.** On every app init, the library compares `Settings.Global.BOOT_COUNT` against the value recorded on the previous run. If a reboot has occurred since the last run — whether or not `BOOT_COMPLETED` was delivered to your app — the library re-arms every persisted trigger on a background thread. This means that on an OEM device where `BOOT_COMPLETED` was suppressed, simply opening your app (or having it cold-started by any other entry point: push notification, geofence, share intent) recovers all missed and upcoming alarms. Previously, opening the app alone did not recover them. This recovery runs unconditionally — it is not gated by the `notifee_init_warmup_enabled` metadata flag, because it is a correctness fix rather than a startup optimization.
528
528
 
529
- **2. Vendor settings helper APIs.** The existing `getPowerManagerInfo()` and `openPowerManagerSettings()` APIs let your app guide the user directly to the correct vendor settings screen (Xiaomi Autostart, Huawei Protected Apps, Oppo Startup Manager, and 13 more vendors) to whitelist the app. Once whitelisted, `BOOT_COMPLETED` is delivered normally on every reboot and exact alarm timing is preserved without waiting for the next app cold-start. The same whitelist also prevents the OS from killing your foreground service when the app is backgrounded — so this helper is the primary mitigation path for **both** trigger-notification reliability and foreground-service reliability on OEM devices.
529
+ **2. Vendor settings helper APIs.** The existing `getPowerManagerInfo()` and `openPowerManagerSettings()` APIs let your app guide the user toward known vendor settings candidates (Xiaomi Autostart, Huawei Protected Apps, Oppo Startup Manager, and 13 more vendors) to whitelist the app. These candidates are opened best-effort: Android may reject or fail to resolve a vendor-specific settings activity on some firmware variants, and the helper fails safely instead of crashing. Consumer apps do not need to inherit package-visibility `<queries>` declarations for these helpers, and the helper does not use the direct `ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` request path. Whitelisting the app in vendor settings can restore normal reboot delivery and reduce background foreground-service kills on affected devices, but this remains subject to OEM firmware and user/device policy — so this helper is a mitigation path for **both** trigger-notification reliability and foreground-service reliability on OEM devices.
530
530
 
531
531
  A typical integration that combines both layers looks like this:
532
532
 
@@ -8,7 +8,6 @@
8
8
  <uses-permission android:name="android.permission.INTERNET" />
9
9
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
10
10
  <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
11
- <uses-permission android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS" android:maxSdkVersion="30" />
12
11
  <!-- For Xiaomi devices to enable heads-up notifications as default (https://github.com/invertase/notifee/issues/296) -->
13
12
  <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" android:minSdkVersion="23" />
14
13
 
@@ -39,6 +39,7 @@ import app.notifee.core.event.ForegroundServiceEvent;
39
39
  import app.notifee.core.event.NotificationEvent;
40
40
  import app.notifee.core.interfaces.MethodCallResult;
41
41
  import app.notifee.core.model.NotificationModel;
42
+ import app.notifee.core.utility.ParcelableCompatReader;
42
43
 
43
44
  public class ForegroundService extends Service {
44
45
  private static final String TAG = "ForegroundService";
@@ -186,7 +187,8 @@ public class ForegroundService extends Service {
186
187
  if (extras != null) {
187
188
  // Hash code is sent to service to ensure it is kept the same
188
189
  int hashCode = extras.getInt("hashCode");
189
- Notification notification = extras.getParcelable("notification");
190
+ Notification notification =
191
+ ParcelableCompatReader.getParcelable(extras, "notification", Notification.class);
190
192
  Bundle bundle = extras.getBundle("notificationBundle");
191
193
 
192
194
  if (notification != null && bundle != null) {
@@ -37,6 +37,7 @@ import app.notifee.core.model.NotificationAndroidModel;
37
37
  import app.notifee.core.model.NotificationModel;
38
38
  import app.notifee.core.model.TimestampTriggerModel;
39
39
  import app.notifee.core.utility.AlarmUtils;
40
+ import app.notifee.core.utility.BundleValueReader;
40
41
  import app.notifee.core.utility.ExtendedListenableFuture;
41
42
  import app.notifee.core.utility.ObjectUtils;
42
43
  import com.google.common.util.concurrent.FutureCallback;
@@ -195,7 +196,8 @@ class NotifeeAlarmManager {
195
196
  NotificationManager.displayNotification(notificationModel, triggerBundle),
196
197
  voidDisplayedNotification -> {
197
198
  if (triggerBundle.containsKey("repeatFrequency")
198
- && ObjectUtils.getInt(triggerBundle.get("repeatFrequency")) != -1) {
199
+ && BundleValueReader.getIntPreserving(triggerBundle, "repeatFrequency")
200
+ != -1) {
199
201
  TimestampTriggerModel trigger =
200
202
  TimestampTriggerModel.fromBundle(triggerBundle);
201
203
  // scheduleTimestampTriggerNotification() calls setNextTimestamp()
@@ -424,7 +426,7 @@ class NotifeeAlarmManager {
424
426
  NotificationModel notificationModel =
425
427
  NotificationModel.fromBundle(ObjectUtils.bytesToBundle(notificationBytes));
426
428
 
427
- int triggerType = ObjectUtils.getInt(triggerBundle.get("type"));
429
+ int triggerType = BundleValueReader.getIntPreserving(triggerBundle, "type");
428
430
 
429
431
  switch (triggerType) {
430
432
  case 0:
@@ -23,10 +23,12 @@ import static app.notifee.core.event.NotificationEvent.TYPE_ACTION_PRESS;
23
23
  import static app.notifee.core.event.NotificationEvent.TYPE_PRESS;
24
24
  import static java.lang.Integer.parseInt;
25
25
 
26
+ import android.Manifest;
26
27
  import android.app.Notification;
27
28
  import android.app.PendingIntent;
28
29
  import android.content.Context;
29
30
  import android.content.Intent;
31
+ import android.content.pm.PackageManager;
30
32
  import android.graphics.Bitmap;
31
33
  import android.net.Uri;
32
34
  import android.os.Build;
@@ -59,6 +61,7 @@ import app.notifee.core.model.NotificationAndroidPressActionModel;
59
61
  import app.notifee.core.model.NotificationAndroidStyleModel;
60
62
  import app.notifee.core.model.NotificationModel;
61
63
  import app.notifee.core.model.TimestampTriggerModel;
64
+ import app.notifee.core.utility.BundleValueReader;
62
65
  import app.notifee.core.utility.ExtendedListenableFuture;
63
66
  import app.notifee.core.utility.IntentUtils;
64
67
  import app.notifee.core.utility.ObjectUtils;
@@ -166,18 +169,7 @@ class NotificationManager {
166
169
  // so tapping the notification opens the app (defense-in-depth for paths that
167
170
  // bypass the TS validator).
168
171
  // 2. pressAction has the opt-out sentinel id: user explicitly passed
169
- // pressAction: null in JS — pass null to createIntent so no launch intent
170
- // is created (non-tappable notification).
171
- // 3. pressAction is a normal bundle: pass through unchanged.
172
- // Resolve the effective pressAction bundle for the content intent.
173
- // Three cases:
174
- // 1. pressAction is null (absent from bundle, e.g. trigger rehydrated from Room DB
175
- // after app kill): synthesize default { id:'default', launchActivity:'default' }
176
- // so tapping the notification opens the app (defense-in-depth for paths that
177
- // bypass the TS validator).
178
- // 2. pressAction has the opt-out sentinel id: user explicitly passed
179
- // pressAction: null in JS — pass null to createIntent so no launch intent
180
- // is created (non-tappable notification).
172
+ // pressAction: null in JS, so no content intent is created.
181
173
  // 3. pressAction is a normal bundle: pass through unchanged.
182
174
  //
183
175
  // pressActionForIntent → used for creating the launch intent (or null for opt-out)
@@ -203,23 +195,25 @@ class NotificationManager {
203
195
 
204
196
  int targetSdkVersion =
205
197
  ContextHolder.getApplicationContext().getApplicationInfo().targetSdkVersion;
206
- if (targetSdkVersion >= Build.VERSION_CODES.S
207
- && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
208
- builder.setContentIntent(
209
- NotificationPendingIntent.createIntent(
210
- notificationModel.getHashCode(),
211
- pressActionForIntent,
212
- TYPE_PRESS,
213
- new String[] {"notification", "pressAction"},
214
- notificationModel.toBundle(),
215
- pressActionForExtras));
216
- } else {
217
- builder.setContentIntent(
218
- ReceiverService.createIntent(
219
- ReceiverService.PRESS_INTENT,
220
- new String[] {"notification", "pressAction"},
221
- notificationModel.toBundle(),
222
- pressActionForIntent));
198
+ if (pressActionForIntent != null) {
199
+ if (targetSdkVersion >= Build.VERSION_CODES.S
200
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
201
+ builder.setContentIntent(
202
+ NotificationPendingIntent.createIntent(
203
+ notificationModel.getHashCode(),
204
+ pressActionForIntent,
205
+ TYPE_PRESS,
206
+ new String[] {"notification", "pressAction"},
207
+ notificationModel.toBundle(),
208
+ pressActionForExtras));
209
+ } else {
210
+ builder.setContentIntent(
211
+ ReceiverService.createIntent(
212
+ ReceiverService.PRESS_INTENT,
213
+ new String[] {"notification", "pressAction"},
214
+ notificationModel.toBundle(),
215
+ pressActionForIntent));
216
+ }
223
217
  }
224
218
 
225
219
  if (notificationModel.getTitle() != null) {
@@ -248,9 +242,7 @@ class NotificationManager {
248
242
 
249
243
  builder.setColorized(androidModel.getColorized());
250
244
 
251
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
252
- builder.setChronometerCountDown(androidModel.getChronometerCountDown());
253
- }
245
+ builder.setChronometerCountDown(androidModel.getChronometerCountDown());
254
246
 
255
247
  if (androidModel.getGroup() != null) {
256
248
  builder.setGroup(androidModel.getGroup());
@@ -716,7 +708,18 @@ class NotificationManager {
716
708
  Trace.endSection();
717
709
  }
718
710
  } else {
719
- NotificationManagerCompat.from(getApplicationContext())
711
+ Context context = getApplicationContext();
712
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
713
+ && context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
714
+ != PackageManager.PERMISSION_GRANTED) {
715
+ String message =
716
+ "POST_NOTIFICATIONS permission is not granted. Notification was not"
717
+ + " displayed.";
718
+ Logger.w(TAG, message);
719
+ return Futures.immediateFailedFuture(new SecurityException(message));
720
+ }
721
+
722
+ NotificationManagerCompat.from(context)
720
723
  .notify(androidBundle.getTag(), hashCode, notification);
721
724
  }
722
725
 
@@ -733,7 +736,7 @@ class NotificationManager {
733
736
 
734
737
  static ListenableFuture<Void> createTriggerNotification(
735
738
  NotificationModel notificationModel, Bundle triggerBundle) {
736
- int triggerType = ObjectUtils.getInt(triggerBundle.get("type"));
739
+ int triggerType = BundleValueReader.getIntPreserving(triggerBundle, "type");
737
740
  ListenableFuture<Void> scheduleFuture;
738
741
  switch (triggerType) {
739
742
  case 0:
@@ -873,10 +876,6 @@ class NotificationManager {
873
876
  () -> {
874
877
  List<Bundle> notifications = new ArrayList<Bundle>();
875
878
 
876
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
877
- return notifications;
878
- }
879
-
880
879
  android.app.NotificationManager notificationManager =
881
880
  (android.app.NotificationManager)
882
881
  getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
@@ -896,19 +895,19 @@ class NotificationManager {
896
895
  notificationBundle = new Bundle();
897
896
  notificationBundle.putString("id", "" + sbNotification.getId());
898
897
 
899
- Object title = extras.get(Notification.EXTRA_TITLE);
898
+ Object title = BundleValueReader.getValue(extras, Notification.EXTRA_TITLE);
900
899
 
901
900
  if (title != null) {
902
901
  notificationBundle.putString("title", title.toString());
903
902
  }
904
903
 
905
- Object text = extras.get(Notification.EXTRA_TEXT);
904
+ Object text = BundleValueReader.getValue(extras, Notification.EXTRA_TEXT);
906
905
 
907
906
  if (text != null) {
908
907
  notificationBundle.putString("body", text.toString());
909
908
  }
910
909
 
911
- Object subtitle = extras.get(Notification.EXTRA_SUB_TEXT);
910
+ Object subtitle = BundleValueReader.getValue(extras, Notification.EXTRA_SUB_TEXT);
912
911
 
913
912
  if (subtitle != null) {
914
913
  notificationBundle.putString("subtitle", subtitle.toString());
@@ -927,7 +926,8 @@ class NotificationManager {
927
926
 
928
927
  displayNotificationBundle.putString("id", "" + sbNotification.getId());
929
928
  } else {
930
- displayNotificationBundle.putString("id", "" + notificationBundle.get("id"));
929
+ displayNotificationBundle.putString(
930
+ "id", String.valueOf(BundleValueReader.getValue(notificationBundle, "id")));
931
931
  }
932
932
 
933
933
  if (triggerBundle != null) {
@@ -972,7 +972,7 @@ class NotificationManager {
972
972
  if (!shouldIncludeInData(key)) {
973
973
  continue;
974
974
  }
975
- Object value = extras.get(key);
975
+ Object value = BundleValueReader.getValue(extras, key);
976
976
  if (value != null) {
977
977
  dataBundle.putString(key, value.toString());
978
978
  }
@@ -21,6 +21,7 @@ import static app.notifee.core.event.NotificationEvent.TYPE_ACTION_PRESS;
21
21
  import static app.notifee.core.event.NotificationEvent.TYPE_DISMISSED;
22
22
  import static app.notifee.core.event.NotificationEvent.TYPE_PRESS;
23
23
 
24
+ import android.annotation.SuppressLint;
24
25
  import android.app.PendingIntent;
25
26
  import android.app.Service;
26
27
  import android.content.Context;
@@ -171,10 +172,6 @@ public class ReceiverService extends Service {
171
172
  }
172
173
  }
173
174
 
174
- // ACTION_CLOSE_SYSTEM_DIALOGS is deprecated since API 31, but is still needed on API < 31
175
- // to close the notification drawer after an action press. Already guarded with Build.VERSION
176
- // check.
177
- @SuppressWarnings("deprecation")
178
175
  private void onActionPressIntent(Intent intent) {
179
176
  Bundle notification = intent.getBundleExtra("notification");
180
177
  Bundle pressAction = intent.getBundleExtra("pressAction");
@@ -221,16 +218,24 @@ public class ReceiverService extends Service {
221
218
  mainComponent,
222
219
  pressActionBundle.getLaunchActivityFlags());
223
220
 
224
- int targetSdkVersion =
225
- ContextHolder.getApplicationContext().getApplicationInfo().targetSdkVersion;
221
+ closeSystemDialogsBestEffort();
222
+ }
223
+ }
226
224
 
227
- // Close notification drawer if application SDK is Android 11 and lower
228
- // See
229
- // https://developer.android.com/about/versions/12/behavior-changes-all#close-system-dialogs
230
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
231
- ContextHolder.getApplicationContext()
232
- .sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
233
- }
225
+ @SuppressWarnings("deprecation")
226
+ @SuppressLint("MissingPermission")
227
+ private void closeSystemDialogsBestEffort() {
228
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
229
+ return;
230
+ }
231
+
232
+ // BROADCAST_CLOSE_SYSTEM_DIALOGS is protected/system-only, so the library intentionally does
233
+ // not declare it. Keep this pre-Android 12 legacy broadcast best-effort; Android 12+ skips it.
234
+ try {
235
+ ContextHolder.getApplicationContext()
236
+ .sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
237
+ } catch (SecurityException e) {
238
+ Logger.w(TAG, "Unable to close system dialogs with legacy broadcast", e);
234
239
  }
235
240
  }
236
241
 
@@ -43,10 +43,7 @@ public abstract class NotifeeCoreDatabase extends RoomDatabase {
43
43
  static final ListeningExecutorService databaseWriteListeningExecutor =
44
44
  MoreExecutors.listeningDecorator(databaseWriteExecutor);
45
45
 
46
- /**
47
- * Migrate from: version 1 to version 2 - where the {@link WorkDataEntity} has an extra field:
48
- * alert
49
- */
46
+ /** Migrate from version 1 to version 2 by adding the with_alarm_manager column. */
50
47
  @VisibleForTesting
51
48
  static final Migration MIGRATION_1_2 =
52
49
  new Migration(1, 2) {
@@ -23,7 +23,7 @@ import androidx.annotation.NonNull;
23
23
  import androidx.annotation.Nullable;
24
24
  import androidx.core.app.NotificationCompat;
25
25
  import androidx.core.app.NotificationManagerCompat;
26
- import app.notifee.core.utility.ObjectUtils;
26
+ import app.notifee.core.utility.BundleValueReader;
27
27
  import java.util.ArrayList;
28
28
  import java.util.Objects;
29
29
 
@@ -72,11 +72,8 @@ public class ChannelModel {
72
72
  }
73
73
 
74
74
  public Integer getImportance() {
75
- if (mChannelBundle.containsKey("importance")) {
76
- return ObjectUtils.getInt(mChannelBundle.get("importance"));
77
- }
78
-
79
- return NotificationManagerCompat.IMPORTANCE_DEFAULT;
75
+ return BundleValueReader.getIntPreserving(
76
+ mChannelBundle, "importance", NotificationManagerCompat.IMPORTANCE_DEFAULT);
80
77
  }
81
78
 
82
79
  public @Nullable Integer getLightColor() {
@@ -88,14 +85,11 @@ public class ChannelModel {
88
85
  }
89
86
 
90
87
  public int getVisibility() {
91
- if (mChannelBundle.containsKey("visibility")) {
92
- return ObjectUtils.getInt(mChannelBundle.get("visibility"));
93
- }
94
-
95
- return NotificationCompat.VISIBILITY_PRIVATE;
88
+ return BundleValueReader.getIntPreserving(
89
+ mChannelBundle, "visibility", NotificationCompat.VISIBILITY_PRIVATE);
96
90
  }
97
91
 
98
- // Bundle.getParcelableArrayList returns ArrayList<?>, so the (Integer) cast is inherently
92
+ // BundleValueReader.getArrayListValue returns ArrayList<?>, so the (Integer) cast is inherently
99
93
  // unchecked but correct for our serialization format.
100
94
  @SuppressWarnings("unchecked")
101
95
  public long[] getVibrationPattern() {
@@ -104,7 +98,8 @@ public class ChannelModel {
104
98
  }
105
99
 
106
100
  ArrayList<?> vibrationPattern =
107
- Objects.requireNonNull(mChannelBundle.getParcelableArrayList("vibrationPattern"));
101
+ Objects.requireNonNull(
102
+ BundleValueReader.getArrayListValue(mChannelBundle, "vibrationPattern"));
108
103
 
109
104
  long[] vibrateArray = new long[vibrationPattern.size()];
110
105
 
@@ -20,7 +20,7 @@ package app.notifee.core.model;
20
20
  import android.os.Bundle;
21
21
  import androidx.annotation.NonNull;
22
22
  import app.notifee.core.Logger;
23
- import app.notifee.core.utility.ObjectUtils;
23
+ import app.notifee.core.utility.BundleValueReader;
24
24
  import java.util.concurrent.TimeUnit;
25
25
 
26
26
  public class IntervalTriggerModel {
@@ -54,10 +54,6 @@ public class IntervalTriggerModel {
54
54
  }
55
55
 
56
56
  public int getInterval() {
57
- if (mIntervalTriggerBundle.containsKey("interval")) {
58
- return ObjectUtils.getInt(mIntervalTriggerBundle.get("interval"));
59
- }
60
-
61
- return -1;
57
+ return BundleValueReader.getIntPreserving(mIntervalTriggerBundle, "interval", -1);
62
58
  }
63
59
  }
@@ -77,8 +77,7 @@ public class NotificationAndroidActionModel {
77
77
  * @return RemoteInput
78
78
  */
79
79
  public @Nullable RemoteInput getRemoteInput(NotificationCompat.Action.Builder actionBuilder) {
80
- if (mNotificationAndroidActionBundle.containsKey("input")
81
- && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT_WATCH) {
80
+ if (mNotificationAndroidActionBundle.containsKey("input")) {
82
81
  Bundle inputBundle =
83
82
  Objects.requireNonNull(mNotificationAndroidActionBundle.getBundle("input"));
84
83
 
@@ -29,7 +29,9 @@ import androidx.annotation.RequiresApi;
29
29
  import androidx.core.app.NotificationCompat;
30
30
  import androidx.core.app.NotificationManagerCompat;
31
31
  import app.notifee.core.Logger;
32
+ import app.notifee.core.utility.BundleValueReader;
32
33
  import app.notifee.core.utility.ObjectUtils;
34
+ import app.notifee.core.utility.ParcelableCompatReader;
33
35
  import app.notifee.core.utility.ResourceUtils;
34
36
  import java.util.ArrayList;
35
37
  import java.util.Objects;
@@ -50,7 +52,9 @@ public class NotificationAndroidModel {
50
52
  public @Nullable ArrayList<NotificationAndroidActionModel> getActions() {
51
53
  if (mNotificationAndroidBundle.containsKey("actions")) {
52
54
  ArrayList<Bundle> actionBundles =
53
- Objects.requireNonNull(mNotificationAndroidBundle.getParcelableArrayList("actions"));
55
+ Objects.requireNonNull(
56
+ ParcelableCompatReader.getParcelableArrayList(
57
+ mNotificationAndroidBundle, "actions", Bundle.class));
54
58
  ArrayList<NotificationAndroidActionModel> actions = new ArrayList<>(actionBundles.size());
55
59
 
56
60
  for (Bundle actionBundle : actionBundles) {
@@ -72,7 +76,8 @@ public class NotificationAndroidModel {
72
76
 
73
77
  ArrayList<?> foregroundServiceTypesArrayList =
74
78
  Objects.requireNonNull(
75
- mNotificationAndroidBundle.getParcelableArrayList("foregroundServiceTypes"));
79
+ BundleValueReader.getArrayListValue(
80
+ mNotificationAndroidBundle, "foregroundServiceTypes"));
76
81
 
77
82
  if (foregroundServiceTypesArrayList.isEmpty()) {
78
83
  Logger.w(TAG, "foregroundServiceTypes is empty; treating as absent");
@@ -137,12 +142,8 @@ public class NotificationAndroidModel {
137
142
  * @return Integer
138
143
  */
139
144
  public @Nullable Integer getBadgeIconType() {
140
- if (mNotificationAndroidBundle.containsKey("badgeIconType")) {
141
-
142
- return ObjectUtils.getInt(mNotificationAndroidBundle.get("badgeIconType"));
143
- }
144
-
145
- return NotificationCompat.BADGE_ICON_LARGE;
145
+ return BundleValueReader.getIntPreserving(
146
+ mNotificationAndroidBundle, "badgeIconType", NotificationCompat.BADGE_ICON_LARGE);
146
147
  }
147
148
 
148
149
  /**
@@ -266,11 +267,8 @@ public class NotificationAndroidModel {
266
267
  * @return int
267
268
  */
268
269
  public int getGroupAlertBehaviour() {
269
- if (mNotificationAndroidBundle.containsKey("groupAlertBehavior")) {
270
- return ObjectUtils.getInt(mNotificationAndroidBundle.get("groupAlertBehavior"));
271
- }
272
-
273
- return NotificationCompat.GROUP_ALERT_ALL;
270
+ return BundleValueReader.getIntPreserving(
271
+ mNotificationAndroidBundle, "groupAlertBehavior", NotificationCompat.GROUP_ALERT_ALL);
274
272
  }
275
273
 
276
274
  /**
@@ -327,14 +325,15 @@ public class NotificationAndroidModel {
327
325
  *
328
326
  * @return ArrayList<Integer>
329
327
  */
330
- // Bundle.getParcelableArrayList returns ArrayList<?>, so element casts to String/Integer
328
+ // BundleValueReader.getArrayListValue returns ArrayList<?>, so element casts to String/Integer
331
329
  // are inherently unchecked but correct for our serialization format.
332
330
  @SuppressWarnings("unchecked")
333
331
  public @Nullable ArrayList<Integer> getLights() {
334
332
  if (mNotificationAndroidBundle.containsKey("lights")) {
335
333
  try {
336
334
  ArrayList<?> lightList =
337
- Objects.requireNonNull(mNotificationAndroidBundle.getParcelableArrayList("lights"));
335
+ Objects.requireNonNull(
336
+ BundleValueReader.getArrayListValue(mNotificationAndroidBundle, "lights"));
338
337
  String rawColor = (String) lightList.get(0);
339
338
 
340
339
  ArrayList<Integer> lights = new ArrayList<>(3);
@@ -368,7 +367,7 @@ public class NotificationAndroidModel {
368
367
  */
369
368
  public Integer getNumber() {
370
369
  if (mNotificationAndroidBundle.containsKey("badgeCount")) {
371
- return ObjectUtils.getInt(mNotificationAndroidBundle.get("badgeCount"));
370
+ return BundleValueReader.getIntPreserving(mNotificationAndroidBundle, "badgeCount");
372
371
  }
373
372
 
374
373
  return null;
@@ -403,7 +402,8 @@ public class NotificationAndroidModel {
403
402
  }
404
403
 
405
404
  ArrayList<?> flagsArrayList =
406
- Objects.requireNonNull(mNotificationAndroidBundle.getParcelableArrayList("flags"));
405
+ Objects.requireNonNull(
406
+ BundleValueReader.getArrayListValue(mNotificationAndroidBundle, "flags"));
407
407
 
408
408
  int[] flagsArray = new int[flagsArrayList.size()];
409
409
 
@@ -469,7 +469,11 @@ public class NotificationAndroidModel {
469
469
  return NotificationCompat.PRIORITY_DEFAULT;
470
470
  }
471
471
 
472
- int importance = ObjectUtils.getInt(mNotificationAndroidBundle.get("importance"));
472
+ int importance =
473
+ BundleValueReader.getIntPreserving(
474
+ mNotificationAndroidBundle,
475
+ "importance",
476
+ NotificationManagerCompat.IMPORTANCE_DEFAULT);
473
477
  switch (importance) {
474
478
  case NotificationManagerCompat.IMPORTANCE_HIGH:
475
479
  return NotificationCompat.PRIORITY_HIGH;
@@ -494,8 +498,8 @@ public class NotificationAndroidModel {
494
498
  Objects.requireNonNull(mNotificationAndroidBundle.getBundle("progress"));
495
499
 
496
500
  return new AndroidProgress(
497
- ObjectUtils.getInt(progressBundle.get("max")),
498
- ObjectUtils.getInt(progressBundle.get("current")),
501
+ BundleValueReader.getIntPreserving(progressBundle, "max"),
502
+ BundleValueReader.getIntPreserving(progressBundle, "current"),
499
503
  progressBundle.getBoolean("indeterminate", false));
500
504
  }
501
505
 
@@ -607,7 +611,7 @@ public class NotificationAndroidModel {
607
611
  */
608
612
  public @Nullable Long getTimeoutAfter() {
609
613
  if (mNotificationAndroidBundle.containsKey("timeoutAfter")) {
610
- return ObjectUtils.getLong(mNotificationAndroidBundle.get("timeoutAfter"));
614
+ return BundleValueReader.getLongPreserving(mNotificationAndroidBundle, "timeoutAfter");
611
615
  }
612
616
 
613
617
  return null;
@@ -635,7 +639,7 @@ public class NotificationAndroidModel {
635
639
  *
636
640
  * @return long[]
637
641
  */
638
- // Bundle.getParcelableArrayList returns ArrayList<?>, so the (Integer) cast is inherently
642
+ // BundleValueReader.getArrayListValue returns ArrayList<?>, so the (Integer) cast is inherently
639
643
  // unchecked but correct for our serialization format.
640
644
  @SuppressWarnings("unchecked")
641
645
  public long[] getVibrationPattern() {
@@ -645,7 +649,7 @@ public class NotificationAndroidModel {
645
649
 
646
650
  ArrayList<?> vibrationPattern =
647
651
  Objects.requireNonNull(
648
- mNotificationAndroidBundle.getParcelableArrayList("vibrationPattern"));
652
+ BundleValueReader.getArrayListValue(mNotificationAndroidBundle, "vibrationPattern"));
649
653
 
650
654
  long[] vibrateArray = new long[vibrationPattern.size()];
651
655
 
@@ -663,11 +667,8 @@ public class NotificationAndroidModel {
663
667
  * @return int
664
668
  */
665
669
  public int getVisibility() {
666
- if (mNotificationAndroidBundle.containsKey("visibility")) {
667
- return ObjectUtils.getInt(mNotificationAndroidBundle.get("visibility"));
668
- }
669
-
670
- return NotificationCompat.VISIBILITY_PRIVATE;
670
+ return BundleValueReader.getIntPreserving(
671
+ mNotificationAndroidBundle, "visibility", NotificationCompat.VISIBILITY_PRIVATE);
671
672
  }
672
673
 
673
674
  /**
@@ -676,11 +677,7 @@ public class NotificationAndroidModel {
676
677
  * @return long
677
678
  */
678
679
  public long getTimestamp() {
679
- if (mNotificationAndroidBundle.containsKey("timestamp")) {
680
- return ObjectUtils.getLong(mNotificationAndroidBundle.get("timestamp"));
681
- }
682
-
683
- return -1;
680
+ return BundleValueReader.getLongPreserving(mNotificationAndroidBundle, "timestamp", -1L);
684
681
  }
685
682
 
686
683
  public static class AndroidProgress {