react-native-notify-kit 10.4.6 → 10.4.7

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.
@@ -429,6 +429,33 @@ public class ForegroundService extends Service {
429
429
  }
430
430
  }
431
431
 
432
+ @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
433
+ private static int selectDefensiveForegroundServiceType(int declaredTypes) {
434
+ int[] preferredTypes = {
435
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
436
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
437
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK,
438
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
439
+ };
440
+
441
+ for (int type : preferredTypes) {
442
+ if (isDeclared(declaredTypes, type)) {
443
+ return type;
444
+ }
445
+ }
446
+
447
+ return declaredTypes;
448
+ }
449
+
450
+ private static boolean isDeclared(int declaredTypes, int type) {
451
+ return (declaredTypes & type) == type;
452
+ }
453
+
454
+ private static boolean hasAnyForegroundServiceType(int foregroundServiceTypes) {
455
+ return foregroundServiceTypes != ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE
456
+ && foregroundServiceTypes != ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST;
457
+ }
458
+
432
459
  /**
433
460
  * Ensures Android's 5-second {@code startForeground()} contract is satisfied before an early
434
461
  * return from {@link #onStartCommand(Intent, int, int)}.
@@ -448,9 +475,9 @@ public class ForegroundService extends Service {
448
475
  * required.
449
476
  *
450
477
  * @throws RuntimeException if the manifest is missing required {@code foregroundServiceType}
451
- * declarations on API 34+, or if the defensive {@code startForeground()} call fails for any
452
- * other reason. In both cases, the exception terminates the process with a crash report that
453
- * is more actionable than the ANR that would otherwise occur.
478
+ * declarations on API 34+, or if the defensive {@code startForeground()} call fails for a
479
+ * non-platform-permission reason. {@link SecurityException} is handled as a best-effort stop
480
+ * because API 34+ can deny while-in-use types when the app is backgrounded.
454
481
  */
455
482
  @SuppressLint({"ForegroundServiceType", "MissingPermission"})
456
483
  private void ensureStartForegroundContractSatisfied() {
@@ -463,7 +490,7 @@ public class ForegroundService extends Service {
463
490
  int declaredTypes = 0;
464
491
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
465
492
  declaredTypes = getDeclaredForegroundServiceType();
466
- if (declaredTypes == 0) {
493
+ if (!hasAnyForegroundServiceType(declaredTypes)) {
467
494
  String msg =
468
495
  "react-native-notify-kit: ForegroundService cannot start — "
469
496
  + "no foregroundServiceType declared in AndroidManifest.xml on API 34+. "
@@ -488,15 +515,22 @@ public class ForegroundService extends Service {
488
515
  .setSmallIcon(android.R.drawable.ic_dialog_info)
489
516
  .build();
490
517
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
491
- // On API 34+, pass the manifest-declared type explicitly rather than relying on
492
- // the 2-param startForeground() to resolve type 0 manifest type. This avoids
493
- // ambiguity about implicit type resolution behavior across OEM variants.
494
- startForeground(DEFENSIVE_NOTIFICATION_ID, placeholder, declaredTypes);
518
+ // The STOP/null defensive placeholder does not represent the real workload. Prefer a
519
+ // declared type without while-in-use runtime restrictions before falling back to the
520
+ // manifest-declared mask.
521
+ int defensiveTypes = selectDefensiveForegroundServiceType(declaredTypes);
522
+ startForeground(DEFENSIVE_NOTIFICATION_ID, placeholder, defensiveTypes);
495
523
  } else {
496
524
  startForeground(DEFENSIVE_NOTIFICATION_ID, placeholder);
497
525
  }
498
526
  mStartForegroundCalled = true;
499
527
  stopForegroundCompat();
528
+ } catch (SecurityException e) {
529
+ Logger.w(
530
+ TAG,
531
+ "Defensive startForeground() was denied by Android while stopping the service; "
532
+ + "continuing with stopSelf() to avoid a fatal app crash.",
533
+ e);
500
534
  } catch (Exception e) {
501
535
  String msg =
502
536
  "react-native-notify-kit: defensive startForeground() failed. "
@@ -10,13 +10,16 @@ import android.app.Notification;
10
10
  import android.app.NotificationChannel;
11
11
  import android.app.NotificationManager;
12
12
  import android.app.Service;
13
+ import android.content.ComponentName;
13
14
  import android.content.Context;
14
15
  import android.content.Intent;
16
+ import android.content.pm.PackageManager;
15
17
  import android.content.pm.ServiceInfo;
16
18
  import android.os.Bundle;
17
19
  import androidx.core.app.NotificationCompat;
18
20
  import app.notifee.core.event.NotificationEvent;
19
21
  import java.lang.reflect.Field;
22
+ import java.lang.reflect.Method;
20
23
  import java.util.ArrayList;
21
24
  import java.util.List;
22
25
  import org.greenrobot.eventbus.Subscribe;
@@ -30,6 +33,9 @@ import org.robolectric.RobolectricTestRunner;
30
33
  import org.robolectric.RuntimeEnvironment;
31
34
  import org.robolectric.android.controller.ServiceController;
32
35
  import org.robolectric.annotation.Config;
36
+ import org.robolectric.annotation.Implementation;
37
+ import org.robolectric.annotation.Implements;
38
+ import org.robolectric.shadows.ShadowService;
33
39
 
34
40
  @RunWith(RobolectricTestRunner.class)
35
41
  public class ForegroundServiceTest {
@@ -198,6 +204,90 @@ public class ForegroundServiceTest {
198
204
  assertEquals(Service.START_STICKY_COMPATIBILITY, result);
199
205
  }
200
206
 
207
+ @Test
208
+ @Config(sdk = 34)
209
+ public void onStartCommand_stopIntentApi34MicrophoneAndDataSync_usesDataSyncDefensiveType()
210
+ throws Exception {
211
+ declareForegroundServiceTypes(
212
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
213
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
214
+ ServiceController<ForegroundService> controller =
215
+ Robolectric.buildService(ForegroundService.class);
216
+ ForegroundService service = controller.create().get();
217
+
218
+ int result = service.onStartCommand(buildStopIntent(), 0, 1);
219
+
220
+ assertEquals(Service.START_STICKY_COMPATIBILITY, result);
221
+ assertEquals(
222
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC, getLastForegroundServiceType(service));
223
+ }
224
+
225
+ @Test
226
+ @Config(sdk = 34)
227
+ public void onStartCommand_stopIntentApi34CameraAndDataSync_usesDataSyncDefensiveType()
228
+ throws Exception {
229
+ declareForegroundServiceTypes(
230
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA | ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
231
+ ServiceController<ForegroundService> controller =
232
+ Robolectric.buildService(ForegroundService.class);
233
+ ForegroundService service = controller.create().get();
234
+
235
+ int result = service.onStartCommand(buildStopIntent(), 0, 1);
236
+
237
+ assertEquals(Service.START_STICKY_COMPATIBILITY, result);
238
+ assertEquals(
239
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC, getLastForegroundServiceType(service));
240
+ }
241
+
242
+ @Test
243
+ @Config(sdk = 34)
244
+ public void
245
+ onStartCommand_stopIntentApi34ShortServiceAndMicrophone_usesShortServiceDefensiveType()
246
+ throws Exception {
247
+ declareForegroundServiceTypes(
248
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
249
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
250
+ ServiceController<ForegroundService> controller =
251
+ Robolectric.buildService(ForegroundService.class);
252
+ ForegroundService service = controller.create().get();
253
+
254
+ int result = service.onStartCommand(buildStopIntent(), 0, 1);
255
+
256
+ assertEquals(Service.START_STICKY_COMPATIBILITY, result);
257
+ assertEquals(
258
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE, getLastForegroundServiceType(service));
259
+ }
260
+
261
+ @Test
262
+ @Config(sdk = 34)
263
+ public void onStartCommand_stopIntentApi34MicrophoneOnly_doesNotUseUndeclaredShortService()
264
+ throws Exception {
265
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
266
+ ServiceController<ForegroundService> controller =
267
+ Robolectric.buildService(ForegroundService.class);
268
+ ForegroundService service = controller.create().get();
269
+
270
+ int result = service.onStartCommand(buildStopIntent(), 0, 1);
271
+
272
+ assertEquals(Service.START_STICKY_COMPATIBILITY, result);
273
+ assertEquals(
274
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE, getLastForegroundServiceType(service));
275
+ }
276
+
277
+ @Test
278
+ @Config(sdk = 34, shadows = ThrowingStartForegroundShadowService.class)
279
+ public void onStartCommand_stopIntentApi34SecurityException_doesNotCrash() throws Exception {
280
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
281
+ ServiceController<ForegroundService> controller =
282
+ Robolectric.buildService(ForegroundService.class);
283
+ ForegroundService service = controller.create().get();
284
+
285
+ int result = service.onStartCommand(buildStopIntent(), 0, 1);
286
+
287
+ assertEquals(Service.START_STICKY_COMPATIBILITY, result);
288
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
289
+ }
290
+
201
291
  // ──────────────────────────────────────────────────────────────────────────
202
292
  // START path and onTimeout event emission (regression guards for 9.1.13)
203
293
  // ──────────────────────────────────────────────────────────────────────────
@@ -391,6 +481,42 @@ public class ForegroundServiceTest {
391
481
  return intent;
392
482
  }
393
483
 
484
+ private static Intent buildStopIntent() {
485
+ Intent intent = new Intent();
486
+ intent.setAction(ForegroundService.STOP_FOREGROUND_SERVICE_ACTION);
487
+ return intent;
488
+ }
489
+
490
+ private static void declareForegroundServiceTypes(int foregroundServiceTypes) throws Exception {
491
+ Context context = RuntimeEnvironment.getApplication();
492
+ ComponentName component = new ComponentName(context, ForegroundService.class);
493
+ ServiceInfo serviceInfo;
494
+ try {
495
+ serviceInfo =
496
+ context.getPackageManager().getServiceInfo(component, PackageManager.GET_META_DATA);
497
+ } catch (PackageManager.NameNotFoundException e) {
498
+ serviceInfo = new ServiceInfo();
499
+ }
500
+ serviceInfo.packageName = component.getPackageName();
501
+ serviceInfo.name = component.getClassName();
502
+ setForegroundServiceType(serviceInfo, foregroundServiceTypes);
503
+ org.robolectric.Shadows.shadowOf(context.getPackageManager()).addOrUpdateService(serviceInfo);
504
+ }
505
+
506
+ private static void setForegroundServiceType(ServiceInfo serviceInfo, int foregroundServiceTypes)
507
+ throws Exception {
508
+ Field field = ServiceInfo.class.getDeclaredField("mForegroundServiceType");
509
+ field.setAccessible(true);
510
+ field.setInt(serviceInfo, foregroundServiceTypes);
511
+ }
512
+
513
+ private static int getLastForegroundServiceType(Service service) throws Exception {
514
+ Object shadowService = org.robolectric.Shadows.shadowOf(service);
515
+ Method method = ShadowService.class.getDeclaredMethod("getForegroundServiceType");
516
+ method.setAccessible(true);
517
+ return (int) method.invoke(shadowService);
518
+ }
519
+
394
520
  /**
395
521
  * Populates the service's private static tracking fields directly, simulating the post-START
396
522
  * state that onTimeout expects to observe. Using reflection here (rather than running a full
@@ -426,4 +552,12 @@ public class ForegroundServiceTest {
426
552
  events.add(event);
427
553
  }
428
554
  }
555
+
556
+ @Implements(Service.class)
557
+ public static class ThrowingStartForegroundShadowService extends ShadowService {
558
+ @Implementation
559
+ protected void startForeground(int id, Notification notification, int foregroundServiceType) {
560
+ throw new SecurityException("while-in-use foreground service type denied");
561
+ }
562
+ }
429
563
  }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "10.4.6";
1
+ export declare const version = "10.4.7";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Generated by genversion.
2
- export const version = '10.4.6';
2
+ export const version = '10.4.7';
3
3
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-notify-kit",
3
- "version": "10.4.6",
3
+ "version": "10.4.7",
4
4
  "author": "Marco Crupi",
5
5
  "description": "Maintained Notifee-compatible React Native notifications library for Android, iOS, FCM Mode, rich notifications, foreground services, and Expo CNG development builds.",
6
6
  "main": "dist/index.js",
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by genversion.
2
- export const version = '10.4.6';
2
+ export const version = '10.4.7';