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.
- package/README.md +68 -2
- package/android/build.gradle +5 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/androidTest/java/app/notifee/core/DoScheduledWorkOrderingTest.java +234 -0
- package/android/src/androidTest/java/app/notifee/core/RebootRecoveryTest.java +204 -0
- package/android/src/androidTest/java/app/notifee/core/database/TimingWorkDataRepository.java +56 -0
- package/android/src/androidTest/java/app/notifee/core/database/WorkDataRepositoryRaceTest.java +221 -0
- package/android/src/androidTest/res/drawable/test_icon.xml +14 -0
- package/android/src/main/baseline-prof.txt +382 -0
- package/android/src/main/java/app/notifee/core/ForegroundService.java +269 -80
- package/android/src/main/java/app/notifee/core/InitProvider.java +48 -5
- package/android/src/main/java/app/notifee/core/Logger.java +5 -0
- package/android/src/main/java/app/notifee/core/NotifeeAlarmManager.java +160 -79
- package/android/src/main/java/app/notifee/core/NotificationManager.java +370 -288
- package/android/src/main/java/app/notifee/core/WarmupHelper.java +84 -0
- package/android/src/main/java/app/notifee/core/database/WorkDataRepository.java +38 -34
- package/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java +17 -1
- package/android/src/main/kotlin/io/invertase/notifee/NotifeeApiModule.kt +30 -0
- package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +182 -0
- package/android/src/test/java/app/notifee/core/database/WorkDataRepositoryFutureContractTest.java +279 -0
- package/dist/NotifeeApiModule.d.ts +1 -0
- package/dist/NotifeeApiModule.js +6 -0
- package/dist/NotifeeApiModule.js.map +1 -1
- package/dist/specs/NativeNotifeeModule.d.ts +1 -0
- package/dist/specs/NativeNotifeeModule.js.map +1 -1
- package/dist/types/Module.d.ts +19 -0
- package/dist/types/NotificationAndroid.d.ts +58 -0
- package/dist/types/NotificationAndroid.js +43 -0
- package/dist/types/NotificationAndroid.js.map +1 -1
- package/dist/utils/validate.d.ts +1 -0
- package/dist/utils/validate.js +10 -4
- package/dist/utils/validate.js.map +1 -1
- package/dist/validators/validateAndroidChannel.js +3 -3
- package/dist/validators/validateAndroidChannel.js.map +1 -1
- package/dist/validators/validateAndroidNotification.js +36 -12
- package/dist/validators/validateAndroidNotification.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/ios/NotifeeCore/NotifeeCore+NSNotificationCenter.m +5 -1
- package/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m +5 -5
- package/ios/NotifeeCore/NotifeeCore.m +28 -9
- package/ios/NotifeeCore/NotifeeCoreDelegateHolder.m +5 -1
- package/ios/NotifeeCore/NotifeeCoreExtensionHelper.m +7 -2
- package/ios/RNNotifee/NotifeeApiModule.mm +31 -10
- package/package.json +1 -1
- package/src/NotifeeApiModule.ts +7 -0
- package/src/specs/NativeNotifeeModule.ts +1 -0
- package/src/types/Module.ts +20 -0
- package/src/types/NotificationAndroid.ts +62 -0
- package/src/utils/validate.ts +12 -4
- package/src/validators/validateAndroidChannel.ts +3 -3
- package/src/validators/validateAndroidNotification.ts +43 -10
- 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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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 (
|
|
168
|
-
|
|
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
|
-
|
|
192
|
+
if (notification != null && bundle != null) {
|
|
193
|
+
NotificationModel notificationModel = NotificationModel.fromBundle(bundle);
|
|
171
194
|
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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+;
|
|
229
|
-
+ "
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
context =
|
|
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
|
-
|
|
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);
|