react-native-notify-kit 9.3.0 → 9.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +68 -2
  2. package/android/build.gradle +5 -0
  3. package/android/proguard-rules.pro +1 -0
  4. package/android/src/androidTest/java/app/notifee/core/DoScheduledWorkOrderingTest.java +234 -0
  5. package/android/src/androidTest/java/app/notifee/core/RebootRecoveryTest.java +204 -0
  6. package/android/src/androidTest/java/app/notifee/core/database/TimingWorkDataRepository.java +56 -0
  7. package/android/src/androidTest/java/app/notifee/core/database/WorkDataRepositoryRaceTest.java +221 -0
  8. package/android/src/androidTest/res/drawable/test_icon.xml +14 -0
  9. package/android/src/main/baseline-prof.txt +382 -0
  10. package/android/src/main/java/app/notifee/core/ForegroundService.java +269 -80
  11. package/android/src/main/java/app/notifee/core/InitProvider.java +48 -5
  12. package/android/src/main/java/app/notifee/core/Logger.java +5 -0
  13. package/android/src/main/java/app/notifee/core/NotifeeAlarmManager.java +160 -79
  14. package/android/src/main/java/app/notifee/core/NotificationManager.java +370 -288
  15. package/android/src/main/java/app/notifee/core/WarmupHelper.java +84 -0
  16. package/android/src/main/java/app/notifee/core/database/WorkDataRepository.java +38 -34
  17. package/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java +17 -1
  18. package/android/src/main/kotlin/io/invertase/notifee/NotifeeApiModule.kt +30 -0
  19. package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +182 -0
  20. package/android/src/test/java/app/notifee/core/database/WorkDataRepositoryFutureContractTest.java +279 -0
  21. package/dist/NotifeeApiModule.d.ts +1 -0
  22. package/dist/NotifeeApiModule.js +6 -0
  23. package/dist/NotifeeApiModule.js.map +1 -1
  24. package/dist/specs/NativeNotifeeModule.d.ts +1 -0
  25. package/dist/specs/NativeNotifeeModule.js.map +1 -1
  26. package/dist/types/Module.d.ts +19 -0
  27. package/dist/types/NotificationAndroid.d.ts +58 -0
  28. package/dist/types/NotificationAndroid.js +43 -0
  29. package/dist/types/NotificationAndroid.js.map +1 -1
  30. package/dist/utils/validate.d.ts +1 -0
  31. package/dist/utils/validate.js +10 -4
  32. package/dist/utils/validate.js.map +1 -1
  33. package/dist/validators/validateAndroidChannel.js +3 -3
  34. package/dist/validators/validateAndroidChannel.js.map +1 -1
  35. package/dist/validators/validateAndroidNotification.js +36 -12
  36. package/dist/validators/validateAndroidNotification.js.map +1 -1
  37. package/dist/version.d.ts +1 -1
  38. package/dist/version.js +1 -1
  39. package/ios/NotifeeCore/NotifeeCore+NSNotificationCenter.m +5 -1
  40. package/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m +5 -5
  41. package/ios/NotifeeCore/NotifeeCore.m +28 -9
  42. package/ios/NotifeeCore/NotifeeCoreDelegateHolder.m +5 -1
  43. package/ios/NotifeeCore/NotifeeCoreExtensionHelper.m +7 -2
  44. package/ios/RNNotifee/NotifeeApiModule.mm +31 -10
  45. package/package.json +1 -1
  46. package/src/NotifeeApiModule.ts +7 -0
  47. package/src/specs/NativeNotifeeModule.ts +1 -0
  48. package/src/types/Module.ts +20 -0
  49. package/src/types/NotificationAndroid.ts +62 -0
  50. package/src/utils/validate.ts +12 -4
  51. package/src/validators/validateAndroidChannel.ts +3 -3
  52. package/src/validators/validateAndroidNotification.ts +43 -10
  53. package/src/version.ts +1 -1
@@ -19,6 +19,8 @@ package app.notifee.core;
19
19
 
20
20
  import android.annotation.SuppressLint;
21
21
  import android.app.Notification;
22
+ import android.app.NotificationChannel;
23
+ import android.app.NotificationManager;
22
24
  import android.app.Service;
23
25
  import android.content.ComponentName;
24
26
  import android.content.Context;
@@ -28,8 +30,10 @@ import android.content.pm.ServiceInfo;
28
30
  import android.os.Build;
29
31
  import android.os.Bundle;
30
32
  import android.os.IBinder;
33
+ import android.os.Trace;
31
34
  import androidx.annotation.Nullable;
32
35
  import androidx.annotation.RequiresApi;
36
+ import androidx.core.app.NotificationCompat;
33
37
  import androidx.core.app.NotificationManagerCompat;
34
38
  import app.notifee.core.event.ForegroundServiceEvent;
35
39
  import app.notifee.core.event.NotificationEvent;
@@ -44,6 +48,19 @@ public class ForegroundService extends Service {
44
48
  "app.notifee.core.ForegroundService.STOP";
45
49
 
46
50
  private static final Object sLock = new Object();
51
+ private static final String DEFENSIVE_CHANNEL_ID = "notifee_fg_default";
52
+ private static final int DEFENSIVE_NOTIFICATION_ID = Integer.MAX_VALUE - 1;
53
+
54
+ /**
55
+ * Tracks whether startForeground() has been called on THIS service instance. This is an instance
56
+ * field (not static) because the static fields below track cross-invocation notification state
57
+ * shared across the process lifetime, but mStartForegroundCalled must track whether this specific
58
+ * service instance — which may be a fresh recreation after process death — has fulfilled
59
+ * Android's startForeground() contract. If the process is killed and Android recreates the
60
+ * service, all statics reset AND a new instance is created; we need per-instance tracking to know
61
+ * whether the recreated instance has called startForeground() before attempting stopSelf().
62
+ */
63
+ private volatile boolean mStartForegroundCalled = false;
47
64
 
48
65
  public static String mCurrentNotificationId = null;
49
66
 
@@ -133,6 +150,11 @@ public class ForegroundService extends Service {
133
150
  // Call start service first with stop action
134
151
  context.startService(intent);
135
152
  } catch (IllegalStateException illegalStateException) {
153
+ Logger.w(
154
+ TAG,
155
+ "startService() threw IllegalStateException on STOP path;"
156
+ + " falling back to stopService()",
157
+ illegalStateException);
136
158
  // try to stop with stopService command
137
159
  context.stopService(intent);
138
160
  } catch (Exception exception) {
@@ -143,78 +165,43 @@ public class ForegroundService extends Service {
143
165
  @SuppressLint({"ForegroundServiceType", "MissingPermission"})
144
166
  @Override
145
167
  public int onStartCommand(Intent intent, int flags, int startId) {
146
- // Check if action is to stop the foreground service
147
- if (intent == null || STOP_FOREGROUND_SERVICE_ACTION.equals(intent.getAction())) {
148
- stopSelf();
149
- synchronized (sLock) {
150
- mCurrentNotificationId = null;
151
- mCurrentForegroundServiceType = -1;
152
- mCurrentNotificationBundle = null;
153
- mCurrentNotification = null;
154
- mCurrentHashCode = 0;
168
+ Trace.beginSection("notifee:ForegroundService.onStartCommand");
169
+ try {
170
+ // Check if action is to stop the foreground service
171
+ if (intent == null || STOP_FOREGROUND_SERVICE_ACTION.equals(intent.getAction())) {
172
+ ensureStartForegroundContractSatisfied();
173
+ stopSelf();
174
+ synchronized (sLock) {
175
+ mCurrentNotificationId = null;
176
+ mCurrentForegroundServiceType = -1;
177
+ mCurrentNotificationBundle = null;
178
+ mCurrentNotification = null;
179
+ mCurrentHashCode = 0;
180
+ }
181
+ return Service.START_STICKY_COMPATIBILITY;
155
182
  }
156
- return Service.START_STICKY_COMPATIBILITY;
157
- }
158
-
159
- Bundle extras = intent.getExtras();
160
183
 
161
- if (extras != null) {
162
- // Hash code is sent to service to ensure it is kept the same
163
- int hashCode = extras.getInt("hashCode");
164
- Notification notification = extras.getParcelable("notification");
165
- Bundle bundle = extras.getBundle("notificationBundle");
184
+ Bundle extras = intent.getExtras();
166
185
 
167
- if (notification != null && bundle != null) {
168
- NotificationModel notificationModel = NotificationModel.fromBundle(bundle);
186
+ if (extras != null) {
187
+ // Hash code is sent to service to ensure it is kept the same
188
+ int hashCode = extras.getInt("hashCode");
189
+ Notification notification = extras.getParcelable("notification");
190
+ Bundle bundle = extras.getBundle("notificationBundle");
169
191
 
170
- Object pendingEvent = null;
192
+ if (notification != null && bundle != null) {
193
+ NotificationModel notificationModel = NotificationModel.fromBundle(bundle);
171
194
 
172
- synchronized (sLock) {
173
- if (mCurrentNotificationId == null) {
174
- mCurrentNotificationId = notificationModel.getId();
175
- mCurrentNotificationBundle = bundle;
176
- mCurrentNotification = notification;
177
- mCurrentHashCode = hashCode;
178
-
179
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
180
- int foregroundServiceType = notificationModel.getAndroid().getForegroundServiceType();
181
- if (foregroundServiceType == ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST) {
182
- foregroundServiceType = resolveManifestServiceType();
183
- }
184
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
185
- && foregroundServiceType == ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) {
186
- Logger.e(
187
- TAG,
188
- "Resolved foreground service type is NONE on API 34+; aborting"
189
- + " startForeground to avoid InvalidForegroundServiceTypeException.");
190
- mCurrentNotificationId = null;
191
- mCurrentNotificationBundle = null;
192
- return START_NOT_STICKY;
193
- }
194
- startForeground(hashCode, notification, foregroundServiceType);
195
- mCurrentForegroundServiceType = foregroundServiceType;
196
- } else {
197
- startForeground(hashCode, notification);
198
- }
195
+ Object pendingEvent = null;
196
+ boolean noneEarlyReturn = false;
199
197
 
200
- // On headless task complete
201
- final MethodCallResult<Void> methodCallResult =
202
- (e, aVoid) -> {
203
- stopForegroundCompat();
204
- synchronized (sLock) {
205
- mCurrentNotificationId = null;
206
- mCurrentForegroundServiceType = -1;
207
- mCurrentNotificationBundle = null;
208
- mCurrentNotification = null;
209
- mCurrentHashCode = 0;
210
- }
211
- };
198
+ synchronized (sLock) {
199
+ if (mCurrentNotificationId == null) {
200
+ mCurrentNotificationId = notificationModel.getId();
201
+ mCurrentNotificationBundle = bundle;
202
+ mCurrentNotification = notification;
203
+ mCurrentHashCode = hashCode;
212
204
 
213
- pendingEvent = new ForegroundServiceEvent(notificationModel, methodCallResult);
214
- } else {
215
- if (mCurrentNotificationId.equals(notificationModel.getId())) {
216
- boolean shouldPostNotificationAgain = true;
217
- // find if we need to start the service again if the type was changed
218
205
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
219
206
  int foregroundServiceType =
220
207
  notificationModel.getAndroid().getForegroundServiceType();
@@ -225,32 +212,110 @@ public class ForegroundService extends Service {
225
212
  && foregroundServiceType == ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) {
226
213
  Logger.e(
227
214
  TAG,
228
- "Resolved foreground service type is NONE on API 34+; skipping type"
229
- + " change.");
230
- } else if (foregroundServiceType != mCurrentForegroundServiceType) {
231
- startForeground(hashCode, notification, foregroundServiceType);
215
+ "Resolved foreground service type is NONE on API 34+; aborting"
216
+ + " startForeground to avoid InvalidForegroundServiceTypeException.");
217
+ // Reset stale state while still holding the lock.
218
+ mCurrentNotificationId = null;
219
+ mCurrentForegroundServiceType = -1;
220
+ mCurrentNotificationBundle = null;
221
+ mCurrentNotification = null;
222
+ mCurrentHashCode = 0;
223
+ // Set flag to call the helper AFTER releasing sLock — it performs
224
+ // Binder IPC (PackageManager + startForeground) and must not hold sLock.
225
+ noneEarlyReturn = true;
226
+ } else {
227
+ Trace.beginSection("notifee:startForeground");
228
+ try {
229
+ startForeground(hashCode, notification, foregroundServiceType);
230
+ } finally {
231
+ Trace.endSection();
232
+ }
233
+ mStartForegroundCalled = true;
232
234
  mCurrentForegroundServiceType = foregroundServiceType;
233
- shouldPostNotificationAgain = false;
234
235
  }
236
+ } else {
237
+ Trace.beginSection("notifee:startForeground");
238
+ try {
239
+ startForeground(hashCode, notification);
240
+ } finally {
241
+ Trace.endSection();
242
+ }
243
+ mStartForegroundCalled = true;
235
244
  }
236
- if (shouldPostNotificationAgain) {
237
- NotificationManagerCompat.from(ContextHolder.getApplicationContext())
238
- .notify(hashCode, notification);
245
+
246
+ if (!noneEarlyReturn) {
247
+ // On headless task complete
248
+ final MethodCallResult<Void> methodCallResult =
249
+ (e, aVoid) -> {
250
+ stopForegroundCompat();
251
+ synchronized (sLock) {
252
+ mCurrentNotificationId = null;
253
+ mCurrentForegroundServiceType = -1;
254
+ mCurrentNotificationBundle = null;
255
+ mCurrentNotification = null;
256
+ mCurrentHashCode = 0;
257
+ }
258
+ };
259
+
260
+ pendingEvent = new ForegroundServiceEvent(notificationModel, methodCallResult);
239
261
  }
240
262
  } else {
241
- pendingEvent =
242
- new NotificationEvent(NotificationEvent.TYPE_FG_ALREADY_EXIST, notificationModel);
263
+ if (mCurrentNotificationId.equals(notificationModel.getId())) {
264
+ boolean shouldPostNotificationAgain = true;
265
+ // find if we need to start the service again if the type was changed
266
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
267
+ int foregroundServiceType =
268
+ notificationModel.getAndroid().getForegroundServiceType();
269
+ if (foregroundServiceType == ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST) {
270
+ foregroundServiceType = resolveManifestServiceType();
271
+ }
272
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
273
+ && foregroundServiceType == ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) {
274
+ Logger.e(
275
+ TAG,
276
+ "Resolved foreground service type is NONE on API 34+; skipping type"
277
+ + " change.");
278
+ } else if (foregroundServiceType != mCurrentForegroundServiceType) {
279
+ Trace.beginSection("notifee:startForeground");
280
+ try {
281
+ startForeground(hashCode, notification, foregroundServiceType);
282
+ } finally {
283
+ Trace.endSection();
284
+ }
285
+ mStartForegroundCalled = true;
286
+ mCurrentForegroundServiceType = foregroundServiceType;
287
+ shouldPostNotificationAgain = false;
288
+ }
289
+ }
290
+ if (shouldPostNotificationAgain) {
291
+ NotificationManagerCompat.from(ContextHolder.getApplicationContext())
292
+ .notify(hashCode, notification);
293
+ }
294
+ } else {
295
+ pendingEvent =
296
+ new NotificationEvent(
297
+ NotificationEvent.TYPE_FG_ALREADY_EXIST, notificationModel);
298
+ }
243
299
  }
244
300
  }
245
- }
246
301
 
247
- if (pendingEvent != null) {
248
- EventBus.post(pendingEvent);
302
+ // Handle NONE early return outside the lock — the helper performs Binder IPC
303
+ // (PackageManager query + startForeground) that must not hold sLock.
304
+ if (noneEarlyReturn) {
305
+ ensureStartForegroundContractSatisfied();
306
+ return START_NOT_STICKY;
307
+ }
308
+
309
+ if (pendingEvent != null) {
310
+ EventBus.post(pendingEvent);
311
+ }
249
312
  }
250
313
  }
251
- }
252
314
 
253
- return START_NOT_STICKY;
315
+ return START_NOT_STICKY;
316
+ } finally {
317
+ Trace.endSection();
318
+ }
254
319
  }
255
320
 
256
321
  @Nullable
@@ -305,6 +370,26 @@ public class ForegroundService extends Service {
305
370
  : ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST;
306
371
  }
307
372
 
373
+ /**
374
+ * Ensures the defensive notification channel exists. Required for the placeholder notification
375
+ * used on the STOP path when startForeground() was never called on this instance.
376
+ */
377
+ private void ensureDefensiveChannel() {
378
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
379
+ NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
380
+ if (nm != null && nm.getNotificationChannel(DEFENSIVE_CHANNEL_ID) == null) {
381
+ NotificationChannel channel =
382
+ new NotificationChannel(
383
+ DEFENSIVE_CHANNEL_ID, "Foreground Service", NotificationManager.IMPORTANCE_MIN);
384
+ channel.setShowBadge(false);
385
+ channel.enableLights(false);
386
+ channel.enableVibration(false);
387
+ channel.setSound(null, null);
388
+ nm.createNotificationChannel(channel);
389
+ }
390
+ }
391
+ }
392
+
308
393
  @SuppressWarnings("deprecation")
309
394
  private void stopForegroundCompat() {
310
395
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -314,6 +399,110 @@ public class ForegroundService extends Service {
314
399
  }
315
400
  }
316
401
 
402
+ /**
403
+ * Queries the PackageManager for the {@code foregroundServiceType} attribute declared on this
404
+ * service's {@code <service>} element in AndroidManifest.xml.
405
+ *
406
+ * <p>This is intentionally separate from {@link #resolveManifestServiceType()} (which resolves
407
+ * the effective type for the normal startForeground path). That method is static, uses {@link
408
+ * ContextHolder#getApplicationContext()} (which may be null during early recreation), and has
409
+ * different return semantics — it maps missing types to {@code FOREGROUND_SERVICE_TYPE_MANIFEST}
410
+ * on API 29-33 and to {@code FOREGROUND_SERVICE_TYPE_NONE} on API 34+. This method is an instance
411
+ * method that uses {@code this} as context (always valid inside a running Service) and returns
412
+ * the raw declared value without any API-level mapping, which is what the proactive manifest
413
+ * check in {@link #ensureStartForegroundContractSatisfied()} needs.
414
+ *
415
+ * @return the raw {@code foregroundServiceType} bitmask from the manifest, or 0 if the service is
416
+ * not declared or has no explicit type
417
+ */
418
+ private int getDeclaredForegroundServiceType() {
419
+ try {
420
+ ComponentName component = new ComponentName(this, ForegroundService.class);
421
+ ServiceInfo info =
422
+ getPackageManager().getServiceInfo(component, PackageManager.GET_META_DATA);
423
+ return info.getForegroundServiceType();
424
+ } catch (PackageManager.NameNotFoundException e) {
425
+ return 0;
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Ensures Android's 5-second {@code startForeground()} contract is satisfied before an early
431
+ * return from {@link #onStartCommand(Intent, int, int)}.
432
+ *
433
+ * <p>This method is idempotent: if {@link #mStartForegroundCalled} is already {@code true} (i.e.,
434
+ * this service instance has previously called {@code startForeground()} successfully), this
435
+ * method returns immediately without side effects.
436
+ *
437
+ * <p>On API 34+ (Android 14), this method performs a proactive manifest check via {@link
438
+ * #getDeclaredForegroundServiceType()} before attempting the defensive {@code startForeground()}
439
+ * call. If no {@code foregroundServiceType} is declared in the manifest, the method throws a
440
+ * {@link RuntimeException} with a clear error message and documentation URL instead of proceeding
441
+ * to a call that would fail with a cryptic {@code SecurityException}.
442
+ *
443
+ * <p>On API levels below 34, the manifest check is skipped and the defensive {@code
444
+ * startForeground()} is called directly, as no {@code foregroundServiceType} declaration is
445
+ * required.
446
+ *
447
+ * @throws RuntimeException if the manifest is missing required {@code foregroundServiceType}
448
+ * declarations on API 34+, or if the defensive {@code startForeground()} call fails for any
449
+ * other reason. In both cases, the exception terminates the process with a crash report that
450
+ * is more actionable than the ANR that would otherwise occur.
451
+ */
452
+ @SuppressLint({"ForegroundServiceType", "MissingPermission"})
453
+ private void ensureStartForegroundContractSatisfied() {
454
+ if (mStartForegroundCalled) {
455
+ return;
456
+ }
457
+
458
+ // Proactive manifest check on API 34+: fail fast with actionable message.
459
+ // Also caches the declared type for use in the 3-param startForeground() call below.
460
+ int declaredTypes = 0;
461
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
462
+ declaredTypes = getDeclaredForegroundServiceType();
463
+ if (declaredTypes == 0) {
464
+ String msg =
465
+ "react-native-notify-kit: ForegroundService cannot start — "
466
+ + "no foregroundServiceType declared in AndroidManifest.xml on API 34+. "
467
+ + "See https://github.com/marcocrupi/react-native-notify-kit"
468
+ + "#foreground-service-setup-android-14";
469
+ Logger.e(TAG, msg);
470
+ // Intentional crash: a RuntimeException here terminates the process with a clear,
471
+ // actionable crash report within milliseconds — well inside the 5-second ANR budget.
472
+ // The alternative (catching and calling stopSelf()) leaves Android's startForeground()
473
+ // contract unsatisfied, which produces a ForegroundServiceDidNotStartInTimeException
474
+ // ANR with a framework-only stack trace that gives no indication of the root cause.
475
+ // Do NOT suppress this throw without also providing an alternative mechanism to satisfy
476
+ // the startForeground() contract or terminate the process before the ANR fires.
477
+ throw new RuntimeException(msg);
478
+ }
479
+ }
480
+
481
+ try {
482
+ ensureDefensiveChannel();
483
+ Notification placeholder =
484
+ new NotificationCompat.Builder(this, DEFENSIVE_CHANNEL_ID)
485
+ .setSmallIcon(android.R.drawable.ic_dialog_info)
486
+ .build();
487
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
488
+ // On API 34+, pass the manifest-declared type explicitly rather than relying on
489
+ // the 2-param startForeground() to resolve type 0 → manifest type. This avoids
490
+ // ambiguity about implicit type resolution behavior across OEM variants.
491
+ startForeground(DEFENSIVE_NOTIFICATION_ID, placeholder, declaredTypes);
492
+ } else {
493
+ startForeground(DEFENSIVE_NOTIFICATION_ID, placeholder);
494
+ }
495
+ mStartForegroundCalled = true;
496
+ stopForegroundCompat();
497
+ } catch (Exception e) {
498
+ String msg =
499
+ "react-native-notify-kit: defensive startForeground() failed. "
500
+ + "This indicates an inconsistent service state and the process cannot recover.";
501
+ Logger.e(TAG, msg, e);
502
+ throw new RuntimeException(msg, e);
503
+ }
504
+ }
505
+
317
506
  private void handleTimeout(int startId, int fgsType) {
318
507
  Logger.e(
319
508
  TAG,
@@ -20,12 +20,17 @@ package app.notifee.core;
20
20
  import android.content.ContentProvider;
21
21
  import android.content.ContentValues;
22
22
  import android.content.Context;
23
+ import android.content.pm.ApplicationInfo;
24
+ import android.content.pm.PackageManager;
23
25
  import android.content.pm.ProviderInfo;
24
26
  import android.database.Cursor;
25
27
  import android.net.Uri;
28
+ import android.os.Trace;
26
29
  import androidx.annotation.CallSuper;
27
30
  import androidx.annotation.NonNull;
28
31
  import androidx.annotation.Nullable;
32
+ import java.util.concurrent.ExecutorService;
33
+ import java.util.concurrent.Executors;
29
34
 
30
35
  @KeepForSdk
31
36
  public class InitProvider extends ContentProvider {
@@ -42,21 +47,59 @@ public class InitProvider extends ContentProvider {
42
47
  super.attachInfo(context, info);
43
48
  }
44
49
 
50
+ private static final String TAG = "InitProvider";
51
+
45
52
  @KeepForSdk
46
53
  @CallSuper
47
54
  @Override
48
55
  public boolean onCreate() {
49
- if (ContextHolder.getApplicationContext() == null) {
50
- Context context = getContext();
51
- if (context != null && context.getApplicationContext() != null) {
52
- context = context.getApplicationContext();
56
+ Trace.beginSection("notifee:InitProvider.onCreate");
57
+ try {
58
+ if (ContextHolder.getApplicationContext() == null) {
59
+ Context context = getContext();
60
+ if (context != null && context.getApplicationContext() != null) {
61
+ context = context.getApplicationContext();
62
+ }
63
+ ContextHolder.setApplicationContext(context);
64
+ }
65
+
66
+ Context appContext = ContextHolder.getApplicationContext();
67
+ if (appContext != null && isWarmupEnabled(appContext)) {
68
+ dispatchWarmup(appContext);
53
69
  }
54
- ContextHolder.setApplicationContext(context);
70
+ } finally {
71
+ Trace.endSection();
55
72
  }
56
73
 
57
74
  return false;
58
75
  }
59
76
 
77
+ private static boolean isWarmupEnabled(Context context) {
78
+ try {
79
+ ApplicationInfo ai =
80
+ context
81
+ .getPackageManager()
82
+ .getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
83
+ return ai.metaData == null || ai.metaData.getBoolean("notifee_init_warmup_enabled", true);
84
+ } catch (PackageManager.NameNotFoundException e) {
85
+ return true;
86
+ }
87
+ }
88
+
89
+ private static void dispatchWarmup(final Context context) {
90
+ ExecutorService executor =
91
+ Executors.newSingleThreadExecutor(
92
+ r -> {
93
+ Thread t = new Thread(r, "notifee-init-warmup");
94
+ t.setDaemon(true);
95
+ t.setPriority(Thread.MIN_PRIORITY);
96
+ return t;
97
+ });
98
+
99
+ executor.submit(() -> WarmupHelper.runWarmup(context));
100
+ executor.shutdown();
101
+ }
102
+
60
103
  @Nullable
61
104
  @Override
62
105
  public Cursor query(
@@ -48,6 +48,11 @@ public class Logger {
48
48
  Log.w(TAG, tagAndMessage(tag, message));
49
49
  }
50
50
 
51
+ @KeepForSdk
52
+ public static void w(@NonNull String tag, String message, Throwable throwable) {
53
+ Log.w(TAG, tagAndMessage(tag, message), throwable);
54
+ }
55
+
51
56
  @KeepForSdk
52
57
  public static void e(@NonNull String tag, String message, Exception e) {
53
58
  Log.e(TAG, tagAndMessage(tag, message), e);