react-native-notify-kit 10.7.2 → 10.8.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 (44) hide show
  1. package/android/src/main/java/app/notifee/core/ForegroundService.java +363 -66
  2. package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +1194 -32
  3. package/android/src/test/java/app/notifee/core/NotificationManagerActionRoutingTest.java +37 -0
  4. package/dist/fcm/parseFcmPayload.d.ts +1 -0
  5. package/dist/fcm/parseFcmPayload.js.map +1 -1
  6. package/dist/fcm/reconstructNotification.js +96 -8
  7. package/dist/fcm/reconstructNotification.js.map +1 -1
  8. package/dist/validators/validateAndroidPressAction.js +3 -0
  9. package/dist/validators/validateAndroidPressAction.js.map +1 -1
  10. package/dist/validators/validateIOSNotification.js +5 -2
  11. package/dist/validators/validateIOSNotification.js.map +1 -1
  12. package/dist/version.d.ts +1 -1
  13. package/dist/version.js +1 -1
  14. package/ios/NotifeeCore/NotifeeCore.m +36 -3
  15. package/ios/NotifeeCore/NotifeeCoreExtensionHelper.m +82 -3
  16. package/ios/NotifeeCore/NotifeeCoreFcmIdentity.h +91 -0
  17. package/package.json +1 -1
  18. package/server/README.md +5 -3
  19. package/server/dist/buildPayload.d.ts.map +1 -1
  20. package/server/dist/buildPayload.js +2 -0
  21. package/server/dist/buildPayload.js.map +1 -1
  22. package/server/dist/ios.d.ts.map +1 -1
  23. package/server/dist/ios.js +8 -1
  24. package/server/dist/ios.js.map +1 -1
  25. package/server/dist/serialize.d.ts +2 -0
  26. package/server/dist/serialize.d.ts.map +1 -1
  27. package/server/dist/serialize.js +6 -0
  28. package/server/dist/serialize.js.map +1 -1
  29. package/server/dist/types.d.ts +1 -1
  30. package/server/dist/types.d.ts.map +1 -1
  31. package/server/dist/validation.d.ts.map +1 -1
  32. package/server/dist/validation.js +160 -6
  33. package/server/dist/validation.js.map +1 -1
  34. package/server/src/buildPayload.ts +2 -0
  35. package/server/src/ios.ts +8 -1
  36. package/server/src/serialize.ts +8 -0
  37. package/server/src/types.ts +1 -0
  38. package/server/src/validation.ts +187 -6
  39. package/src/fcm/parseFcmPayload.ts +1 -0
  40. package/src/fcm/reconstructNotification.ts +105 -10
  41. package/src/internal/fcmContract.d.ts +57 -12
  42. package/src/validators/validateAndroidPressAction.ts +4 -0
  43. package/src/validators/validateIOSNotification.ts +7 -2
  44. package/src/version.ts +1 -1
@@ -1,22 +1,29 @@
1
1
  package app.notifee.core;
2
2
 
3
3
  import static org.junit.Assert.assertEquals;
4
+ import static org.junit.Assert.assertFalse;
4
5
  import static org.junit.Assert.assertNotNull;
5
6
  import static org.junit.Assert.assertNull;
6
7
  import static org.junit.Assert.assertThrows;
7
8
  import static org.junit.Assert.assertTrue;
8
9
  import static org.junit.Assert.fail;
9
10
 
11
+ import android.app.ForegroundServiceStartNotAllowedException;
12
+ import android.app.InvalidForegroundServiceTypeException;
13
+ import android.app.MissingForegroundServiceTypeException;
10
14
  import android.app.Notification;
11
15
  import android.app.NotificationChannel;
12
16
  import android.app.NotificationManager;
13
17
  import android.app.Service;
14
18
  import android.content.ComponentName;
15
19
  import android.content.Context;
20
+ import android.content.ContextWrapper;
16
21
  import android.content.Intent;
17
22
  import android.content.pm.PackageManager;
18
23
  import android.content.pm.ServiceInfo;
24
+ import android.os.Build;
19
25
  import android.os.Bundle;
26
+ import android.os.Looper;
20
27
  import androidx.core.app.NotificationCompat;
21
28
  import app.notifee.core.event.ForegroundServiceEvent;
22
29
  import app.notifee.core.event.NotificationEvent;
@@ -24,6 +31,12 @@ import java.lang.reflect.Field;
24
31
  import java.lang.reflect.Method;
25
32
  import java.util.ArrayList;
26
33
  import java.util.List;
34
+ import java.util.concurrent.CountDownLatch;
35
+ import java.util.concurrent.ExecutorService;
36
+ import java.util.concurrent.Executors;
37
+ import java.util.concurrent.Future;
38
+ import java.util.concurrent.TimeUnit;
39
+ import java.util.concurrent.atomic.AtomicReference;
27
40
  import org.greenrobot.eventbus.Subscribe;
28
41
  import org.greenrobot.eventbus.ThreadMode;
29
42
  import org.junit.After;
@@ -37,12 +50,15 @@ import org.robolectric.android.controller.ServiceController;
37
50
  import org.robolectric.annotation.Config;
38
51
  import org.robolectric.annotation.Implementation;
39
52
  import org.robolectric.annotation.Implements;
53
+ import org.robolectric.shadows.ShadowLog;
40
54
  import org.robolectric.shadows.ShadowNotificationManager;
41
55
  import org.robolectric.shadows.ShadowService;
42
56
 
43
57
  @RunWith(RobolectricTestRunner.class)
44
58
  public class ForegroundServiceTest {
45
59
 
60
+ private final List<ServiceController<ForegroundService>> controllers = new ArrayList<>();
61
+
46
62
  @Before
47
63
  public void setUp() {
48
64
  // Initialize ContextHolder so the service can access the application context
@@ -51,6 +67,11 @@ public class ForegroundServiceTest {
51
67
 
52
68
  @After
53
69
  public void tearDown() throws Exception {
70
+ for (int i = controllers.size() - 1; i >= 0; i--) {
71
+ controllers.get(i).destroy();
72
+ }
73
+ org.robolectric.Shadows.shadowOf(Looper.getMainLooper()).idle();
74
+ ContextHolder.setApplicationContext(RuntimeEnvironment.getApplication());
54
75
  // Reset public and private static fields to prevent cross-test pollution. The three private
55
76
  // statics are cleared via reflection here so that tests which seed them directly (to exercise
56
77
  // onTimeout without running the full START path) cannot leak state into neighbouring tests.
@@ -61,6 +82,1163 @@ public class ForegroundServiceTest {
61
82
  setPrivateStatic("mCurrentHashCode", 0);
62
83
  }
63
84
 
85
+ @Test
86
+ @Config(sdk = {24, 26, 30, 33, 34})
87
+ public void stop_withoutInstance_doesNotStartServiceOrEmitRunner() {
88
+ RecordingServiceContext context = useRecordingContext();
89
+ FgsRunnerCapture capture = new FgsRunnerCapture();
90
+ EventBus.register(capture);
91
+ try {
92
+ ForegroundService.stop();
93
+ idleMain();
94
+ assertTrue(context.starts.isEmpty());
95
+ assertEquals(0, context.stops);
96
+ assertTrue(capture.events.isEmpty());
97
+ assertNull(ForegroundService.mCurrentNotificationId);
98
+ } finally {
99
+ EventBus.unregister(capture);
100
+ }
101
+ }
102
+
103
+ @Test
104
+ @Config(sdk = 33)
105
+ public void stop_startStopDestroySecondStop_doesNotRecreateService() {
106
+ RecordingServiceContext context = useRecordingContext();
107
+ ServiceController<ForegroundService> controller = buildService();
108
+ ForegroundService service = controller.create().get();
109
+ createChannel();
110
+ service.onStartCommand(buildStartIntent("first", 101), 0, 1);
111
+
112
+ ForegroundService.stop();
113
+ idleMain();
114
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
115
+ controller.destroy();
116
+ controllers.remove(controller);
117
+ ForegroundService.stop();
118
+ idleMain();
119
+ assertTrue(context.starts.isEmpty());
120
+ assertNull(ForegroundService.mCurrentNotificationId);
121
+ }
122
+
123
+ @Test
124
+ @Config(sdk = 33)
125
+ public void stop_nullNotificationCache_stillStopsLiveInstance() throws Exception {
126
+ RecordingServiceContext context = useRecordingContext();
127
+ ForegroundService service = buildService().create().get();
128
+ createChannel();
129
+ service.onStartCommand(buildStartIntent("live", 102), 0, 1);
130
+ ForegroundService.mCurrentNotificationId = null;
131
+ ForegroundService.mCurrentForegroundServiceType = -1;
132
+ setPrivateStatic("mCurrentNotificationBundle", null);
133
+ setPrivateStatic("mCurrentNotification", null);
134
+ setPrivateStatic("mCurrentHashCode", 0);
135
+
136
+ ForegroundService.stop();
137
+ idleMain();
138
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
139
+ assertTrue(context.starts.isEmpty());
140
+ }
141
+
142
+ @Test
143
+ @Config(sdk = 33)
144
+ public void stop_concurrentDestroy_doesNotDispatchNewGeneration() throws Exception {
145
+ RecordingServiceContext context = useRecordingContext();
146
+ ServiceController<ForegroundService> controller = buildService();
147
+ ForegroundService service = controller.create().get();
148
+ createChannel();
149
+ service.onStartCommand(buildStartIntent("old", 103), 0, 1);
150
+ CountDownLatch begin = new CountDownLatch(1);
151
+ AtomicReference<Throwable> failure = new AtomicReference<>();
152
+ Thread stop =
153
+ new Thread(
154
+ () -> {
155
+ try {
156
+ assertTrue(begin.await(5, TimeUnit.SECONDS));
157
+ ForegroundService.stop();
158
+ } catch (Throwable e) {
159
+ failure.set(e);
160
+ }
161
+ });
162
+ stop.start();
163
+ begin.countDown();
164
+ controller.destroy();
165
+ controllers.remove(controller);
166
+ stop.join(5000);
167
+ assertFalse(stop.isAlive());
168
+ assertNull(failure.get());
169
+ idleMain();
170
+ assertTrue(context.starts.isEmpty());
171
+ }
172
+
173
+ @Test
174
+ @Config(sdk = 33)
175
+ public void onDestroy_oldInstance_preservesNewInstanceAndAllNotificationState() throws Exception {
176
+ RecordingServiceContext context = useRecordingContext();
177
+ ServiceController<ForegroundService> oldController = buildService();
178
+ ForegroundService old = oldController.create().get();
179
+ createChannel();
180
+ old.onStartCommand(buildStartIntent("old", 104), 0, 1);
181
+ ForegroundService current = buildService().create().get();
182
+ current.onStartCommand(buildStartIntent("new", 105, "new title"), 0, 2);
183
+ oldController.destroy();
184
+ controllers.remove(oldController);
185
+
186
+ assertCurrentState("new", 105, "new title");
187
+ ForegroundService.stop();
188
+ idleMain();
189
+ assertTrue(org.robolectric.Shadows.shadowOf(current).isStoppedBySelf());
190
+ assertTrue(context.starts.isEmpty());
191
+ }
192
+
193
+ @Test
194
+ @Config(sdk = 33)
195
+ public void runnerCompletion_oldInstance_doesNotDemoteOrClearNewInstance() throws Exception {
196
+ useRecordingContext();
197
+ FgsRunnerCapture capture = new FgsRunnerCapture();
198
+ EventBus.register(capture);
199
+ try {
200
+ ForegroundService old = buildService().create().get();
201
+ createChannel();
202
+ old.onStartCommand(buildStartIntent("old", 106), 0, 1);
203
+ ForegroundService current = buildService().create().get();
204
+ current.onStartCommand(buildStartIntent("new", 107, "new title"), 0, 2);
205
+ assertEquals(2, capture.events.size());
206
+
207
+ capture.events.get(0).setCompletionResult();
208
+ idleMain();
209
+ assertCurrentState("new", 107, "new title");
210
+ assertFalse(org.robolectric.Shadows.shadowOf(current).isForegroundStopped());
211
+ } finally {
212
+ EventBus.unregister(capture);
213
+ }
214
+ }
215
+
216
+ @Test
217
+ @Config(sdk = 33)
218
+ public void runnerCompletion_oldRunOnSameInstance_preservesNewRun() throws Exception {
219
+ useRecordingContext();
220
+ FgsRunnerCapture capture = new FgsRunnerCapture();
221
+ EventBus.register(capture);
222
+ try {
223
+ ForegroundService service = buildService().create().get();
224
+ createChannel();
225
+ service.onStartCommand(buildStartIntent("old", 108), 0, 1);
226
+ service.onStartCommand(buildStopIntent(), 0, 2);
227
+ service.onStartCommand(buildStartIntent("new", 109, "new title"), 0, 3);
228
+ assertEquals(2, capture.events.size());
229
+
230
+ capture.events.get(0).setCompletionResult();
231
+ idleMain();
232
+ assertCurrentState("new", 109, "new title");
233
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
234
+ capture.events.get(1).setCompletionResult();
235
+ idleMain();
236
+ assertNull(ForegroundService.mCurrentNotificationId);
237
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
238
+ } finally {
239
+ EventBus.unregister(capture);
240
+ }
241
+ }
242
+
243
+ @Test
244
+ @Config(sdk = 33)
245
+ public void stop_acceptedPendingStart_fulfillsRealContractBeforeStopping() {
246
+ RecordingServiceContext context = useRecordingContext();
247
+ createChannel();
248
+ ForegroundService.start(110, buildNotification(), buildNotificationBundle("pending"));
249
+ ForegroundService.stop();
250
+ idleMain();
251
+ assertEquals(1, context.starts.size());
252
+ ForegroundService service = buildService().create().get();
253
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
254
+ service.onStartCommand(context.starts.get(0), 0, 1);
255
+ idleMain();
256
+ assertEquals(110, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
257
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
258
+ assertNull(ForegroundService.mCurrentNotificationId);
259
+ assertEquals(1, context.starts.size());
260
+ }
261
+
262
+ @Test
263
+ @Config(sdk = 33)
264
+ public void stop_betweenCreateAndPendingDelivery_doesNotStopBeforeRealPromotion() {
265
+ RecordingServiceContext context = useRecordingContext();
266
+ createChannel();
267
+ ForegroundService.start(111, buildNotification(), buildNotificationBundle("pending"));
268
+ ForegroundService service = buildService().create().get();
269
+ ForegroundService.stop();
270
+ idleMain();
271
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
272
+ assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
273
+
274
+ service.onStartCommand(context.starts.get(0), 0, 1);
275
+ idleMain();
276
+ assertEquals(111, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
277
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
278
+ assertEquals(1, context.starts.size());
279
+ }
280
+
281
+ @Test
282
+ @Config(sdk = 33)
283
+ public void stop_newStartAcceptedBeforeQueuedStop_doesNotCancelLaterStart() throws Exception {
284
+ RecordingServiceContext context = useRecordingContext();
285
+ createChannel();
286
+ ForegroundService service = buildService().create().get();
287
+ service.onStartCommand(buildStartIntent("same", 112, "A"), 0, 1);
288
+ ForegroundService.stop();
289
+ Intent later = buildStartIntent("same", 112, "B");
290
+ ForegroundService.start(
291
+ 112, later.getParcelableExtra("notification"), later.getBundleExtra("notificationBundle"));
292
+ idleMain();
293
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
294
+ service.onStartCommand(context.starts.get(0), 0, 2);
295
+ idleMain();
296
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
297
+ assertCurrentState("same", 112, "B");
298
+ }
299
+
300
+ @Test
301
+ @Config(
302
+ sdk = {28, 33, 34},
303
+ shadows = {RejectBStartForegroundShadowService.class, RejectBNotificationShadowManager.class})
304
+ public void stop_failedRestart_preservesPreviousSubmittedStateAndRunner() throws Exception {
305
+ RecordingServiceContext context = useRecordingContext();
306
+ if (Build.VERSION.SDK_INT >= 34) {
307
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
308
+ }
309
+ createChannel();
310
+ FgsRunnerCapture capture = new FgsRunnerCapture();
311
+ EventBus.register(capture);
312
+ try {
313
+ ForegroundService service = buildService().create().get();
314
+ Intent a = buildStartIntent("same", 177, "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
315
+ ForegroundService.start(
316
+ 177, a.getParcelableExtra("notification"), a.getBundleExtra("notificationBundle"));
317
+ service.onStartCommand(context.starts.get(0), 0, 1);
318
+ Field tokenField = ForegroundService.class.getDeclaredField("mRunnerToken");
319
+ tokenField.setAccessible(true);
320
+ Object tokenA = tokenField.get(service);
321
+ int typeA = ForegroundService.mCurrentForegroundServiceType;
322
+
323
+ ForegroundService.stop();
324
+ Intent b = buildStartIntent("same", 177, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
325
+ ForegroundService.start(
326
+ 177, b.getParcelableExtra("notification"), b.getBundleExtra("notificationBundle"));
327
+ SecurityException failure =
328
+ assertThrows(
329
+ SecurityException.class, () -> service.onStartCommand(context.starts.get(1), 0, 2));
330
+
331
+ assertEquals("posting B rejected", failure.getMessage());
332
+ assertEquals(
333
+ "A",
334
+ org.robolectric.Shadows.shadowOf(
335
+ (NotificationManager)
336
+ RuntimeEnvironment.getApplication()
337
+ .getSystemService(Context.NOTIFICATION_SERVICE))
338
+ .getNotification(177)
339
+ .extras
340
+ .getString(Notification.EXTRA_TITLE));
341
+ assertCurrentState("same", 177, "A");
342
+ assertEquals(typeA, ForegroundService.mCurrentForegroundServiceType);
343
+ assertTrue(tokenA == tokenField.get(service));
344
+ assertEquals(1, capture.events.size());
345
+ assertTrue(ForegroundService.repostIfActive("same"));
346
+ idleMain();
347
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
348
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
349
+ capture.events.get(0).setCompletionResult();
350
+ idleMain();
351
+ assertCurrentState("same", 177, "A");
352
+ assertTrue(tokenA == tokenField.get(service));
353
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
354
+
355
+ // A failed reservation must still be replaced by a fresh runner when C succeeds in the
356
+ // reserved session, even if C uses a different notification ID.
357
+ Intent c = buildStartIntent("new", 178, "C", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
358
+ ForegroundService.start(
359
+ 178, c.getParcelableExtra("notification"), c.getBundleExtra("notificationBundle"));
360
+ service.onStartCommand(context.starts.get(2), 0, 3);
361
+ assertCurrentState("new", 178, "C");
362
+ assertFalse(tokenA == tokenField.get(service));
363
+ assertEquals(2, capture.events.size());
364
+ capture.events.get(0).setCompletionResult();
365
+ idleMain();
366
+ assertCurrentState("new", 178, "C");
367
+ capture.events.get(1).setCompletionResult();
368
+ idleMain();
369
+ assertNull(ForegroundService.mCurrentNotificationId);
370
+ } finally {
371
+ EventBus.unregister(capture);
372
+ }
373
+ }
374
+
375
+ @Test
376
+ @Config(sdk = 33)
377
+ public void onStartCommand_stopAndNullWithPendingStart_doNotSuppressRealStart() {
378
+ RecordingServiceContext context = useRecordingContext();
379
+ createChannel();
380
+ ForegroundService.start(113, buildNotification(), buildNotificationBundle("pending"));
381
+ ForegroundService service = buildService().create().get();
382
+ service.onStartCommand(buildStopIntent(), 0, 1);
383
+ service.onStartCommand(null, 0, 2);
384
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
385
+ assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
386
+
387
+ service.onStartCommand(context.starts.get(0), 0, 3);
388
+ idleMain();
389
+ assertEquals(113, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
390
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
391
+ }
392
+
393
+ @Test
394
+ @Config(
395
+ sdk = {31, 33, 34, 35},
396
+ shadows = DefensiveFailureShadowService.class)
397
+ public void onStartCommand_stopAndNullDenied_areNonFatalAndDoNotClaimPromotion()
398
+ throws Exception {
399
+ if (Build.VERSION.SDK_INT >= 34) {
400
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
401
+ }
402
+ DefensiveFailureShadowService.failure = new ForegroundServiceStartNotAllowedException("denied");
403
+ DefensiveFailureShadowService.attempts = 0;
404
+ ShadowLog.clear();
405
+ ForegroundService service = buildService().create().get();
406
+ service.onStartCommand(buildStopIntent(), 0, 1);
407
+ service.onStartCommand(null, 0, 2);
408
+
409
+ assertEquals(2, DefensiveFailureShadowService.attempts);
410
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
411
+ assertTrue(
412
+ ShadowLog.getLogsForTag("NOTIFEE").stream()
413
+ .anyMatch(item -> item.throwable == DefensiveFailureShadowService.failure));
414
+ }
415
+
416
+ @Test
417
+ @Config(sdk = 31, shadows = DefensiveFailureShadowService.class)
418
+ public void onStartCommand_stopDeniedApi31_isNonFatal() {
419
+ DefensiveFailureShadowService.failure = new ForegroundServiceStartNotAllowedException("denied");
420
+ ForegroundService service = buildService().create().get();
421
+ service.onStartCommand(buildStopIntent(), 0, 1);
422
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
423
+ }
424
+
425
+ @Test
426
+ @Config(
427
+ sdk = {24, 30, 33},
428
+ shadows = DefensiveFailureShadowService.class)
429
+ public void onStartCommand_stopGenericIllegalState_remainsObservable() {
430
+ assertTerminalFailureObservable(new IllegalStateException("unrelated ISE"));
431
+ }
432
+
433
+ @Test
434
+ @Config(sdk = 34, shadows = DefensiveFailureShadowService.class)
435
+ public void onStartCommand_stopMissingType_remainsObservable() throws Exception {
436
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
437
+ assertTerminalFailureObservable(new MissingForegroundServiceTypeException("missing type"));
438
+ }
439
+
440
+ @Test
441
+ @Config(sdk = 34, shadows = DefensiveFailureShadowService.class)
442
+ public void onStartCommand_stopInvalidType_remainsObservable() throws Exception {
443
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
444
+ assertTerminalFailureObservable(new InvalidForegroundServiceTypeException("invalid type"));
445
+ }
446
+
447
+ @Test
448
+ @Config(sdk = 34, shadows = DefensiveFailureShadowService.class)
449
+ public void onStartCommand_noneDenied_isNotSoftSuppressed() throws Exception {
450
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
451
+ DefensiveFailureShadowService.failure =
452
+ new ForegroundServiceStartNotAllowedException("NONE denied");
453
+ ForegroundService service = buildService().create().get();
454
+ Intent none = buildStartIntent("none", 114, "none", ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE);
455
+ // The model normalizes NONE to MANIFEST on API 34. Removing only the application context
456
+ // makes manifest resolution return NONE; the Service still has its declared defensive type.
457
+ ContextHolder.setApplicationContext(null);
458
+ RuntimeException thrown =
459
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(none, 0, 1));
460
+ assertTrue(
461
+ thrown == DefensiveFailureShadowService.failure
462
+ || thrown.getCause() == DefensiveFailureShadowService.failure);
463
+ assertEquals(Integer.MAX_VALUE - 1, DefensiveFailureShadowService.lastId);
464
+ assertEquals(
465
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC, DefensiveFailureShadowService.lastType);
466
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
467
+ }
468
+
469
+ @Test
470
+ @Config(sdk = 33, shadows = SecurityDefensiveChannelShadowManager.class)
471
+ public void onStartCommand_terminalChannelSecurityException_remainsObservable() {
472
+ for (Intent terminal : new Intent[] {buildStopIntent(), null}) {
473
+ ForegroundService service = buildService().create().get();
474
+ SecurityException thrown =
475
+ assertThrows(SecurityException.class, () -> service.onStartCommand(terminal, 0, 1));
476
+ assertEquals("defensive channel failure", thrown.getMessage());
477
+ assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
478
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
479
+ }
480
+ }
481
+
482
+ @Test
483
+ @Config(sdk = 33, shadows = SecurityStopForegroundShadowService.class)
484
+ public void onStartCommand_terminalTeardownSecurityException_remainsObservable() {
485
+ for (Intent terminal : new Intent[] {buildStopIntent(), null}) {
486
+ ForegroundService service = buildService().create().get();
487
+ SecurityException thrown =
488
+ assertThrows(SecurityException.class, () -> service.onStartCommand(terminal, 0, 1));
489
+ assertEquals("defensive teardown failure", thrown.getMessage());
490
+ assertEquals(
491
+ Integer.MAX_VALUE - 1,
492
+ org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
493
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
494
+ }
495
+ }
496
+
497
+ @Test
498
+ @Config(sdk = 33, shadows = DefensiveFailureShadowService.class)
499
+ public void onStartCommand_pendingStartDenied_isNotSoftSuppressedByStop() {
500
+ RecordingServiceContext context = useRecordingContext();
501
+ ForegroundService.start(115, buildNotification(), buildNotificationBundle("pending"));
502
+ ForegroundService.stop();
503
+ DefensiveFailureShadowService.failure =
504
+ new ForegroundServiceStartNotAllowedException("real START denied");
505
+ ForegroundService service = buildService().create().get();
506
+ assertThrows(
507
+ ForegroundServiceStartNotAllowedException.class,
508
+ () -> service.onStartCommand(context.starts.get(0), 0, 1));
509
+ idleMain();
510
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
511
+ }
512
+
513
+ private void assertTerminalFailureObservable(RuntimeException failure) {
514
+ DefensiveFailureShadowService.failure = failure;
515
+ ForegroundService service = buildService().create().get();
516
+ RuntimeException thrown =
517
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(buildStopIntent(), 0, 1));
518
+ assertTrue(thrown == failure || thrown.getCause() == failure);
519
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
520
+ }
521
+
522
+ @Test
523
+ @Config(sdk = 33)
524
+ public void runnerCompletion_stopSupersededByNewStart_preservesLaterRun() throws Exception {
525
+ RecordingServiceContext context = useRecordingContext();
526
+ FgsRunnerCapture capture = new FgsRunnerCapture();
527
+ EventBus.register(capture);
528
+ try {
529
+ ForegroundService service = buildService().create().get();
530
+ createChannel();
531
+ service.onStartCommand(buildStartIntent("same", 116, "A"), 0, 1);
532
+ ForegroundService.stop();
533
+ Intent later = buildStartIntent("same", 116, "B");
534
+ ForegroundService.start(
535
+ 116,
536
+ later.getParcelableExtra("notification"),
537
+ later.getBundleExtra("notificationBundle"));
538
+ service.onStartCommand(context.starts.get(0), 0, 2);
539
+ capture.events.get(0).setCompletionResult();
540
+ idleMain();
541
+
542
+ assertCurrentState("same", 116, "B");
543
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
544
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
545
+ assertEquals(2, capture.events.size());
546
+ capture.events.get(1).setCompletionResult();
547
+ idleMain();
548
+ assertNull(ForegroundService.mCurrentNotificationId);
549
+ } finally {
550
+ EventBus.unregister(capture);
551
+ }
552
+ }
553
+
554
+ @Test
555
+ @Config(sdk = 33)
556
+ public void stop_createdButUnpublishedStart_waitsForRealStartCommand() {
557
+ RecordingServiceContext context = useRecordingContext();
558
+ ForegroundService service = buildService().create().get();
559
+ ForegroundService.stop();
560
+ idleMain();
561
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
562
+ assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
563
+ createChannel();
564
+ service.onStartCommand(buildStartIntent("unknown", 117), 0, 1);
565
+ idleMain();
566
+ assertEquals(117, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
567
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
568
+ assertTrue(context.starts.isEmpty());
569
+ }
570
+
571
+ @Test
572
+ @Config(sdk = 33)
573
+ public void stop_multiplePendingStarts_waitsForAllRealCommands() {
574
+ RecordingServiceContext context = useRecordingContext();
575
+ createChannel();
576
+ ForegroundService.start(118, buildNotification(), buildNotificationBundle("pending"));
577
+ ForegroundService.start(118, buildNotification(), buildNotificationBundle("pending"));
578
+ ForegroundService.stop();
579
+ ForegroundService service = buildService().create().get();
580
+ service.onStartCommand(context.starts.get(0), 0, 1);
581
+ idleMain();
582
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
583
+ service.onStartCommand(context.starts.get(1), 0, 2);
584
+ idleMain();
585
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
586
+ assertEquals(2, org.robolectric.Shadows.shadowOf(service).getStopSelfResultId());
587
+ assertEquals(2, context.starts.size());
588
+ }
589
+
590
+ @Test
591
+ @Config(sdk = 33)
592
+ public void onStartCommand_pendingWithoutPromotion_doesNotAcquireStopAuthority() {
593
+ RecordingServiceContext context = useRecordingContext();
594
+ // Accepted START, but its delivery has no notification. STOP must not turn this into a
595
+ // successful/absent START obligation, even though malformed START handling is out of scope.
596
+ ForegroundService.start(119, null, buildNotificationBundle("pending"));
597
+ ForegroundService.stop();
598
+ ForegroundService service = buildService().create().get();
599
+ service.onStartCommand(context.starts.get(0), 0, 1);
600
+ idleMain();
601
+ ForegroundService.stop();
602
+ service.onStartCommand(null, 0, 2);
603
+ idleMain();
604
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
605
+ assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
606
+ assertEquals(1, context.starts.size());
607
+ }
608
+
609
+ @Test
610
+ @Config(sdk = 33)
611
+ public void start_dispatchFailure_doesNotCancelEarlierStop() {
612
+ RecordingServiceContext context = useRecordingContext();
613
+ createChannel();
614
+ ForegroundService service = buildService().create().get();
615
+ service.onStartCommand(buildStartIntent("live", 120), 0, 1);
616
+ ForegroundService.stop();
617
+ context.startFailure = new ForegroundServiceStartNotAllowedException("dispatch rejected");
618
+ assertThrows(
619
+ ForegroundServiceStartNotAllowedException.class,
620
+ () -> ForegroundService.start(121, buildNotification(), buildNotificationBundle("failed")));
621
+ idleMain();
622
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
623
+ assertNull(ForegroundService.mCurrentNotificationId);
624
+ assertEquals(1, context.starts.size());
625
+ }
626
+
627
+ @Test
628
+ @Config(sdk = 33, shadows = RefuseStopSelfShadowService.class)
629
+ public void stop_frameworkRefusesTokenStop_preservesCurrentState() throws Exception {
630
+ RecordingServiceContext context = useRecordingContext();
631
+ ForegroundService service = buildService().create().get();
632
+ createChannel();
633
+ service.onStartCommand(buildStartIntent("live", 122, "live title"), 0, 1);
634
+ ForegroundService.stop();
635
+ idleMain();
636
+ assertCurrentState("live", 122, "live title");
637
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
638
+ assertTrue(context.starts.isEmpty());
639
+ }
640
+
641
+ @Test
642
+ @Config(sdk = 34)
643
+ public void onTimeout_oldInstance_doesNotClearNewStateOrEmitNewInstanceEvent() throws Exception {
644
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
645
+ createChannel();
646
+ ForegroundService old = buildService().create().get();
647
+ old.onStartCommand(
648
+ buildStartIntent("old", 123, "old", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
649
+ 0,
650
+ 1);
651
+ ForegroundService current = buildService().create().get();
652
+ current.onStartCommand(
653
+ buildStartIntent(
654
+ "new", 124, "new title", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
655
+ 0,
656
+ 2);
657
+ FgsEventCapture capture = new FgsEventCapture();
658
+ EventBus.register(capture);
659
+ try {
660
+ old.onTimeout(1);
661
+ assertCurrentState("new", 124, "new title");
662
+ assertEquals(
663
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
664
+ ForegroundService.mCurrentForegroundServiceType);
665
+ assertTrue(capture.events.isEmpty());
666
+ } finally {
667
+ EventBus.unregister(capture);
668
+ }
669
+ }
670
+
671
+ @Test
672
+ @Config(sdk = 33, shadows = FailStopForegroundShadowService.class)
673
+ public void onStartCommand_denialOutsidePromotion_isStillFatal() {
674
+ ForegroundService service = buildService().create().get();
675
+ RuntimeException thrown =
676
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(buildStopIntent(), 0, 1));
677
+ assertTrue(
678
+ thrown instanceof ForegroundServiceStartNotAllowedException
679
+ || thrown.getCause() instanceof ForegroundServiceStartNotAllowedException);
680
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
681
+ }
682
+
683
+ @Test
684
+ @Config(sdk = 33)
685
+ public void stop_duringStartAcceptance_preservesAndThenStopsAcceptedStart() throws Exception {
686
+ CountDownLatch dispatchEntered = new CountDownLatch(1);
687
+ CountDownLatch releaseDispatch = new CountDownLatch(1);
688
+ CountDownLatch stopAttempted = new CountDownLatch(1);
689
+ RecordingServiceContext context =
690
+ new RecordingServiceContext(RuntimeEnvironment.getApplication()) {
691
+ @Override
692
+ public ComponentName startForegroundService(Intent intent) {
693
+ dispatchEntered.countDown();
694
+ try {
695
+ assertTrue(releaseDispatch.await(5, TimeUnit.SECONDS));
696
+ } catch (InterruptedException e) {
697
+ throw new AssertionError(e);
698
+ }
699
+ return super.startForegroundService(intent);
700
+ }
701
+ };
702
+ ContextHolder.setApplicationContext(context);
703
+ createChannel();
704
+ Notification notification = buildNotification();
705
+ Bundle bundle = buildNotificationBundle("pending");
706
+ ExecutorService workers = Executors.newFixedThreadPool(2);
707
+ try {
708
+ Future<?> start = workers.submit(() -> ForegroundService.start(125, notification, bundle));
709
+ assertTrue(dispatchEntered.await(5, TimeUnit.SECONDS));
710
+ Future<?> stop =
711
+ workers.submit(
712
+ () -> {
713
+ stopAttempted.countDown();
714
+ ForegroundService.stop();
715
+ });
716
+ assertTrue(stopAttempted.await(5, TimeUnit.SECONDS));
717
+ releaseDispatch.countDown();
718
+ start.get(5, TimeUnit.SECONDS);
719
+ stop.get(5, TimeUnit.SECONDS);
720
+
721
+ ForegroundService service = buildService().create().get();
722
+ service.onStartCommand(context.starts.get(0), 0, 1);
723
+ idleMain();
724
+ assertEquals(
725
+ 125, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
726
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
727
+ assertEquals(1, context.starts.size());
728
+ } finally {
729
+ releaseDispatch.countDown();
730
+ workers.shutdownNow();
731
+ }
732
+ }
733
+
734
+ @Test
735
+ @Config(sdk = 33)
736
+ public void stop_queuedForDestroyedInstance_doesNotTouchLaterInstance() throws Exception {
737
+ RecordingServiceContext context = useRecordingContext();
738
+ createChannel();
739
+ ServiceController<ForegroundService> oldController = buildService();
740
+ ForegroundService old = oldController.create().get();
741
+ old.onStartCommand(buildStartIntent("old", 126), 0, 1);
742
+ ForegroundService.stop();
743
+ oldController.destroy();
744
+ controllers.remove(oldController);
745
+ ForegroundService current = buildService().create().get();
746
+ current.onStartCommand(buildStartIntent("new", 127, "new title"), 0, 2);
747
+ idleMain();
748
+ assertCurrentState("new", 127, "new title");
749
+ assertFalse(org.robolectric.Shadows.shadowOf(current).isStoppedBySelf());
750
+ assertTrue(context.starts.isEmpty());
751
+ }
752
+
753
+ @Test
754
+ @Config(sdk = 33)
755
+ public void stop_liveInstanceWithUnavailableApplicationContext_stillReachesInstance() {
756
+ RecordingServiceContext context = useRecordingContext();
757
+ createChannel();
758
+ ForegroundService service = buildService().create().get();
759
+ service.onStartCommand(buildStartIntent("live", 128), 0, 1);
760
+ ContextHolder.setApplicationContext(null);
761
+ ForegroundService.stop();
762
+ idleMain();
763
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
764
+ assertTrue(context.starts.isEmpty());
765
+ }
766
+
767
+ @Test
768
+ @Config(sdk = 24)
769
+ public void stop_pendingStartBeforeApi26_preservesPlainStartContract() {
770
+ RecordingServiceContext context = useRecordingContext();
771
+ ForegroundService.start(129, buildNotification(), buildNotificationBundle("pending"));
772
+ ForegroundService.stop();
773
+ ForegroundService service = buildService().create().get();
774
+ service.onStartCommand(context.starts.get(0), 0, 1);
775
+ idleMain();
776
+ assertEquals(129, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
777
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
778
+ assertEquals(1, context.starts.size());
779
+ }
780
+
781
+ @Test
782
+ @Config(sdk = 33)
783
+ public void runnerCompletion_afterStopBeforeNewDelivery_doesNotDemotePendingRun() {
784
+ RecordingServiceContext context = useRecordingContext();
785
+ createChannel();
786
+ FgsRunnerCapture capture = new FgsRunnerCapture();
787
+ EventBus.register(capture);
788
+ try {
789
+ ForegroundService service = buildService().create().get();
790
+ service.onStartCommand(buildStartIntent("same", 130), 0, 1);
791
+ ForegroundService.stop();
792
+ ForegroundService.start(130, buildNotification(), buildNotificationBundle("same"));
793
+ capture.events.get(0).setCompletionResult();
794
+ idleMain();
795
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
796
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
797
+ service.onStartCommand(context.starts.get(0), 0, 2);
798
+ assertEquals(2, capture.events.size());
799
+ assertEquals("same", ForegroundService.mCurrentNotificationId);
800
+ } finally {
801
+ EventBus.unregister(capture);
802
+ }
803
+ }
804
+
805
+ @Test
806
+ @Config(sdk = 33)
807
+ public void stop_laterRealPromotion_settlesEarlierDeliveredUnknownContract() {
808
+ RecordingServiceContext context = useRecordingContext();
809
+ ForegroundService.start(131, null, buildNotificationBundle("unknown"));
810
+ ForegroundService.stop();
811
+ ForegroundService service = buildService().create().get();
812
+ service.onStartCommand(context.starts.get(0), 0, 1);
813
+ idleMain();
814
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
815
+ createChannel();
816
+ ForegroundService.start(132, buildNotification(), buildNotificationBundle("valid"));
817
+ service.onStartCommand(context.starts.get(1), 0, 2);
818
+ ForegroundService.stop();
819
+ idleMain();
820
+ assertEquals(132, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
821
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
822
+ assertNull(ForegroundService.mCurrentNotificationId);
823
+ assertEquals(2, context.starts.size());
824
+ }
825
+
826
+ @Test
827
+ @Config(sdk = 28, shadows = RejectBStartForegroundShadowService.class)
828
+ public void stop_failedUpdateOnRealForegroundInstance_doesNotFenceStopForever() throws Exception {
829
+ RecordingServiceContext context = useRecordingContext();
830
+ createChannel();
831
+ ForegroundService service = buildService().create().get();
832
+ service.onStartCommand(buildStartIntent("same", 133, "A"), 0, 1);
833
+ Intent update = buildStartIntent("same", 133, "B");
834
+ ForegroundService.start(
835
+ 133,
836
+ update.getParcelableExtra("notification"),
837
+ update.getBundleExtra("notificationBundle"));
838
+ assertThrows(
839
+ SecurityException.class, () -> service.onStartCommand(context.starts.get(0), 0, 2));
840
+ assertCurrentState("same", 133, "A");
841
+ ForegroundService.stop();
842
+ idleMain();
843
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
844
+ assertNull(ForegroundService.mCurrentNotificationId);
845
+ }
846
+
847
+ @Test
848
+ @Config(sdk = 34)
849
+ public void onTimeout_oldRunOnSameInstance_preservesNewRun() throws Exception {
850
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
851
+ createChannel();
852
+ ForegroundService service = buildService().create().get();
853
+ service.onStartCommand(
854
+ buildStartIntent("old", 134, "old", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
855
+ 0,
856
+ 1);
857
+ service.onStartCommand(buildStopIntent(), 0, 2);
858
+ service.onStartCommand(
859
+ buildStartIntent(
860
+ "new", 135, "new title", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
861
+ 0,
862
+ 3);
863
+ FgsEventCapture capture = new FgsEventCapture();
864
+ EventBus.register(capture);
865
+ try {
866
+ service.onTimeout(1);
867
+ assertCurrentState("new", 135, "new title");
868
+ assertEquals(
869
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
870
+ ForegroundService.mCurrentForegroundServiceType);
871
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
872
+ assertTrue(capture.events.isEmpty());
873
+ } finally {
874
+ EventBus.unregister(capture);
875
+ }
876
+ }
877
+
878
+ @Test
879
+ @Config(sdk = 34)
880
+ public void onTimeout_sameRunEarlierStartId_preservesLatestUpdateTimeoutBehavior()
881
+ throws Exception {
882
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
883
+ createChannel();
884
+ ForegroundService service = buildService().create().get();
885
+ service.onStartCommand(
886
+ buildStartIntent("same", 136, "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
887
+ 0,
888
+ 1);
889
+ service.onStartCommand(
890
+ buildStartIntent("same", 136, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
891
+ 0,
892
+ 2);
893
+ FgsEventCapture capture = new FgsEventCapture();
894
+ EventBus.register(capture);
895
+ try {
896
+ service.onTimeout(1);
897
+ assertNull(ForegroundService.mCurrentNotificationId);
898
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
899
+ assertEquals(1, capture.events.size());
900
+ assertEquals("B", capture.events.get(0).getNotification().toBundle().getString("title"));
901
+ } finally {
902
+ EventBus.unregister(capture);
903
+ }
904
+ }
905
+
906
+ @Test
907
+ @Config(sdk = 33)
908
+ public void start_serviceDeliveryBeforeDispatchReturns_canFulfillRealContract() throws Exception {
909
+ CountDownLatch dispatchEntered = new CountDownLatch(1);
910
+ CountDownLatch promotionObserved = new CountDownLatch(1);
911
+ AtomicReference<Boolean> blocked = new AtomicReference<>(false);
912
+ RecordingServiceContext context =
913
+ new RecordingServiceContext(RuntimeEnvironment.getApplication()) {
914
+ @Override
915
+ public ComponentName startForegroundService(Intent intent) {
916
+ ComponentName result = super.startForegroundService(intent);
917
+ dispatchEntered.countDown();
918
+ try {
919
+ // The timeout releases the simulated Binder reply even for the broken candidate.
920
+ blocked.set(!promotionObserved.await(2, TimeUnit.SECONDS));
921
+ } catch (InterruptedException e) {
922
+ throw new AssertionError(e);
923
+ }
924
+ return result;
925
+ }
926
+ };
927
+ ContextHolder.setApplicationContext(context);
928
+ createChannel();
929
+ Notification notification = buildNotification();
930
+ Bundle bundle = buildNotificationBundle("pending");
931
+ ExecutorService worker = Executors.newSingleThreadExecutor();
932
+ try {
933
+ Future<?> start = worker.submit(() -> ForegroundService.start(137, notification, bundle));
934
+ assertTrue(dispatchEntered.await(5, TimeUnit.SECONDS));
935
+ ForegroundService service = buildService().create().get();
936
+ service.onStartCommand(context.starts.get(0), 0, 1);
937
+ promotionObserved.countDown();
938
+ start.get(5, TimeUnit.SECONDS);
939
+ assertFalse("Service promotion waited for the dispatch caller", blocked.get());
940
+ assertEquals(
941
+ 137, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
942
+ } finally {
943
+ promotionObserved.countDown();
944
+ worker.shutdownNow();
945
+ }
946
+ }
947
+
948
+ @Test
949
+ @Config(sdk = 33)
950
+ public void start_ambiguousDispatchFailure_retainsRealObligationUntilDelivery() {
951
+ RecordingServiceContext context = useRecordingContext();
952
+ context.startFailure = new IllegalStateException("Binder outcome unknown");
953
+ assertThrows(
954
+ IllegalStateException.class,
955
+ () ->
956
+ ForegroundService.start(138, buildNotification(), buildNotificationBundle("pending")));
957
+ ForegroundService.stop();
958
+ ForegroundService service = buildService().create().get();
959
+ service.onStartCommand(null, 0, 1);
960
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
961
+ assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
962
+ createChannel();
963
+ service.onStartCommand(context.starts.get(0), 0, 2);
964
+ idleMain();
965
+ assertEquals(138, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
966
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
967
+ }
968
+
969
+ @Test
970
+ @Config(sdk = 33)
971
+ public void onStartCommand_oldAmbiguousDelivery_preservesNewForegroundRun() throws Exception {
972
+ RecordingServiceContext context = useRecordingContext();
973
+ createChannel();
974
+ context.startFailure = new IllegalStateException("old reply unknown");
975
+ assertThrows(
976
+ IllegalStateException.class,
977
+ () -> ForegroundService.start(139, buildNotification(), buildNotificationBundle("old")));
978
+ ForegroundService.stop();
979
+ context.startFailure = null;
980
+ Intent fresh = buildStartIntent("new", 140, "new title");
981
+ ForegroundService.start(
982
+ 140, fresh.getParcelableExtra("notification"), fresh.getBundleExtra("notificationBundle"));
983
+ FgsRunnerCapture capture = new FgsRunnerCapture();
984
+ EventBus.register(capture);
985
+ try {
986
+ ForegroundService service = buildService().create().get();
987
+ service.onStartCommand(context.starts.get(1), 0, 1);
988
+ service.onStartCommand(context.starts.get(0), 0, 2);
989
+ idleMain();
990
+ assertCurrentState("new", 140, "new title");
991
+ assertEquals(1, capture.events.size());
992
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
993
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
994
+ capture.events.get(0).setCompletionResult();
995
+ assertNull(ForegroundService.mCurrentNotificationId);
996
+ } finally {
997
+ EventBus.unregister(capture);
998
+ }
999
+ }
1000
+
1001
+ @Test
1002
+ @Config(sdk = 33)
1003
+ public void onStartCommand_oldAmbiguousDeliveryAfterNewRunnerEnds_fulfillsThenStopsOnlyOldWork() {
1004
+ RecordingServiceContext context = useRecordingContext();
1005
+ createChannel();
1006
+ context.startFailure = new IllegalStateException("old reply unknown");
1007
+ assertThrows(
1008
+ IllegalStateException.class,
1009
+ () -> ForegroundService.start(141, buildNotification(), buildNotificationBundle("old")));
1010
+ ForegroundService.stop();
1011
+ context.startFailure = null;
1012
+ ForegroundService.start(142, buildNotification(), buildNotificationBundle("new"));
1013
+ FgsRunnerCapture capture = new FgsRunnerCapture();
1014
+ EventBus.register(capture);
1015
+ try {
1016
+ ForegroundService service = buildService().create().get();
1017
+ service.onStartCommand(context.starts.get(1), 0, 1);
1018
+ capture.events.get(0).setCompletionResult();
1019
+ service.onStartCommand(context.starts.get(0), 0, 2);
1020
+ idleMain();
1021
+ assertEquals(
1022
+ 141, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
1023
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
1024
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
1025
+ assertNull(ForegroundService.mCurrentNotificationId);
1026
+ assertEquals(1, capture.events.size());
1027
+ } finally {
1028
+ EventBus.unregister(capture);
1029
+ }
1030
+ }
1031
+
1032
+ @Test
1033
+ @Config(sdk = 33)
1034
+ public void start_lateDispatchReply_doesNotCancelStopAfterRealDelivery() throws Exception {
1035
+ CountDownLatch dispatchEntered = new CountDownLatch(1);
1036
+ CountDownLatch releaseReply = new CountDownLatch(1);
1037
+ RecordingServiceContext context =
1038
+ new RecordingServiceContext(RuntimeEnvironment.getApplication()) {
1039
+ @Override
1040
+ public ComponentName startForegroundService(Intent intent) {
1041
+ ComponentName result = super.startForegroundService(intent);
1042
+ dispatchEntered.countDown();
1043
+ try {
1044
+ assertTrue(releaseReply.await(5, TimeUnit.SECONDS));
1045
+ } catch (InterruptedException e) {
1046
+ throw new AssertionError(e);
1047
+ }
1048
+ return result;
1049
+ }
1050
+ };
1051
+ ContextHolder.setApplicationContext(context);
1052
+ createChannel();
1053
+ ExecutorService worker = Executors.newSingleThreadExecutor();
1054
+ Notification notification = buildNotification();
1055
+ Bundle bundle = buildNotificationBundle("pending");
1056
+ try {
1057
+ Future<?> start = worker.submit(() -> ForegroundService.start(143, notification, bundle));
1058
+ assertTrue(dispatchEntered.await(5, TimeUnit.SECONDS));
1059
+ ForegroundService service = buildService().create().get();
1060
+ service.onStartCommand(context.starts.get(0), 0, 1);
1061
+ ForegroundService.stop();
1062
+ releaseReply.countDown();
1063
+ start.get(5, TimeUnit.SECONDS);
1064
+ idleMain();
1065
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
1066
+ assertNull(ForegroundService.mCurrentNotificationId);
1067
+ } finally {
1068
+ releaseReply.countDown();
1069
+ worker.shutdownNow();
1070
+ }
1071
+ }
1072
+
1073
+ @Test
1074
+ @Config(
1075
+ sdk = {33, 34},
1076
+ shadows = DenyOldNotificationShadowService.class)
1077
+ public void onStartCommand_supersededStartDenial_remainsARealStartFailure() throws Exception {
1078
+ if (Build.VERSION.SDK_INT >= 34) {
1079
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
1080
+ }
1081
+ RecordingServiceContext context = useRecordingContext();
1082
+ createChannel();
1083
+ Intent old = buildStartIntent("old", 144, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
1084
+ context.startFailure = new IllegalStateException("old reply unknown");
1085
+ assertThrows(
1086
+ IllegalStateException.class,
1087
+ () ->
1088
+ ForegroundService.start(
1089
+ 144,
1090
+ old.getParcelableExtra("notification"),
1091
+ old.getBundleExtra("notificationBundle")));
1092
+ ForegroundService.stop();
1093
+ context.startFailure = null;
1094
+ ForegroundService.start(145, buildNotification(), buildNotificationBundle("new"));
1095
+ FgsRunnerCapture capture = new FgsRunnerCapture();
1096
+ EventBus.register(capture);
1097
+ try {
1098
+ ForegroundService service = buildService().create().get();
1099
+ service.onStartCommand(context.starts.get(1), 0, 1);
1100
+ capture.events.get(0).setCompletionResult();
1101
+ assertThrows(
1102
+ ForegroundServiceStartNotAllowedException.class,
1103
+ () -> service.onStartCommand(context.starts.get(0), 0, 2));
1104
+ idleMain();
1105
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
1106
+ assertNull(ForegroundService.mCurrentNotificationId);
1107
+ } finally {
1108
+ EventBus.unregister(capture);
1109
+ }
1110
+ }
1111
+
1112
+ @Implements(Service.class)
1113
+ public static class DenyOldNotificationShadowService extends ShadowService {
1114
+ @Implementation
1115
+ protected void startForeground(int id, Notification notification) {
1116
+ if ("B".equals(notification.extras.getString(Notification.EXTRA_TITLE))) {
1117
+ throw new ForegroundServiceStartNotAllowedException("real old START denied");
1118
+ }
1119
+ super.startForeground(id, notification);
1120
+ }
1121
+ }
1122
+
1123
+ @Implements(Service.class)
1124
+ public static class RefuseStopSelfShadowService extends ShadowService {
1125
+ @Implementation
1126
+ protected boolean stopSelfResult(int startId) {
1127
+ return false;
1128
+ }
1129
+ }
1130
+
1131
+ @Implements(Service.class)
1132
+ public static class FailStopForegroundShadowService extends ShadowService {
1133
+ @Implementation
1134
+ protected void stopForeground(int flags) {
1135
+ throw new ForegroundServiceStartNotAllowedException("not from startForeground");
1136
+ }
1137
+ }
1138
+
1139
+ private static void assertCurrentState(String id, int hash, String title) throws Exception {
1140
+ assertEquals(id, ForegroundService.mCurrentNotificationId);
1141
+ assertEquals(hash, getPrivateStatic("mCurrentHashCode"));
1142
+ assertEquals(
1143
+ title, ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
1144
+ assertEquals(
1145
+ title,
1146
+ ((Notification) getPrivateStatic("mCurrentNotification"))
1147
+ .extras.getString(Notification.EXTRA_TITLE));
1148
+ }
1149
+
1150
+ private static void idleMain() {
1151
+ org.robolectric.Shadows.shadowOf(Looper.getMainLooper()).idle();
1152
+ }
1153
+
1154
+ private RecordingServiceContext useRecordingContext() {
1155
+ RecordingServiceContext context =
1156
+ new RecordingServiceContext(RuntimeEnvironment.getApplication());
1157
+ ContextHolder.setApplicationContext(context);
1158
+ return context;
1159
+ }
1160
+
1161
+ private ServiceController<ForegroundService> buildService() {
1162
+ ServiceController<ForegroundService> controller =
1163
+ Robolectric.buildService(ForegroundService.class);
1164
+ controllers.add(controller);
1165
+ return controller;
1166
+ }
1167
+
1168
+ private static class RecordingServiceContext extends ContextWrapper {
1169
+ final List<Intent> starts = new ArrayList<>();
1170
+ int stops;
1171
+ RuntimeException startFailure;
1172
+
1173
+ RecordingServiceContext(Context base) {
1174
+ super(base);
1175
+ }
1176
+
1177
+ @Override
1178
+ public ComponentName startService(Intent intent) {
1179
+ starts.add(new Intent(intent));
1180
+ return intent.getComponent();
1181
+ }
1182
+
1183
+ @Override
1184
+ public ComponentName startForegroundService(Intent intent) {
1185
+ starts.add(new Intent(intent));
1186
+ if (startFailure != null) {
1187
+ throw startFailure;
1188
+ }
1189
+ return intent.getComponent();
1190
+ }
1191
+
1192
+ @Override
1193
+ public boolean stopService(Intent intent) {
1194
+ stops++;
1195
+ return true;
1196
+ }
1197
+ }
1198
+
1199
+ @Implements(Service.class)
1200
+ public static class DefensiveFailureShadowService extends ShadowService {
1201
+ static RuntimeException failure;
1202
+ static int attempts;
1203
+ static int lastId;
1204
+ static int lastType;
1205
+
1206
+ @Implementation
1207
+ protected void startForeground(int id, Notification notification) {
1208
+ attempts++;
1209
+ lastId = id;
1210
+ lastType = ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE;
1211
+ throw failure;
1212
+ }
1213
+
1214
+ @Implementation
1215
+ protected void startForeground(int id, Notification notification, int foregroundServiceType) {
1216
+ attempts++;
1217
+ lastId = id;
1218
+ lastType = foregroundServiceType;
1219
+ throw failure;
1220
+ }
1221
+ }
1222
+
1223
+ @Implements(NotificationManager.class)
1224
+ public static class SecurityDefensiveChannelShadowManager extends ShadowNotificationManager {
1225
+ @Implementation
1226
+ protected void createNotificationChannel(NotificationChannel channel) {
1227
+ if ("notifee_fg_default".equals(channel.getId())) {
1228
+ throw new SecurityException("defensive channel failure");
1229
+ }
1230
+ super.createNotificationChannel(channel);
1231
+ }
1232
+ }
1233
+
1234
+ @Implements(Service.class)
1235
+ public static class SecurityStopForegroundShadowService extends ShadowService {
1236
+ @Implementation
1237
+ protected void stopForeground(int flags) {
1238
+ throw new SecurityException("defensive teardown failure");
1239
+ }
1240
+ }
1241
+
64
1242
  /**
65
1243
  * Regression test for Bug #1: a STOP intent arriving on a fresh service instance that has never
66
1244
  * called startForeground() must not crash. The defensive startForeground() path should fire,
@@ -68,8 +1246,7 @@ public class ForegroundServiceTest {
68
1246
  */
69
1247
  @Test
70
1248
  public void onStartCommand_stopIntentBeforeStart_doesNotCrash() {
71
- ServiceController<ForegroundService> controller =
72
- Robolectric.buildService(ForegroundService.class);
1249
+ ServiceController<ForegroundService> controller = buildService();
73
1250
  ForegroundService service = controller.create().get();
74
1251
 
75
1252
  Intent stopIntent = new Intent();
@@ -86,8 +1263,7 @@ public class ForegroundServiceTest {
86
1263
  */
87
1264
  @Test
88
1265
  public void onStartCommand_nullIntent_doesNotCrash() {
89
- ServiceController<ForegroundService> controller =
90
- Robolectric.buildService(ForegroundService.class);
1266
+ ServiceController<ForegroundService> controller = buildService();
91
1267
  ForegroundService service = controller.create().get();
92
1268
 
93
1269
  // Null intent simulates service recreation after process kill
@@ -103,8 +1279,7 @@ public class ForegroundServiceTest {
103
1279
  */
104
1280
  @Test
105
1281
  public void onStartCommand_stopIntent_resetsPublicStaticState() {
106
- ServiceController<ForegroundService> controller =
107
- Robolectric.buildService(ForegroundService.class);
1282
+ ServiceController<ForegroundService> controller = buildService();
108
1283
  ForegroundService service = controller.create().get();
109
1284
 
110
1285
  // Set some stale state to simulate a prior invocation
@@ -132,8 +1307,7 @@ public class ForegroundServiceTest {
132
1307
  public void onStartCommand_stopIntentApi34NoManifestType_throwsRuntimeException() {
133
1308
  // Robolectric's default shadow PackageManager returns FOREGROUND_SERVICE_TYPE_NONE (0)
134
1309
  // for services without an explicit foregroundServiceType in the test manifest.
135
- ServiceController<ForegroundService> controller =
136
- Robolectric.buildService(ForegroundService.class);
1310
+ ServiceController<ForegroundService> controller = buildService();
137
1311
  ForegroundService service = controller.create().get();
138
1312
 
139
1313
  Intent stopIntent = new Intent();
@@ -148,8 +1322,7 @@ public class ForegroundServiceTest {
148
1322
  @Test
149
1323
  @Config(sdk = 34)
150
1324
  public void onStartCommand_stopIntentApi34NoManifestType_messageContainsDocUrl() {
151
- ServiceController<ForegroundService> controller =
152
- Robolectric.buildService(ForegroundService.class);
1325
+ ServiceController<ForegroundService> controller = buildService();
153
1326
  ForegroundService service = controller.create().get();
154
1327
 
155
1328
  Intent stopIntent = new Intent();
@@ -171,8 +1344,7 @@ public class ForegroundServiceTest {
171
1344
  @Test
172
1345
  @Config(sdk = 33)
173
1346
  public void onStartCommand_stopIntentApiBelow34_defensivePathRunsNormally() {
174
- ServiceController<ForegroundService> controller =
175
- Robolectric.buildService(ForegroundService.class);
1347
+ ServiceController<ForegroundService> controller = buildService();
176
1348
  ForegroundService service = controller.create().get();
177
1349
 
178
1350
  Intent stopIntent = new Intent();
@@ -192,8 +1364,7 @@ public class ForegroundServiceTest {
192
1364
  @Test
193
1365
  @Config(sdk = 33)
194
1366
  public void onStartCommand_stopIntentAfterSuccessfulStart_skipsDefensivePath() {
195
- ServiceController<ForegroundService> controller =
196
- Robolectric.buildService(ForegroundService.class);
1367
+ ServiceController<ForegroundService> controller = buildService();
197
1368
  ForegroundService service = controller.create().get();
198
1369
 
199
1370
  Intent stopIntent = new Intent();
@@ -214,8 +1385,7 @@ public class ForegroundServiceTest {
214
1385
  declareForegroundServiceTypes(
215
1386
  ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
216
1387
  | ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
217
- ServiceController<ForegroundService> controller =
218
- Robolectric.buildService(ForegroundService.class);
1388
+ ServiceController<ForegroundService> controller = buildService();
219
1389
  ForegroundService service = controller.create().get();
220
1390
 
221
1391
  int result = service.onStartCommand(buildStopIntent(), 0, 1);
@@ -231,8 +1401,7 @@ public class ForegroundServiceTest {
231
1401
  throws Exception {
232
1402
  declareForegroundServiceTypes(
233
1403
  ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA | ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
234
- ServiceController<ForegroundService> controller =
235
- Robolectric.buildService(ForegroundService.class);
1404
+ ServiceController<ForegroundService> controller = buildService();
236
1405
  ForegroundService service = controller.create().get();
237
1406
 
238
1407
  int result = service.onStartCommand(buildStopIntent(), 0, 1);
@@ -250,8 +1419,7 @@ public class ForegroundServiceTest {
250
1419
  declareForegroundServiceTypes(
251
1420
  ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
252
1421
  | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
253
- ServiceController<ForegroundService> controller =
254
- Robolectric.buildService(ForegroundService.class);
1422
+ ServiceController<ForegroundService> controller = buildService();
255
1423
  ForegroundService service = controller.create().get();
256
1424
 
257
1425
  int result = service.onStartCommand(buildStopIntent(), 0, 1);
@@ -266,8 +1434,7 @@ public class ForegroundServiceTest {
266
1434
  public void onStartCommand_stopIntentApi34MicrophoneOnly_doesNotUseUndeclaredShortService()
267
1435
  throws Exception {
268
1436
  declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
269
- ServiceController<ForegroundService> controller =
270
- Robolectric.buildService(ForegroundService.class);
1437
+ ServiceController<ForegroundService> controller = buildService();
271
1438
  ForegroundService service = controller.create().get();
272
1439
 
273
1440
  int result = service.onStartCommand(buildStopIntent(), 0, 1);
@@ -281,8 +1448,7 @@ public class ForegroundServiceTest {
281
1448
  @Config(sdk = 34, shadows = ThrowingStartForegroundShadowService.class)
282
1449
  public void onStartCommand_stopIntentApi34SecurityException_doesNotCrash() throws Exception {
283
1450
  declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
284
- ServiceController<ForegroundService> controller =
285
- Robolectric.buildService(ForegroundService.class);
1451
+ ServiceController<ForegroundService> controller = buildService();
286
1452
  ForegroundService service = controller.create().get();
287
1453
 
288
1454
  int result = service.onStartCommand(buildStopIntent(), 0, 1);
@@ -308,8 +1474,7 @@ public class ForegroundServiceTest {
308
1474
  public void onStartCommand_startIntent_setsCurrentNotificationIdAndCallsStartForeground()
309
1475
  throws Exception {
310
1476
  createChannel();
311
- ServiceController<ForegroundService> controller =
312
- Robolectric.buildService(ForegroundService.class);
1477
+ ServiceController<ForegroundService> controller = buildService();
313
1478
  ForegroundService service = controller.create().get();
314
1479
 
315
1480
  String id = "fgs-start-happy";
@@ -814,8 +1979,7 @@ public class ForegroundServiceTest {
814
1979
  @Test
815
1980
  @Config(sdk = 34)
816
1981
  public void onTimeout_api34_emitsFgTimeoutEventWithStartIdAndSentinelFgsType() throws Exception {
817
- ServiceController<ForegroundService> controller =
818
- Robolectric.buildService(ForegroundService.class);
1982
+ ServiceController<ForegroundService> controller = buildService();
819
1983
  ForegroundService service = controller.create().get();
820
1984
 
821
1985
  String id = "fgs-timeout-api34";
@@ -857,8 +2021,7 @@ public class ForegroundServiceTest {
857
2021
  @Test
858
2022
  @Config(sdk = 35)
859
2023
  public void onTimeout_api35_emitsFgTimeoutEventWithExplicitFgsType() throws Exception {
860
- ServiceController<ForegroundService> controller =
861
- Robolectric.buildService(ForegroundService.class);
2024
+ ServiceController<ForegroundService> controller = buildService();
862
2025
  ForegroundService service = controller.create().get();
863
2026
 
864
2027
  String id = "fgs-timeout-api35";
@@ -894,8 +2057,7 @@ public class ForegroundServiceTest {
894
2057
  @Test
895
2058
  @Config(sdk = 34)
896
2059
  public void onTimeout_withNoActiveState_doesNotCrashOrEmitEvent() {
897
- ServiceController<ForegroundService> controller =
898
- Robolectric.buildService(ForegroundService.class);
2060
+ ServiceController<ForegroundService> controller = buildService();
899
2061
  ForegroundService service = controller.create().get();
900
2062
 
901
2063
  FgsEventCapture capture = new FgsEventCapture();