react-native-notify-kit 9.2.1 → 9.4.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 (50) hide show
  1. package/README.md +85 -7
  2. package/android/build.gradle +6 -5
  3. package/android/proguard-rules.pro +1 -0
  4. package/android/src/main/baseline-prof.txt +382 -0
  5. package/android/src/main/java/app/notifee/core/ForegroundService.java +269 -80
  6. package/android/src/main/java/app/notifee/core/InitProvider.java +48 -5
  7. package/android/src/main/java/app/notifee/core/Logger.java +5 -0
  8. package/android/src/main/java/app/notifee/core/NotificationManager.java +217 -150
  9. package/android/src/main/java/app/notifee/core/NotificationPendingIntent.java +10 -0
  10. package/android/src/main/java/app/notifee/core/WarmupHelper.java +84 -0
  11. package/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java +17 -1
  12. package/android/src/main/kotlin/io/invertase/notifee/NotifeeApiModule.kt +30 -0
  13. package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +182 -0
  14. package/android/src/test/java/app/notifee/core/NotificationPendingIntentTest.java +108 -0
  15. package/dist/NotifeeApiModule.d.ts +1 -0
  16. package/dist/NotifeeApiModule.js +6 -0
  17. package/dist/NotifeeApiModule.js.map +1 -1
  18. package/dist/specs/NativeNotifeeModule.d.ts +1 -0
  19. package/dist/specs/NativeNotifeeModule.js.map +1 -1
  20. package/dist/types/Module.d.ts +19 -0
  21. package/dist/types/NotificationAndroid.d.ts +66 -4
  22. package/dist/types/NotificationAndroid.js +43 -0
  23. package/dist/types/NotificationAndroid.js.map +1 -1
  24. package/dist/utils/validate.d.ts +1 -0
  25. package/dist/utils/validate.js +10 -4
  26. package/dist/utils/validate.js.map +1 -1
  27. package/dist/validators/validateAndroidChannel.js +3 -3
  28. package/dist/validators/validateAndroidChannel.js.map +1 -1
  29. package/dist/validators/validateAndroidNotification.js +58 -14
  30. package/dist/validators/validateAndroidNotification.js.map +1 -1
  31. package/dist/validators/validateIOSPermissions.js +1 -1
  32. package/dist/validators/validateIOSPermissions.js.map +1 -1
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/ios/NotifeeCore/NotifeeCore+NSNotificationCenter.m +5 -1
  36. package/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m +15 -15
  37. package/ios/NotifeeCore/NotifeeCore.m +41 -14
  38. package/ios/NotifeeCore/NotifeeCoreDelegateHolder.m +32 -18
  39. package/ios/NotifeeCore/NotifeeCoreExtensionHelper.m +7 -2
  40. package/ios/RNNotifee/NotifeeApiModule.mm +33 -11
  41. package/package.json +1 -1
  42. package/src/NotifeeApiModule.ts +7 -0
  43. package/src/specs/NativeNotifeeModule.ts +1 -0
  44. package/src/types/Module.ts +20 -0
  45. package/src/types/NotificationAndroid.ts +70 -4
  46. package/src/utils/validate.ts +12 -4
  47. package/src/validators/validateAndroidChannel.ts +3 -3
  48. package/src/validators/validateAndroidNotification.ts +64 -11
  49. package/src/validators/validateIOSPermissions.ts +1 -1
  50. package/src/version.ts +1 -1
@@ -31,6 +31,7 @@ import android.graphics.Bitmap;
31
31
  import android.net.Uri;
32
32
  import android.os.Build;
33
33
  import android.os.Bundle;
34
+ import android.os.Trace;
34
35
  import android.service.notification.StatusBarNotification;
35
36
  import androidx.annotation.NonNull;
36
37
  import androidx.concurrent.futures.CallbackToFutureAdapter;
@@ -103,156 +104,204 @@ class NotificationManager {
103
104
  */
104
105
  Callable<NotificationCompat.Builder> builderCallable =
105
106
  () -> {
106
- Boolean hasCustomSound = false;
107
- NotificationCompat.Builder builder =
108
- new NotificationCompat.Builder(getApplicationContext(), androidModel.getChannelId());
109
-
110
- // must always keep at top
111
- builder.setExtras(notificationModel.getData());
112
-
113
- builder.setDeleteIntent(
114
- ReceiverService.createIntent(
115
- ReceiverService.DELETE_INTENT,
116
- new String[] {"notification"},
117
- notificationModel.toBundle()));
118
- int targetSdkVersion =
119
- ContextHolder.getApplicationContext().getApplicationInfo().targetSdkVersion;
120
- if (targetSdkVersion >= Build.VERSION_CODES.S
121
- && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
122
- builder.setContentIntent(
123
- NotificationPendingIntent.createIntent(
124
- notificationModel.getHashCode(),
125
- androidModel.getPressAction(),
126
- TYPE_PRESS,
127
- new String[] {"notification", "pressAction"},
128
- notificationModel.toBundle(),
129
- androidModel.getPressAction()));
130
- } else {
131
- builder.setContentIntent(
107
+ Trace.beginSection("notifee:buildNotification");
108
+ try {
109
+ Boolean hasCustomSound = false;
110
+ NotificationCompat.Builder builder =
111
+ new NotificationCompat.Builder(
112
+ getApplicationContext(), androidModel.getChannelId());
113
+
114
+ // must always keep at top
115
+ builder.setExtras(notificationModel.getData());
116
+
117
+ builder.setDeleteIntent(
132
118
  ReceiverService.createIntent(
133
- ReceiverService.PRESS_INTENT,
134
- new String[] {"notification", "pressAction"},
135
- notificationModel.toBundle(),
136
- androidModel.getPressAction()));
137
- }
119
+ ReceiverService.DELETE_INTENT,
120
+ new String[] {"notification"},
121
+ notificationModel.toBundle()));
122
+ // Resolve the effective pressAction bundle for the content intent.
123
+ // Three cases:
124
+ // 1. pressAction is null (absent from bundle, e.g. trigger rehydrated from Room DB
125
+ // after app kill): synthesize default { id:'default', launchActivity:'default' }
126
+ // so tapping the notification opens the app (defense-in-depth for paths that
127
+ // bypass the TS validator).
128
+ // 2. pressAction has the opt-out sentinel id: user explicitly passed
129
+ // pressAction: null in JS — pass null to createIntent so no launch intent
130
+ // is created (non-tappable notification).
131
+ // 3. pressAction is a normal bundle: pass through unchanged.
132
+ // Resolve the effective pressAction bundle for the content intent.
133
+ // Three cases:
134
+ // 1. pressAction is null (absent from bundle, e.g. trigger rehydrated from Room DB
135
+ // after app kill): synthesize default { id:'default', launchActivity:'default' }
136
+ // so tapping the notification opens the app (defense-in-depth for paths that
137
+ // bypass the TS validator).
138
+ // 2. pressAction has the opt-out sentinel id: user explicitly passed
139
+ // pressAction: null in JS — pass null to createIntent so no launch intent
140
+ // is created (non-tappable notification).
141
+ // 3. pressAction is a normal bundle: pass through unchanged.
142
+ //
143
+ // pressActionForIntent → used for creating the launch intent (or null for opt-out)
144
+ // pressActionForExtras → used in the receiver intent extras (event payload);
145
+ // null for cases 1 & 2 to avoid leaking synthesized defaults
146
+ // or the sentinel id into the JS event.
147
+ Bundle pressActionForIntent = androidModel.getPressAction();
148
+ Bundle pressActionForExtras = pressActionForIntent;
149
+ if (pressActionForIntent == null) {
150
+ // Case 1: absent — synthesize default for launch, null for extras
151
+ pressActionForIntent = new Bundle();
152
+ pressActionForIntent.putString("id", "default");
153
+ pressActionForIntent.putString("launchActivity", "default");
154
+ // pressActionForExtras stays null: the original notification didn't have
155
+ // pressAction, so the event shouldn't either.
156
+ } else if (NotificationPendingIntent.PRESS_ACTION_OPT_OUT_ID.equals(
157
+ pressActionForIntent.getString("id"))) {
158
+ // Case 2: explicit opt-out sentinel — no launch intent, no sentinel in extras
159
+ pressActionForIntent = null;
160
+ pressActionForExtras = null;
161
+ }
162
+ // Case 3: normal pressAction — both variables point to the original bundle
163
+
164
+ int targetSdkVersion =
165
+ ContextHolder.getApplicationContext().getApplicationInfo().targetSdkVersion;
166
+ if (targetSdkVersion >= Build.VERSION_CODES.S
167
+ && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
168
+ builder.setContentIntent(
169
+ NotificationPendingIntent.createIntent(
170
+ notificationModel.getHashCode(),
171
+ pressActionForIntent,
172
+ TYPE_PRESS,
173
+ new String[] {"notification", "pressAction"},
174
+ notificationModel.toBundle(),
175
+ pressActionForExtras));
176
+ } else {
177
+ builder.setContentIntent(
178
+ ReceiverService.createIntent(
179
+ ReceiverService.PRESS_INTENT,
180
+ new String[] {"notification", "pressAction"},
181
+ notificationModel.toBundle(),
182
+ pressActionForIntent));
183
+ }
138
184
 
139
- if (notificationModel.getTitle() != null) {
140
- builder.setContentTitle(TextUtils.fromHtml(notificationModel.getTitle()));
141
- }
185
+ if (notificationModel.getTitle() != null) {
186
+ builder.setContentTitle(TextUtils.fromHtml(notificationModel.getTitle()));
187
+ }
142
188
 
143
- if (notificationModel.getSubTitle() != null) {
144
- builder.setSubText(TextUtils.fromHtml(notificationModel.getSubTitle()));
145
- }
189
+ if (notificationModel.getSubTitle() != null) {
190
+ builder.setSubText(TextUtils.fromHtml(notificationModel.getSubTitle()));
191
+ }
146
192
 
147
- if (notificationModel.getBody() != null) {
148
- builder.setContentText(TextUtils.fromHtml(notificationModel.getBody()));
149
- }
193
+ if (notificationModel.getBody() != null) {
194
+ builder.setContentText(TextUtils.fromHtml(notificationModel.getBody()));
195
+ }
150
196
 
151
- if (androidModel.getBadgeIconType() != null) {
152
- builder.setBadgeIconType(androidModel.getBadgeIconType());
153
- }
197
+ if (androidModel.getBadgeIconType() != null) {
198
+ builder.setBadgeIconType(androidModel.getBadgeIconType());
199
+ }
154
200
 
155
- if (androidModel.getCategory() != null) {
156
- builder.setCategory(androidModel.getCategory());
157
- }
201
+ if (androidModel.getCategory() != null) {
202
+ builder.setCategory(androidModel.getCategory());
203
+ }
158
204
 
159
- if (androidModel.getColor() != null) {
160
- builder.setColor(androidModel.getColor());
161
- }
205
+ if (androidModel.getColor() != null) {
206
+ builder.setColor(androidModel.getColor());
207
+ }
162
208
 
163
- builder.setColorized(androidModel.getColorized());
209
+ builder.setColorized(androidModel.getColorized());
164
210
 
165
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
166
- builder.setChronometerCountDown(androidModel.getChronometerCountDown());
167
- }
211
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
212
+ builder.setChronometerCountDown(androidModel.getChronometerCountDown());
213
+ }
168
214
 
169
- if (androidModel.getGroup() != null) {
170
- builder.setGroup(androidModel.getGroup());
171
- }
215
+ if (androidModel.getGroup() != null) {
216
+ builder.setGroup(androidModel.getGroup());
217
+ }
172
218
 
173
- builder.setGroupAlertBehavior(androidModel.getGroupAlertBehaviour());
174
- builder.setGroupSummary(androidModel.getGroupSummary());
219
+ builder.setGroupAlertBehavior(androidModel.getGroupAlertBehaviour());
220
+ builder.setGroupSummary(androidModel.getGroupSummary());
175
221
 
176
- if (androidModel.getInputHistory() != null) {
177
- builder.setRemoteInputHistory(androidModel.getInputHistory());
178
- }
222
+ if (androidModel.getInputHistory() != null) {
223
+ builder.setRemoteInputHistory(androidModel.getInputHistory());
224
+ }
179
225
 
180
- if (androidModel.getLights() != null) {
181
- ArrayList<Integer> lights = androidModel.getLights();
182
- builder.setLights(lights.get(0), lights.get(1), lights.get(2));
183
- }
226
+ if (androidModel.getLights() != null) {
227
+ ArrayList<Integer> lights = androidModel.getLights();
228
+ builder.setLights(lights.get(0), lights.get(1), lights.get(2));
229
+ }
184
230
 
185
- builder.setLocalOnly(androidModel.getLocalOnly());
231
+ builder.setLocalOnly(androidModel.getLocalOnly());
186
232
 
187
- if (androidModel.getNumber() != null) {
188
- builder.setNumber(androidModel.getNumber());
189
- }
233
+ if (androidModel.getNumber() != null) {
234
+ builder.setNumber(androidModel.getNumber());
235
+ }
190
236
 
191
- if (androidModel.getSound() != null) {
192
- Uri soundUri = ResourceUtils.getSoundUri(androidModel.getSound());
193
- if (soundUri != null) {
194
- hasCustomSound = true;
195
- builder.setSound(soundUri);
196
- } else {
197
- Logger.w(
198
- TAG,
199
- "Unable to retrieve sound for notification, sound was specified as: "
200
- + androidModel.getSound());
237
+ if (androidModel.getSound() != null) {
238
+ Uri soundUri = ResourceUtils.getSoundUri(androidModel.getSound());
239
+ if (soundUri != null) {
240
+ hasCustomSound = true;
241
+ builder.setSound(soundUri);
242
+ } else {
243
+ Logger.w(
244
+ TAG,
245
+ "Unable to retrieve sound for notification, sound was specified as: "
246
+ + androidModel.getSound());
247
+ }
201
248
  }
202
- }
203
249
 
204
- builder.setDefaults(androidModel.getDefaults(hasCustomSound));
205
- builder.setOngoing(androidModel.getOngoing());
206
- builder.setOnlyAlertOnce(androidModel.getOnlyAlertOnce());
207
- builder.setPriority(androidModel.getPriority());
250
+ builder.setDefaults(androidModel.getDefaults(hasCustomSound));
251
+ builder.setOngoing(androidModel.getOngoing());
252
+ builder.setOnlyAlertOnce(androidModel.getOnlyAlertOnce());
253
+ builder.setPriority(androidModel.getPriority());
208
254
 
209
- NotificationAndroidModel.AndroidProgress progress = androidModel.getProgress();
210
- if (progress != null) {
211
- builder.setProgress(
212
- progress.getMax(), progress.getCurrent(), progress.getIndeterminate());
213
- }
255
+ NotificationAndroidModel.AndroidProgress progress = androidModel.getProgress();
256
+ if (progress != null) {
257
+ builder.setProgress(
258
+ progress.getMax(), progress.getCurrent(), progress.getIndeterminate());
259
+ }
214
260
 
215
- if (androidModel.getShortcutId() != null) {
216
- builder.setShortcutId(androidModel.getShortcutId());
217
- }
261
+ if (androidModel.getShortcutId() != null) {
262
+ builder.setShortcutId(androidModel.getShortcutId());
263
+ }
218
264
 
219
- builder.setShowWhen(androidModel.getShowTimestamp());
265
+ builder.setShowWhen(androidModel.getShowTimestamp());
220
266
 
221
- Integer smallIconId = androidModel.getSmallIcon();
222
- if (smallIconId != null) {
223
- Integer smallIconLevel = androidModel.getSmallIconLevel();
224
- if (smallIconLevel != null) {
225
- builder.setSmallIcon(smallIconId, smallIconLevel);
226
- } else {
227
- builder.setSmallIcon(smallIconId);
267
+ Integer smallIconId = androidModel.getSmallIcon();
268
+ if (smallIconId != null) {
269
+ Integer smallIconLevel = androidModel.getSmallIconLevel();
270
+ if (smallIconLevel != null) {
271
+ builder.setSmallIcon(smallIconId, smallIconLevel);
272
+ } else {
273
+ builder.setSmallIcon(smallIconId);
274
+ }
228
275
  }
229
- }
230
276
 
231
- if (androidModel.getSortKey() != null) {
232
- builder.setSortKey(androidModel.getSortKey());
233
- }
277
+ if (androidModel.getSortKey() != null) {
278
+ builder.setSortKey(androidModel.getSortKey());
279
+ }
234
280
 
235
- if (androidModel.getTicker() != null) {
236
- builder.setTicker(androidModel.getTicker());
237
- }
281
+ if (androidModel.getTicker() != null) {
282
+ builder.setTicker(androidModel.getTicker());
283
+ }
238
284
 
239
- if (androidModel.getTimeoutAfter() != null) {
240
- builder.setTimeoutAfter(androidModel.getTimeoutAfter());
241
- }
285
+ if (androidModel.getTimeoutAfter() != null) {
286
+ builder.setTimeoutAfter(androidModel.getTimeoutAfter());
287
+ }
242
288
 
243
- builder.setUsesChronometer(androidModel.getShowChronometer());
289
+ builder.setUsesChronometer(androidModel.getShowChronometer());
244
290
 
245
- long[] vibrationPattern = androidModel.getVibrationPattern();
246
- if (vibrationPattern.length > 0) builder.setVibrate(vibrationPattern);
291
+ long[] vibrationPattern = androidModel.getVibrationPattern();
292
+ if (vibrationPattern.length > 0) builder.setVibrate(vibrationPattern);
247
293
 
248
- builder.setVisibility(androidModel.getVisibility());
294
+ builder.setVisibility(androidModel.getVisibility());
249
295
 
250
- long timestamp = androidModel.getTimestamp();
251
- if (timestamp > -1) builder.setWhen(timestamp);
296
+ long timestamp = androidModel.getTimestamp();
297
+ if (timestamp > -1) builder.setWhen(timestamp);
252
298
 
253
- builder.setAutoCancel(androidModel.getAutoCancel());
299
+ builder.setAutoCancel(androidModel.getAutoCancel());
254
300
 
255
- return builder;
301
+ return builder;
302
+ } finally {
303
+ Trace.endSection();
304
+ }
256
305
  };
257
306
 
258
307
  /*
@@ -534,7 +583,8 @@ class NotificationManager {
534
583
  Logger.e(
535
584
  TAG,
536
585
  "cancelAllNotificationsWithIds -> Failed to parse id as integer "
537
- + id);
586
+ + id,
587
+ e);
538
588
  }
539
589
 
540
590
  if (integerId != null) {
@@ -577,47 +627,64 @@ class NotificationManager {
577
627
  return new ExtendedListenableFuture<>(notificationBundleToBuilder(notificationModel))
578
628
  .continueWith(
579
629
  (taskResult) -> {
580
- NotificationCompat.Builder builder = taskResult;
630
+ Trace.beginSection("notifee:displayNotification");
631
+ try {
632
+ NotificationCompat.Builder builder = taskResult;
633
+
634
+ // Add the following extras for `getDisplayedNotifications()`
635
+ Bundle extrasBundle = new Bundle();
636
+ extrasBundle.putBundle(EXTRA_NOTIFEE_NOTIFICATION, notificationModel.toBundle());
637
+ if (triggerBundle != null) {
638
+ extrasBundle.putBundle(EXTRA_NOTIFEE_TRIGGER, triggerBundle);
639
+ }
640
+ builder.addExtras(extrasBundle);
581
641
 
582
- // Add the following extras for `getDisplayedNotifications()`
583
- Bundle extrasBundle = new Bundle();
584
- extrasBundle.putBundle(EXTRA_NOTIFEE_NOTIFICATION, notificationModel.toBundle());
585
- if (triggerBundle != null) {
586
- extrasBundle.putBundle(EXTRA_NOTIFEE_TRIGGER, triggerBundle);
587
- }
588
- builder.addExtras(extrasBundle);
642
+ NotificationAndroidModel androidBundle = notificationModel.getAndroid();
589
643
 
590
- // build notification
591
- Notification notification = Objects.requireNonNull(builder).build();
644
+ // Set foreground service behavior before building (only for FGS notifications).
645
+ // IMMEDIATE eliminates the 10-second display delay on Android 12+.
646
+ if (androidBundle.getAsForegroundService()) {
647
+ builder.setForegroundServiceBehavior(
648
+ androidBundle.getForegroundServiceBehavior());
649
+ }
592
650
 
593
- int hashCode = notificationModel.getHashCode();
651
+ // build notification
652
+ Notification notification = Objects.requireNonNull(builder).build();
594
653
 
595
- NotificationAndroidModel androidBundle = notificationModel.getAndroid();
596
- if (androidBundle.getLoopSound()) {
597
- notification.flags |= Notification.FLAG_INSISTENT;
598
- }
654
+ int hashCode = notificationModel.getHashCode();
655
+ if (androidBundle.getLoopSound()) {
656
+ notification.flags |= Notification.FLAG_INSISTENT;
657
+ }
599
658
 
600
- if (androidBundle.getFlags() != null && androidBundle.getFlags().length > 0) {
601
- for (int flag : androidBundle.getFlags()) {
602
- notification.flags |= flag;
659
+ if (androidBundle.getFlags() != null && androidBundle.getFlags().length > 0) {
660
+ for (int flag : androidBundle.getFlags()) {
661
+ notification.flags |= flag;
662
+ }
603
663
  }
604
- }
605
664
 
606
- if (androidBundle.getLightUpScreen()) {
607
- PowerManagerUtils.lightUpScreenIfNeeded(ContextHolder.getApplicationContext());
608
- }
665
+ if (androidBundle.getLightUpScreen()) {
666
+ PowerManagerUtils.lightUpScreenIfNeeded(ContextHolder.getApplicationContext());
667
+ }
609
668
 
610
- if (androidBundle.getAsForegroundService()) {
611
- ForegroundService.start(hashCode, notification, notificationModel.toBundle());
612
- } else {
613
- NotificationManagerCompat.from(getApplicationContext())
614
- .notify(androidBundle.getTag(), hashCode, notification);
615
- }
669
+ if (androidBundle.getAsForegroundService()) {
670
+ Trace.beginSection("notifee:startForegroundService");
671
+ try {
672
+ ForegroundService.start(hashCode, notification, notificationModel.toBundle());
673
+ } finally {
674
+ Trace.endSection();
675
+ }
676
+ } else {
677
+ NotificationManagerCompat.from(getApplicationContext())
678
+ .notify(androidBundle.getTag(), hashCode, notification);
679
+ }
616
680
 
617
- EventBus.post(
618
- new NotificationEvent(NotificationEvent.TYPE_DELIVERED, notificationModel));
681
+ EventBus.post(
682
+ new NotificationEvent(NotificationEvent.TYPE_DELIVERED, notificationModel));
619
683
 
620
- return Futures.immediateFuture(null);
684
+ return Futures.immediateFuture(null);
685
+ } finally {
686
+ Trace.endSection();
687
+ }
621
688
  },
622
689
  CACHED_THREAD_POOL);
623
690
  }
@@ -31,6 +31,16 @@ public class NotificationPendingIntent {
31
31
  public static final String NOTIFICATION_ID_INTENT_KEY = "notification_id";
32
32
  private static final String TAG = "NotificationPendingIntent";
33
33
 
34
+ /**
35
+ * Reserved sentinel pressAction id used to signal an explicit opt-out from tap-to-open behavior.
36
+ * When the TS validator sees {@code pressAction: null}, it emits a pressAction bundle with this
37
+ * id so the native layer can distinguish "user opted out" from "pressAction absent" (which
38
+ * triggers the default). DO NOT use this as a real pressAction id.
39
+ *
40
+ * <p>The same constant is mirrored in {@code validateAndroidNotification.ts}.
41
+ */
42
+ public static final String PRESS_ACTION_OPT_OUT_ID = "__NOTIFEE_OPT_OUT__";
43
+
34
44
  /**
35
45
  * Creates a PendingIntent, which when sent triggers this class.
36
46
  *
@@ -0,0 +1,84 @@
1
+ package app.notifee.core;
2
+
3
+ /*
4
+ * Copyright (c) 2016-present Invertase Limited & Contributors
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this library except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+
20
+ import android.content.Context;
21
+ import android.os.Trace;
22
+ import androidx.core.app.NotificationManagerCompat;
23
+
24
+ /**
25
+ * Encapsulates the foreground-service warmup logic so it can be called from both {@link
26
+ * InitProvider} (automatic, at app startup) and the JS bridge ({@code prewarmForegroundService()},
27
+ * on-demand).
28
+ *
29
+ * <p>This class runs <b>synchronously</b> on the caller's thread. Callers are responsible for
30
+ * choosing the appropriate thread/executor.
31
+ *
32
+ * <p>Safe to call multiple times (idempotent). After the first call, class loading is a no-op from
33
+ * ART's perspective and the Binder proxy call is cheap.
34
+ */
35
+ @KeepForSdk
36
+ public final class WarmupHelper {
37
+
38
+ private static final String TAG = "WarmupHelper";
39
+
40
+ static final String[] WARMUP_CLASSES = {
41
+ "app.notifee.core.ForegroundService",
42
+ "app.notifee.core.NotificationManager",
43
+ "app.notifee.core.model.NotificationModel",
44
+ "app.notifee.core.model.NotificationAndroidModel",
45
+ "app.notifee.core.model.ChannelModel",
46
+ "androidx.core.app.NotificationCompat$Builder",
47
+ "androidx.core.app.NotificationManagerCompat",
48
+ };
49
+
50
+ private WarmupHelper() {}
51
+
52
+ /**
53
+ * Pre-loads critical foreground-service classes and warms the INotificationManager Binder proxy.
54
+ * Runs synchronously on the caller's thread.
55
+ *
56
+ * @param context Application context used for class loading and Binder warmup.
57
+ */
58
+ @KeepForSdk
59
+ public static void runWarmup(Context context) {
60
+ Trace.beginSection("notifee:warmup");
61
+ try {
62
+ ClassLoader classLoader = context.getClassLoader();
63
+
64
+ // Pre-load critical foreground service classes to move ART class loading/verification
65
+ // cost from the first displayNotification() call to whenever the caller chooses.
66
+ for (String className : WARMUP_CLASSES) {
67
+ try {
68
+ Class.forName(className, true, classLoader);
69
+ } catch (ClassNotFoundException e) {
70
+ Logger.d(TAG, "Warmup class not found: " + className);
71
+ }
72
+ }
73
+
74
+ // Pre-warm INotificationManager Binder proxy by touching NotificationManagerCompat.
75
+ try {
76
+ NotificationManagerCompat.from(context).getNotificationChannels();
77
+ } catch (Exception e) {
78
+ Logger.d(TAG, "Warmup Binder pre-warm failed: " + e.getMessage());
79
+ }
80
+ } finally {
81
+ Trace.endSection();
82
+ }
83
+ }
84
+ }
@@ -74,6 +74,11 @@ public class NotificationAndroidModel {
74
74
  Objects.requireNonNull(
75
75
  mNotificationAndroidBundle.getParcelableArrayList("foregroundServiceTypes"));
76
76
 
77
+ if (foregroundServiceTypesArrayList.isEmpty()) {
78
+ Logger.w(TAG, "foregroundServiceTypes is empty; treating as absent");
79
+ return ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST;
80
+ }
81
+
77
82
  int foregroundServiceType = 0;
78
83
  for (int i = 0; i < foregroundServiceTypesArrayList.size(); i++) {
79
84
  foregroundServiceType |= ObjectUtils.getInt(foregroundServiceTypesArrayList.get(i));
@@ -97,6 +102,17 @@ public class NotificationAndroidModel {
97
102
  return mNotificationAndroidBundle.getBoolean("asForegroundService", false);
98
103
  }
99
104
 
105
+ /**
106
+ * Gets the foreground service behavior for the notification. Controls whether the foreground
107
+ * service notification is shown immediately or deferred on Android 12+.
108
+ *
109
+ * @return int matching a NotificationCompat.FOREGROUND_SERVICE_* constant
110
+ */
111
+ public int getForegroundServiceBehavior() {
112
+ return mNotificationAndroidBundle.getInt(
113
+ "foregroundServiceBehavior", NotificationCompat.FOREGROUND_SERVICE_DEFAULT);
114
+ }
115
+
100
116
  /**
101
117
  * Gets if the notification should light up the screen when displayed
102
118
  *
@@ -328,7 +344,7 @@ public class NotificationAndroidModel {
328
344
 
329
345
  return lights;
330
346
  } catch (Exception e) {
331
- Logger.e(TAG, "getLights -> Failed to parse lights");
347
+ Logger.e(TAG, "getLights -> Failed to parse lights", e);
332
348
  return null;
333
349
  }
334
350
  }
@@ -317,6 +317,36 @@ class NotifeeApiModule(reactContext: ReactApplicationContext) :
317
317
  }
318
318
  }
319
319
 
320
+ override fun prewarmForegroundService(promise: Promise) {
321
+ val context = reactApplicationContext.applicationContext ?: run {
322
+ promise.reject("ERR_CONTEXT", "ReactApplicationContext is null")
323
+ return
324
+ }
325
+ val executor = java.util.concurrent.Executors.newSingleThreadExecutor { r ->
326
+ Thread(r, "notifee-prewarm").apply {
327
+ isDaemon = true
328
+ priority = Thread.MIN_PRIORITY
329
+ }
330
+ }
331
+ executor.submit {
332
+ android.os.Trace.beginSection("notifee:prewarm")
333
+ try {
334
+ app.notifee.core.WarmupHelper.runWarmup(context)
335
+ promise.resolve(null)
336
+ } catch (t: Throwable) {
337
+ // Best-effort semantics: warmup failures don't reject the promise.
338
+ // WarmupHelper logs and swallows internal errors; if something
339
+ // unexpectedly escapes, log it and still resolve so the JS-side
340
+ // await does not hang.
341
+ Logger.e("NotifeeApiModule", "prewarmForegroundService unexpectedly threw", t)
342
+ promise.resolve(null)
343
+ } finally {
344
+ android.os.Trace.endSection()
345
+ }
346
+ }
347
+ executor.shutdown()
348
+ }
349
+
320
350
  override fun hideNotificationDrawer() {
321
351
  NotifeeReactUtils.hideNotificationDrawer()
322
352
  }