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
|
|
452
|
-
*
|
|
453
|
-
*
|
|
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
|
|
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
|
-
//
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
|
|
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.
|
|
1
|
+
export declare const version = "10.4.7";
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-notify-kit",
|
|
3
|
-
"version": "10.4.
|
|
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.
|
|
2
|
+
export const version = '10.4.7';
|