react-native-notify-kit 10.8.0 → 10.8.1

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.
@@ -4,6 +4,7 @@ import static org.junit.Assert.assertEquals;
4
4
  import static org.junit.Assert.assertFalse;
5
5
  import static org.junit.Assert.assertNotNull;
6
6
  import static org.junit.Assert.assertNull;
7
+ import static org.junit.Assert.assertSame;
7
8
  import static org.junit.Assert.assertThrows;
8
9
  import static org.junit.Assert.assertTrue;
9
10
  import static org.junit.Assert.fail;
@@ -31,6 +32,8 @@ import java.lang.reflect.Field;
31
32
  import java.lang.reflect.Method;
32
33
  import java.util.ArrayList;
33
34
  import java.util.List;
35
+ import java.util.Map;
36
+ import java.util.UUID;
34
37
  import java.util.concurrent.CountDownLatch;
35
38
  import java.util.concurrent.ExecutorService;
36
39
  import java.util.concurrent.Executors;
@@ -72,14 +75,3079 @@ public class ForegroundServiceTest {
72
75
  }
73
76
  org.robolectric.Shadows.shadowOf(Looper.getMainLooper()).idle();
74
77
  ContextHolder.setApplicationContext(RuntimeEnvironment.getApplication());
75
- // Reset public and private static fields to prevent cross-test pollution. The three private
76
- // statics are cleared via reflection here so that tests which seed them directly (to exercise
77
- // onTimeout without running the full START path) cannot leak state into neighbouring tests.
78
+ // Reset caches and lifecycle registries only after runtime observations, so preserved
79
+ // UNKNOWN obligations and services created directly cannot leak into neighbouring tests.
78
80
  ForegroundService.mCurrentNotificationId = null;
79
81
  ForegroundService.mCurrentForegroundServiceType = -1;
80
82
  setPrivateStatic("mCurrentNotificationBundle", null);
81
83
  setPrivateStatic("mCurrentNotification", null);
82
84
  setPrivateStatic("mCurrentHashCode", 0);
85
+ // Undelivered UNKNOWN obligations must survive runtime teardown, but never leak across tests.
86
+ issue81Pending().clear();
87
+ setPrivateStatic("sStopRequest", null);
88
+ setPrivateStatic("sRunGeneration", 0L);
89
+ setPrivateStatic("sInstance", null);
90
+ setPrivateStatic("sStateOwner", null);
91
+ SliceAShadowService.reset();
92
+ }
93
+
94
+ // Slice A's platform model deliberately separates acceptance, delivery, promotion and stop.
95
+ // Its state is a host oracle, not a simulation claiming to reproduce AMS/Binder semantics.
96
+ private ForegroundService sliceAService(int type) throws Exception {
97
+ SliceAShadowService.reset();
98
+ declareForegroundServiceTypes(type);
99
+ createChannel();
100
+ ForegroundService service = buildService().create().get();
101
+ sliceADeliver(service, buildStartIntent("slice-a", 820, "A", type), 1);
102
+ return service;
103
+ }
104
+
105
+ private static SliceAShadowService sliceAPlatform(ForegroundService service) {
106
+ return (SliceAShadowService) org.robolectric.Shadows.shadowOf(service);
107
+ }
108
+
109
+ private static void sliceADeliver(ForegroundService service, Intent intent, int id) {
110
+ SliceAShadowService platform = sliceAPlatform(service);
111
+ platform.accepted = Math.max(platform.accepted, id);
112
+ platform.delivered = id;
113
+ service.onStartCommand(intent, 0, id);
114
+ }
115
+
116
+ private static void sliceATimeout(ForegroundService service, int id) {
117
+ service.onTimeout(id, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
118
+ }
119
+
120
+ @Test
121
+ @Config(
122
+ sdk = {34, 35},
123
+ shadows = SliceAShadowService.class)
124
+ public void sliceA_demotionPrecedesDestructiveRetirementAndEventWork() throws Exception {
125
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
126
+ Object runner = issue81Field(service, "mRunnerToken");
127
+ SliceAShadowService.beforeDemotion =
128
+ () -> {
129
+ try {
130
+ assertCurrentState("slice-a", 820, "A");
131
+ assertSame(runner, issue81Field(service, "mRunnerToken"));
132
+ assertTrue(SliceAShadowService.order.isEmpty());
133
+ } catch (Exception e) {
134
+ throw new AssertionError(e);
135
+ }
136
+ };
137
+ SliceAEvents events = new SliceAEvents();
138
+ EventBus.register(events);
139
+ try {
140
+ sliceATimeout(service, 1);
141
+ assertEquals(
142
+ java.util.Arrays.asList("demote", "stop:1", "event"),
143
+ SliceAShadowService.order.stream()
144
+ .filter(x -> !x.equals("log"))
145
+ .collect(java.util.stream.Collectors.toList()));
146
+ assertEquals(1, events.events.size());
147
+ assertEquals(9, events.events.get(0).getType());
148
+ assertFalse(sliceAPlatform(service).foreground);
149
+ assertTrue(sliceAPlatform(service).stopped);
150
+ assertNull(ForegroundService.mCurrentNotificationId);
151
+ } finally {
152
+ EventBus.unregister(events);
153
+ }
154
+ }
155
+
156
+ @Test
157
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
158
+ public void sliceA_oldInstanceAndOldRunPreserveSuccessor() throws Exception {
159
+ ForegroundService old = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
160
+ ForegroundService current = buildService().create().get();
161
+ sliceADeliver(
162
+ current,
163
+ buildStartIntent("new", 821, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
164
+ 2);
165
+ SliceAShadowService.order.clear();
166
+ sliceATimeout(old, 1);
167
+ assertTrue(SliceAShadowService.order.isEmpty());
168
+ assertCurrentState("new", 821, "B");
169
+ sliceADeliver(current, buildStopIntent(), 3);
170
+ sliceADeliver(
171
+ current,
172
+ buildStartIntent("new", 821, "C", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
173
+ 4);
174
+ SliceAShadowService.order.clear();
175
+ sliceATimeout(current, 2);
176
+ assertTrue(SliceAShadowService.order.isEmpty());
177
+ assertCurrentState("new", 821, "C");
178
+ }
179
+
180
+ @Test
181
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
182
+ public void sliceA_ambiguousCompatibleCallbackDemotesWithoutSuccessorStop() throws Exception {
183
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
184
+ // No immutable Android epoch is available for this coincident callback ID after type promotion.
185
+ sliceADeliver(
186
+ service,
187
+ buildStartIntent(
188
+ "slice-a",
189
+ 820,
190
+ "B",
191
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
192
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK),
193
+ 2);
194
+ SliceAShadowService.order.clear();
195
+ sliceATimeout(service, 2);
196
+ assertFalse(sliceAPlatform(service).foreground);
197
+ assertFalse(sliceAPlatform(service).stopped);
198
+ assertFalse(SliceAShadowService.order.stream().anyMatch(x -> x.startsWith("stop:")));
199
+ }
200
+
201
+ @Test
202
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
203
+ public void sliceA_unknownCallbackIdCompatibleDemotesWithoutBorrowingDeliveredId()
204
+ throws Exception {
205
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
206
+ sliceATimeout(service, 99);
207
+ assertFalse(sliceAPlatform(service).foreground);
208
+ assertFalse(sliceAPlatform(service).stopped);
209
+ assertFalse(SliceAShadowService.order.stream().anyMatch(x -> x.startsWith("stop:")));
210
+ }
211
+
212
+ @Test
213
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
214
+ public void sliceA_incompatibleTypeAndSingleArgumentDoNothing() throws Exception {
215
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
216
+ service.onTimeout(1, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
217
+ service.onTimeout(1);
218
+ assertTrue(SliceAShadowService.order.isEmpty());
219
+ assertTrue(sliceAPlatform(service).foreground);
220
+ assertCurrentState("slice-a", 820, "A");
221
+ }
222
+
223
+ @Test
224
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
225
+ public void sliceA_reservedUnknownAndAcceptedUndeliveredStartsRemainHeld() throws Exception {
226
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
227
+ RecordingServiceContext context = useRecordingContext();
228
+ context.startFailure = new RuntimeException("lost Binder reply");
229
+ assertThrows(
230
+ RuntimeException.class,
231
+ () ->
232
+ ForegroundService.start(821, buildNotification(), buildNotificationBundle("pending")));
233
+ Object pending = issue81Pending().values().iterator().next();
234
+ sliceAPlatform(service).accepted = 2;
235
+ sliceATimeout(service, 1);
236
+ assertEquals(1, issue81Pending().size());
237
+ assertSame(pending, issue81Pending().values().iterator().next());
238
+ assertFalse(sliceAPlatform(service).foreground);
239
+ assertFalse(sliceAPlatform(service).stopped);
240
+ assertFalse(SliceAShadowService.order.stream().anyMatch(x -> x.startsWith("stop:")));
241
+ assertTrue(
242
+ ShadowLog.getLogsForTag("NOTIFEE").stream().anyMatch(x -> x.msg.contains("stop=deferred")));
243
+ }
244
+
245
+ @Test
246
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
247
+ public void sliceA_acceptedButUndeliveredAmsStartRefusesConditionalStop() throws Exception {
248
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
249
+ sliceAPlatform(service).accepted = 2; // framework accepted another START outside the registry
250
+ sliceATimeout(service, 1);
251
+ assertEquals(1, sliceAPlatform(service).delivered);
252
+ assertFalse(sliceAPlatform(service).stopped);
253
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
254
+ assertTrue(
255
+ ShadowLog.getLogsForTag("NOTIFEE").stream().anyMatch(x -> x.msg.contains("stop=refused")));
256
+ }
257
+
258
+ @Test
259
+ @Config(
260
+ sdk = {34, 35},
261
+ shadows = SliceAShadowService.class)
262
+ public void sliceA_shortServiceInitialIdStopsLatestSatisfiedSameRun() throws Exception {
263
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
264
+ sliceADeliver(
265
+ service,
266
+ buildStartIntent("slice-a", 820, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
267
+ 2);
268
+ SliceAEvents events = new SliceAEvents();
269
+ EventBus.register(events);
270
+ try {
271
+ service.onTimeout(1);
272
+ assertTrue(sliceAPlatform(service).stopped);
273
+ assertTrue(SliceAShadowService.order.contains("stop:2"));
274
+ assertEquals("B", events.events.get(0).getNotification().toBundle().getString("title"));
275
+ assertEquals(1, events.events.get(0).getExtras().getInt("startId"));
276
+ assertEquals(-1, events.events.get(0).getExtras().getInt("fgsType"));
277
+ assertEquals(1, sliceAPlatform(service).promotions);
278
+ } finally {
279
+ EventBus.unregister(events);
280
+ }
281
+ }
282
+
283
+ @Test
284
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
285
+ public void sliceA_failedUpdateRetainsLatestA() throws Exception {
286
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
287
+ SliceAShadowService.promotionFailure = new SecurityException("update B failed");
288
+ assertThrows(
289
+ SecurityException.class,
290
+ () ->
291
+ sliceADeliver(
292
+ service,
293
+ buildStartIntent(
294
+ "slice-a", 820, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK),
295
+ 2));
296
+ SliceAEvents events = new SliceAEvents();
297
+ EventBus.register(events);
298
+ try {
299
+ sliceATimeout(service, 1);
300
+ assertEquals("A", events.events.get(0).getNotification().toBundle().getString("title"));
301
+ } finally {
302
+ EventBus.unregister(events);
303
+ }
304
+ }
305
+
306
+ @Test
307
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
308
+ public void sliceA_stopRefusalIsNotSuccessAndIsNotRetried() throws Exception {
309
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
310
+ SliceAShadowService.stopResult = false;
311
+ sliceATimeout(service, 1);
312
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
313
+ sliceATimeout(service, 1);
314
+ idleMain();
315
+ assertEquals(1, SliceAShadowService.order.stream().filter(x -> x.startsWith("stop:")).count());
316
+ assertTrue(
317
+ ShadowLog.getLogsForTag("NOTIFEE").stream().anyMatch(x -> x.msg.contains("stop=refused")));
318
+ }
319
+
320
+ @Test
321
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
322
+ public void sliceA_stopThrowStillAttemptsEventAndPropagatesOriginal() throws Exception {
323
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
324
+ RuntimeException failure = new SecurityException("stop failure");
325
+ SliceAShadowService.stopFailure = failure;
326
+ SliceAEvents events = new SliceAEvents();
327
+ EventBus.register(events);
328
+ try {
329
+ assertSame(failure, assertThrows(RuntimeException.class, () -> sliceATimeout(service, 1)));
330
+ assertEquals(1, events.events.size());
331
+ assertNull(ForegroundService.mCurrentNotificationId);
332
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
333
+ sliceATimeout(service, 1);
334
+ assertEquals(1, events.events.size());
335
+ } finally {
336
+ EventBus.unregister(events);
337
+ }
338
+ }
339
+
340
+ @Test
341
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
342
+ public void sliceA_demotionThrowPreservesEvidenceAndPrimaryEvenWhenStopThrows() throws Exception {
343
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
344
+ Object runner = issue81Field(service, "mRunnerToken");
345
+ RuntimeException primary = new SecurityException("demotion failure");
346
+ RuntimeException secondary = new IllegalStateException("fallback failure");
347
+ SliceAShadowService.demotionFailure = primary;
348
+ SliceAShadowService.stopFailure = secondary;
349
+ SliceAEvents events = new SliceAEvents();
350
+ EventBus.register(events);
351
+ try {
352
+ assertSame(primary, assertThrows(RuntimeException.class, () -> sliceATimeout(service, 1)));
353
+ assertCurrentState("slice-a", 820, "A");
354
+ assertSame(runner, issue81Field(service, "mRunnerToken"));
355
+ assertTrue(sliceAPlatform(service).foreground);
356
+ assertEquals(1, events.events.size());
357
+ assertTrue(java.util.Arrays.asList(primary.getSuppressed()).contains(secondary));
358
+ sliceATimeout(service, 1);
359
+ assertEquals(1, events.events.size());
360
+ assertEquals(1, SliceAShadowService.order.stream().filter(x -> x.equals("demote")).count());
361
+ } finally {
362
+ EventBus.unregister(events);
363
+ }
364
+ }
365
+
366
+ @Test
367
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
368
+ public void sliceA_demotionThrowFallbackAcceptedRetiresButStillThrows() throws Exception {
369
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
370
+ RuntimeException failure = new SecurityException("demotion failure");
371
+ SliceAShadowService.demotionFailure = failure;
372
+ assertSame(failure, assertThrows(RuntimeException.class, () -> sliceATimeout(service, 1)));
373
+ assertTrue(sliceAPlatform(service).stopped);
374
+ assertTrue((boolean) issue81Field(service, "mStopInitiated"));
375
+ assertNull(ForegroundService.mCurrentNotificationId);
376
+ }
377
+
378
+ @Test
379
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
380
+ public void sliceA_demotionThrowWithPendingDoesNotFallback() throws Exception {
381
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
382
+ useRecordingContext();
383
+ ForegroundService.start(821, buildNotification(), buildNotificationBundle("pending"));
384
+ sliceAPlatform(service).accepted = 2;
385
+ RuntimeException failure = new SecurityException("demotion failure");
386
+ SliceAShadowService.demotionFailure = failure;
387
+ assertSame(failure, assertThrows(RuntimeException.class, () -> sliceATimeout(service, 1)));
388
+ assertCurrentState("slice-a", 820, "A");
389
+ assertEquals(1, issue81Pending().size());
390
+ assertFalse(SliceAShadowService.order.stream().anyMatch(x -> x.startsWith("stop:")));
391
+ }
392
+
393
+ @Test
394
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
395
+ public void sliceA_duplicatesAcrossOverloadsAttemptOnce() throws Exception {
396
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
397
+ SliceAEvents events = new SliceAEvents();
398
+ EventBus.register(events);
399
+ try {
400
+ service.onTimeout(1);
401
+ service.onTimeout(1, ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
402
+ service.onTimeout(1);
403
+ assertEquals(1, events.events.size());
404
+ assertEquals(1, SliceAShadowService.order.stream().filter(x -> x.equals("demote")).count());
405
+ assertEquals(
406
+ 1, SliceAShadowService.order.stream().filter(x -> x.startsWith("stop:")).count());
407
+ } finally {
408
+ EventBus.unregister(events);
409
+ }
410
+ }
411
+
412
+ @Test
413
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
414
+ public void sliceA_multipleTypeMaskPreservesOriginalMetadata() throws Exception {
415
+ int mask =
416
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
417
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK;
418
+ ForegroundService service = sliceAService(mask);
419
+ SliceAEvents events = new SliceAEvents();
420
+ EventBus.register(events);
421
+ try {
422
+ service.onTimeout(1, mask);
423
+ assertFalse(sliceAPlatform(service).foreground);
424
+ assertEquals(mask, events.events.get(0).getExtras().getInt("fgsType"));
425
+ } finally {
426
+ EventBus.unregister(events);
427
+ }
428
+ }
429
+
430
+ @Test
431
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
432
+ public void sliceA_reentrantPromotionAfterDemotionProtectsSuccessor() throws Exception {
433
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
434
+ SliceAShadowService.afterDemotion =
435
+ () ->
436
+ sliceADeliver(
437
+ service,
438
+ buildStartIntent(
439
+ "slice-a", 820, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
440
+ 2);
441
+ SliceAEvents events = new SliceAEvents();
442
+ EventBus.register(events);
443
+ try {
444
+ sliceATimeout(service, 1);
445
+ assertCurrentState("slice-a", 820, "B");
446
+ assertTrue(sliceAPlatform(service).foreground);
447
+ assertTrue((boolean) issue81Field(service, "mForegroundActive"));
448
+ assertNotNull(issue81Field(service, "mRunnerToken"));
449
+ assertFalse(sliceAPlatform(service).stopped);
450
+ assertFalse(SliceAShadowService.order.stream().anyMatch(x -> x.startsWith("stop:")));
451
+ assertEquals("A", events.events.get(0).getNotification().toBundle().getString("title"));
452
+ } finally {
453
+ EventBus.unregister(events);
454
+ }
455
+ }
456
+
457
+ @Test
458
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
459
+ public void sliceA_reentrantReservationDuringDemotionDefersStop() throws Exception {
460
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
461
+ useRecordingContext();
462
+ SliceAShadowService.afterDemotion =
463
+ () -> {
464
+ ForegroundService.start(821, buildNotification(), buildNotificationBundle("pending"));
465
+ sliceAPlatform(service).accepted = 2;
466
+ };
467
+ sliceATimeout(service, 1);
468
+ assertEquals(1, issue81Pending().size());
469
+ assertFalse(sliceAPlatform(service).stopped);
470
+ assertFalse(SliceAShadowService.order.stream().anyMatch(x -> x.startsWith("stop:")));
471
+ }
472
+
473
+ @Test
474
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
475
+ public void sliceA_reentrantStopReplyCannotMarkSuccessorStopped() throws Exception {
476
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
477
+ SliceAShadowService.afterStop =
478
+ () ->
479
+ sliceADeliver(
480
+ service,
481
+ buildStartIntent("new", 821, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
482
+ 2);
483
+ sliceATimeout(service, 1);
484
+ assertCurrentState("new", 821, "B");
485
+ assertTrue((boolean) issue81Field(service, "mForegroundActive"));
486
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
487
+ }
488
+
489
+ @Test
490
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
491
+ public void sliceA_timeoutAndStopInBothOrders() throws Exception {
492
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
493
+ sliceATimeout(service, 1);
494
+ ForegroundService.stop();
495
+ idleMain();
496
+ assertEquals(1, SliceAShadowService.order.stream().filter(x -> x.startsWith("stop:")).count());
497
+ sliceADeliver(
498
+ service,
499
+ buildStartIntent("new", 821, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
500
+ 2);
501
+ ForegroundService.stop();
502
+ idleMain();
503
+ SliceAShadowService.order.clear();
504
+ sliceATimeout(service, 2);
505
+ assertTrue(SliceAShadowService.order.isEmpty());
506
+ }
507
+
508
+ @Test
509
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
510
+ public void sliceA_timeoutAndRunnerCompletionInBothOrders() throws Exception {
511
+ SliceAShadowService.reset();
512
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
513
+ createChannel();
514
+ ForegroundService service = buildService().create().get();
515
+ FgsRunnerCapture runners = new FgsRunnerCapture();
516
+ EventBus.register(runners);
517
+ try {
518
+ sliceADeliver(
519
+ service,
520
+ buildStartIntent("slice-a", 820, "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
521
+ 1);
522
+ sliceATimeout(service, 1);
523
+ runners.events.get(0).setCompletionResult();
524
+ assertEquals(1, SliceAShadowService.order.stream().filter(x -> x.equals("demote")).count());
525
+ sliceADeliver(
526
+ service,
527
+ buildStartIntent("new", 821, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
528
+ 2);
529
+ runners.events.get(1).setCompletionResult();
530
+ SliceAShadowService.order.clear();
531
+ sliceATimeout(service, 2);
532
+ assertTrue(SliceAShadowService.order.isEmpty());
533
+ } finally {
534
+ EventBus.unregister(runners);
535
+ }
536
+ }
537
+
538
+ @Test
539
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
540
+ public void sliceA_api36And37PolicySimulation() throws Exception {
541
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
542
+ try {
543
+ for (int api : new int[] {36, 37}) {
544
+ org.robolectric.util.ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", api);
545
+ if (api == 37)
546
+ sliceADeliver(
547
+ service,
548
+ buildStartIntent("new", 821, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
549
+ 2);
550
+ sliceATimeout(service, api == 36 ? 1 : 2);
551
+ assertFalse(sliceAPlatform(service).foreground);
552
+ }
553
+ } finally {
554
+ org.robolectric.util.ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 35);
555
+ }
556
+ }
557
+
558
+ @Test
559
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
560
+ public void sliceA_explicitUnresolvedTypeDoesNotMasqueradeAsShortSentinel() throws Exception {
561
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
562
+ service.onTimeout(1, ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST);
563
+ service.onTimeout(1, ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE);
564
+ assertTrue(SliceAShadowService.order.isEmpty());
565
+ assertCurrentState("slice-a", 820, "A");
566
+ }
567
+
568
+ @Test
569
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
570
+ public void sliceA_reservedBinderReplyDoesNotBlockDemotionOrLoseAcceptance() throws Exception {
571
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
572
+ try (Issue81LateReply pending = new Issue81LateReply(null, true)) {
573
+ pending.awaitDispatch();
574
+ Object request = issue81Pending().values().iterator().next();
575
+ assertFalse((boolean) issue82RequestField(request, "accepted"));
576
+ sliceATimeout(service, 1);
577
+ assertFalse(sliceAPlatform(service).foreground);
578
+ assertFalse(SliceAShadowService.order.stream().anyMatch(x -> x.startsWith("stop:")));
579
+ assertSame(request, issue81Pending().values().iterator().next());
580
+ pending.settle();
581
+ sliceAPlatform(service).accepted = 2;
582
+ assertTrue((boolean) issue82RequestField(request, "accepted"));
583
+ idleMain();
584
+ assertSame(request, issue81Pending().values().iterator().next());
585
+ assertFalse(SliceAShadowService.order.stream().anyMatch(x -> x.startsWith("stop:")));
586
+ }
587
+ }
588
+
589
+ @Test
590
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
591
+ public void sliceA_unfulfilledDeliveredStartCannotBecomeStopId() throws Exception {
592
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
593
+ // A failed replacement reserves a new run without satisfying its publication authority.
594
+ ForegroundService.stop();
595
+ RecordingServiceContext context = useRecordingContext();
596
+ ForegroundService.start(821, buildNotification(), buildNotificationBundle("replacement"));
597
+ SliceAShadowService.promotionFailure = new SecurityException("replacement failed");
598
+ assertThrows(SecurityException.class, () -> sliceADeliver(service, context.starts.get(0), 2));
599
+ sliceATimeout(service, 1);
600
+ assertFalse(sliceAPlatform(service).foreground);
601
+ assertFalse(SliceAShadowService.order.stream().anyMatch(x -> x.startsWith("stop:")));
602
+ }
603
+
604
+ @Test
605
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
606
+ public void sliceA_nestedTimeoutPublishesOutsideBothLocksWithoutWaiting() throws Exception {
607
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
608
+ SliceAEvents events = new SliceAEvents();
609
+ EventBus.register(events);
610
+ try {
611
+ SliceAShadowService.afterDemotion =
612
+ () -> {
613
+ sliceADeliver(
614
+ service,
615
+ buildStartIntent("new", 821, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
616
+ 2);
617
+ sliceATimeout(service, 2);
618
+ };
619
+ sliceATimeout(service, 1);
620
+ assertEquals(2, events.events.size());
621
+ assertFalse(
622
+ "publication must not inherit a reentrant caller's monitors", events.publishedUnderLock);
623
+ } finally {
624
+ EventBus.unregister(events);
625
+ }
626
+ }
627
+
628
+ @Test
629
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
630
+ public void sliceA_eventFailureIsBestEffortAndCannotReplaceDemotionFailure() throws Exception {
631
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
632
+ RuntimeException primary = new SecurityException("demotion failure");
633
+ RuntimeException eventFailure = new IllegalArgumentException("event failure");
634
+ SliceAShadowService.demotionFailure = primary;
635
+ SliceAShadowService.stopResult = false;
636
+ int[] attempts = {0};
637
+ try (org.mockito.MockedStatic<EventBus> mocked =
638
+ org.mockito.Mockito.mockStatic(EventBus.class, org.mockito.Mockito.CALLS_REAL_METHODS)) {
639
+ mocked
640
+ .when(() -> EventBus.post(org.mockito.ArgumentMatchers.any()))
641
+ .thenAnswer(
642
+ invocation -> {
643
+ Object event = invocation.getArgument(0);
644
+ if (event instanceof NotificationEvent
645
+ && ((NotificationEvent) event).getType() == 9) {
646
+ attempts[0]++;
647
+ throw eventFailure;
648
+ }
649
+ return invocation.callRealMethod();
650
+ });
651
+ assertSame(primary, assertThrows(RuntimeException.class, () -> sliceATimeout(service, 1)));
652
+ assertTrue(java.util.Arrays.asList(primary.getSuppressed()).contains(eventFailure));
653
+ sliceATimeout(service, 1);
654
+ assertEquals(1, attempts[0]);
655
+ assertTrue(
656
+ ShadowLog.getLogsForTag("NOTIFEE").stream().anyMatch(x -> x.throwable == eventFailure));
657
+ }
658
+ }
659
+
660
+ @Test
661
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
662
+ public void sliceA_reentrantDuplicateAndExplicitStopPreserveTheirAuthority() throws Exception {
663
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
664
+ SliceAShadowService.beforeDemotion = () -> sliceATimeout(service, 1);
665
+ SliceAShadowService.afterDemotion = ForegroundService::stop;
666
+ SliceAEvents events = new SliceAEvents();
667
+ EventBus.register(events);
668
+ try {
669
+ sliceATimeout(service, 1);
670
+ Object stop = getPrivateStatic("sStopRequest");
671
+ assertNotNull(stop);
672
+ assertEquals(1, events.events.size());
673
+ assertFalse(SliceAShadowService.order.stream().anyMatch(x -> x.startsWith("stop:")));
674
+ idleMain();
675
+ assertEquals(1, SliceAShadowService.order.stream().filter(x -> x.equals("stop:1")).count());
676
+ assertNull(getPrivateStatic("sStopRequest"));
677
+ } finally {
678
+ EventBus.unregister(events);
679
+ }
680
+ }
681
+
682
+ @Test
683
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
684
+ public void sliceA_timeoutDiagnosticRemainsErrorLogEventAfterDemotion() throws Exception {
685
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
686
+ SliceAEvents events = new SliceAEvents();
687
+ EventBus.register(events);
688
+ try {
689
+ sliceATimeout(service, 1);
690
+ assertEquals(1, events.logs.size());
691
+ assertEquals(app.notifee.core.event.LogEvent.LEVEL_ERROR, events.logs.get(0).getLevel());
692
+ assertEquals(
693
+ java.util.Arrays.asList("demote", "stop:1", "event", "log"), SliceAShadowService.order);
694
+ } finally {
695
+ EventBus.unregister(events);
696
+ }
697
+ }
698
+
699
+ @Test
700
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
701
+ public void sliceA_diagnosticEventFailureCannotReplacePrimary() throws Exception {
702
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
703
+ RuntimeException primary = new SecurityException("demotion failure");
704
+ RuntimeException logFailure = new IllegalArgumentException("diagnostic event failure");
705
+ SliceAShadowService.demotionFailure = primary;
706
+ SliceAShadowService.stopResult = false;
707
+ int[] attempts = {0};
708
+ try (org.mockito.MockedStatic<EventBus> mocked =
709
+ org.mockito.Mockito.mockStatic(EventBus.class, org.mockito.Mockito.CALLS_REAL_METHODS)) {
710
+ mocked
711
+ .when(() -> EventBus.post(org.mockito.ArgumentMatchers.any()))
712
+ .thenAnswer(
713
+ invocation -> {
714
+ if (invocation.getArgument(0) instanceof app.notifee.core.event.LogEvent) {
715
+ attempts[0]++;
716
+ throw logFailure;
717
+ }
718
+ return invocation.callRealMethod();
719
+ });
720
+ assertSame(primary, assertThrows(RuntimeException.class, () -> sliceATimeout(service, 1)));
721
+ assertEquals(1, attempts[0]);
722
+ assertTrue(java.util.Arrays.asList(primary.getSuppressed()).contains(logFailure));
723
+ }
724
+ }
725
+
726
+ @Test
727
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
728
+ public void sliceA_failedDemotionConsumesRunnerAuthorityWithoutErasingEvidence()
729
+ throws Exception {
730
+ SliceAShadowService.reset();
731
+ createChannel();
732
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
733
+ ForegroundService service = buildService().create().get();
734
+ FgsRunnerCapture runners = new FgsRunnerCapture();
735
+ EventBus.register(runners);
736
+ try {
737
+ sliceADeliver(
738
+ service,
739
+ buildStartIntent("slice-a", 820, "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
740
+ 1);
741
+ Object runner = issue81Field(service, "mRunnerToken");
742
+ RuntimeException failure = new SecurityException("demotion failure");
743
+ SliceAShadowService.demotionFailure = failure;
744
+ SliceAShadowService.stopResult = false;
745
+ assertSame(failure, assertThrows(RuntimeException.class, () -> sliceATimeout(service, 1)));
746
+ runners.events.get(0).setCompletionResult();
747
+ assertFalse(ForegroundService.repostIfActive("slice-a"));
748
+ assertCurrentState("slice-a", 820, "A");
749
+ assertSame(runner, issue81Field(service, "mRunnerToken"));
750
+ assertEquals(1, SliceAShadowService.order.stream().filter(x -> x.equals("demote")).count());
751
+ } finally {
752
+ EventBus.unregister(runners);
753
+ }
754
+ }
755
+
756
+ @Test
757
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
758
+ public void sliceA_supersededOldDeliveryCannotBorrowCurrentRunStopAuthority() throws Exception {
759
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
760
+ RecordingServiceContext context = useRecordingContext();
761
+ ForegroundService.start(820, buildNotification(), buildNotificationBundle("old"));
762
+ Intent old = context.starts.get(0);
763
+ ForegroundService.stop();
764
+ ForegroundService.start(821, buildNotification(), buildNotificationBundle("new"));
765
+ Intent current = context.starts.get(1);
766
+ sliceADeliver(service, current, 3);
767
+ sliceADeliver(service, old, 2); // actual older START delivered after its successor
768
+ assertEquals(3, sliceAPlatform(service).accepted);
769
+ assertEquals(2, sliceAPlatform(service).delivered);
770
+ assertTrue(issue81Pending().isEmpty());
771
+ SliceAShadowService.order.clear();
772
+ sliceATimeout(service, 3);
773
+ assertFalse(sliceAPlatform(service).foreground);
774
+ assertFalse(
775
+ "old run delivery must not supply the timeout stop ID",
776
+ SliceAShadowService.order.stream().anyMatch(x -> x.startsWith("stop:")));
777
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
778
+ }
779
+
780
+ @Test
781
+ @Config(
782
+ sdk = 35,
783
+ shadows = {SliceAShadowService.class, SliceAPostingShadowManager.class})
784
+ public void sliceA_reentrantPostingTimeoutDrainsAtUnlockedBoundary() throws Exception {
785
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
786
+ SliceAEvents events = new SliceAEvents();
787
+ EventBus.register(events);
788
+ try {
789
+ SliceAPostingShadowManager.callback = () -> sliceATimeout(service, 1);
790
+ assertTrue(ForegroundService.repostIfActive("slice-a"));
791
+ assertFalse(sliceAPlatform(service).foreground);
792
+ assertEquals(
793
+ "an admitted timeout must attempt its event before the outer call returns",
794
+ 1,
795
+ events.events.size());
796
+ assertFalse(events.publishedUnderLock);
797
+ } finally {
798
+ EventBus.unregister(events);
799
+ }
800
+ }
801
+
802
+ @Implements(NotificationManager.class)
803
+ public static class SliceAPostingShadowManager extends ShadowNotificationManager {
804
+ static Runnable callback;
805
+
806
+ @Implementation
807
+ protected void notify(String tag, int id, Notification notification) {
808
+ if (callback != null) {
809
+ Runnable timeout = callback;
810
+ callback = null;
811
+ timeout.run();
812
+ }
813
+ super.notify(tag, id, notification);
814
+ }
815
+ }
816
+
817
+ @Test
818
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
819
+ public void sliceA_retiredCallbackMetadataCannotStopNewlyDeliveredSuccessor() throws Exception {
820
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
821
+ RecordingServiceContext context = useRecordingContext();
822
+ ForegroundService.start(821, buildNotification(), buildNotificationBundle("new"));
823
+ Intent successor = context.starts.get(0);
824
+ sliceAPlatform(service).accepted = 2;
825
+ sliceATimeout(service, 2); // Android metadata names an accepted, not yet delivered START.
826
+ assertEquals(1, issue81Pending().size());
827
+ sliceADeliver(service, successor, 2);
828
+ assertTrue(issue81Pending().isEmpty());
829
+ SliceAShadowService.order.clear();
830
+ sliceATimeout(service, 2); // Indistinguishable old duplicate or fresh timeout: A3 applies.
831
+ assertFalse(sliceAPlatform(service).foreground);
832
+ assertFalse(
833
+ "matching callback metadata is not immutable Android epoch provenance",
834
+ sliceAPlatform(service).stopped);
835
+ assertFalse(SliceAShadowService.order.stream().anyMatch(x -> x.startsWith("stop:")));
836
+ }
837
+
838
+ @Test
839
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
840
+ public void sliceA_explicitStopReplyCannotRetireReentrantSuccessor() throws Exception {
841
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
842
+ SliceAShadowService.afterStop =
843
+ () ->
844
+ sliceADeliver(
845
+ service,
846
+ buildStartIntent("new", 821, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
847
+ 2);
848
+ ForegroundService.stop();
849
+ idleMain();
850
+ assertCurrentState("new", 821, "B");
851
+ assertTrue((boolean) issue81Field(service, "mForegroundActive"));
852
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
853
+ }
854
+
855
+ @Test
856
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
857
+ public void sliceA_terminalStopReplyCannotRetireReentrantSuccessor() throws Exception {
858
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
859
+ SliceAShadowService.afterStop =
860
+ () ->
861
+ sliceADeliver(
862
+ service,
863
+ buildStartIntent("new", 821, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
864
+ 3);
865
+ sliceADeliver(service, buildStopIntent(), 2);
866
+ assertCurrentState("new", 821, "B");
867
+ assertTrue((boolean) issue81Field(service, "mForegroundActive"));
868
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
869
+ }
870
+
871
+ @Test
872
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
873
+ public void sliceA_denialStopReplyCannotMarkReentrantSuccessorStopped() throws Exception {
874
+ SliceAShadowService.reset();
875
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
876
+ createChannel();
877
+ ForegroundService service = buildService().create().get();
878
+ RecordingServiceContext context = useRecordingContext();
879
+ Intent denied = issue82Dispatch(context, "A", 821);
880
+ SliceAShadowService.promotionFailure =
881
+ new ForegroundServiceStartNotAllowedException("tracked denial");
882
+ SliceAShadowService.afterStop =
883
+ () -> {
884
+ SliceAShadowService.promotionFailure = null;
885
+ sliceADeliver(
886
+ service,
887
+ buildStartIntent("new", 822, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
888
+ 2);
889
+ };
890
+ sliceADeliver(service, denied, 1);
891
+ assertCurrentState("new", 822, "B");
892
+ assertTrue((boolean) issue81Field(service, "mForegroundActive"));
893
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
894
+ }
895
+
896
+ @Test
897
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
898
+ public void sliceA_timeoutStopReplyWithNewPendingCannotMarkAdmissionStopped() throws Exception {
899
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
900
+ useRecordingContext();
901
+ SliceAShadowService.afterStop =
902
+ () -> ForegroundService.start(821, buildNotification(), buildNotificationBundle("pending"));
903
+ sliceATimeout(service, 1);
904
+ assertEquals(1, issue81Pending().size());
905
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
906
+ }
907
+
908
+ @Test
909
+ @Config(
910
+ sdk = 35,
911
+ shadows = {SliceAShadowService.class, SliceAPostingShadowManager.class})
912
+ public void sliceA_timeoutDuringUpdateCannotRepublishRetiredCache() throws Exception {
913
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
914
+ SliceAEvents events = new SliceAEvents();
915
+ EventBus.register(events);
916
+ try {
917
+ SliceAPostingShadowManager.callback = () -> sliceATimeout(service, 1);
918
+ sliceADeliver(
919
+ service,
920
+ buildStartIntent("slice-a", 820, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
921
+ 2);
922
+ assertNull(ForegroundService.mCurrentNotificationId);
923
+ assertNull(getPrivateStatic("mCurrentNotificationBundle"));
924
+ assertNull(getPrivateStatic("mCurrentNotification"));
925
+ assertEquals(-1, ForegroundService.mCurrentForegroundServiceType);
926
+ assertNull(issue81Field(service, "mRunnerToken"));
927
+ assertFalse((boolean) issue81Field(service, "mForegroundActive"));
928
+ assertEquals(1, events.events.size());
929
+ assertEquals("A", events.events.get(0).getNotification().toBundle().getString("title"));
930
+ assertFalse(events.publishedUnderLock);
931
+ } finally {
932
+ EventBus.unregister(events);
933
+ }
934
+ }
935
+
936
+ @Test
937
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
938
+ public void sliceA_explicitStopRefusalRestoresOnlyOriginalRequest() throws Exception {
939
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
940
+ SliceAShadowService.stopResult = false;
941
+ ForegroundService.stop();
942
+ Object request = getPrivateStatic("sStopRequest");
943
+ idleMain();
944
+ assertSame(request, getPrivateStatic("sStopRequest"));
945
+ assertCurrentState("slice-a", 820, "A");
946
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
947
+ idleMain();
948
+ assertEquals(1, SliceAShadowService.order.stream().filter(x -> x.startsWith("stop:")).count());
949
+ }
950
+
951
+ @Test
952
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
953
+ public void sliceA_explicitStopThrowRestoresOnlyOriginalRequestAndPropagates() throws Exception {
954
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
955
+ RuntimeException failure = new SecurityException("explicit stop failure");
956
+ SliceAShadowService.stopFailure = failure;
957
+ ForegroundService.stop();
958
+ Object request = getPrivateStatic("sStopRequest");
959
+ assertSame(failure, assertThrows(RuntimeException.class, ForegroundServiceTest::idleMain));
960
+ assertSame(request, getPrivateStatic("sStopRequest"));
961
+ assertCurrentState("slice-a", 820, "A");
962
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
963
+ }
964
+
965
+ @Test
966
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
967
+ public void sliceA_retiredCallbackCollisionAcrossGenerationCannotStopSuccessor()
968
+ throws Exception {
969
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
970
+ RecordingServiceContext context = useRecordingContext();
971
+ ForegroundService.stop();
972
+ ForegroundService.start(821, buildNotification(), buildNotificationBundle("new"));
973
+ Intent successor = context.starts.get(0);
974
+ sliceAPlatform(service).accepted = 2;
975
+ sliceATimeout(service, 2);
976
+ assertEquals(1, issue81Pending().size());
977
+ sliceADeliver(service, successor, 2);
978
+ assertTrue(issue81Pending().isEmpty());
979
+ SliceAShadowService.order.clear();
980
+ sliceATimeout(service, 2);
981
+ assertFalse(sliceAPlatform(service).foreground);
982
+ assertFalse(
983
+ "a generation change cannot prove Android callback provenance",
984
+ sliceAPlatform(service).stopped);
985
+ assertFalse(SliceAShadowService.order.stream().anyMatch(x -> x.startsWith("stop:")));
986
+ }
987
+
988
+ @Test
989
+ @Config(sdk = 35, shadows = SliceAShadowService.class)
990
+ public void sliceA_retiredSameRunPromotionRemainsAmbiguous() throws Exception {
991
+ ForegroundService service = sliceAService(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
992
+ SliceAShadowService.stopResult = false;
993
+ sliceATimeout(service, 1);
994
+ sliceADeliver(
995
+ service,
996
+ buildStartIntent("new", 821, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
997
+ 2);
998
+ SliceAShadowService.stopResult = true;
999
+ SliceAShadowService.order.clear();
1000
+ sliceATimeout(service, 2);
1001
+ assertFalse(sliceAPlatform(service).foreground);
1002
+ assertFalse(sliceAPlatform(service).stopped);
1003
+ assertFalse(SliceAShadowService.order.stream().anyMatch(x -> x.startsWith("stop:")));
1004
+ }
1005
+
1006
+ public static class SliceAEvents {
1007
+ boolean publishedUnderLock;
1008
+ final List<NotificationEvent> events = new ArrayList<>();
1009
+ final List<app.notifee.core.event.LogEvent> logs = new ArrayList<>();
1010
+
1011
+ @Subscribe(threadMode = ThreadMode.POSTING)
1012
+ public void notification(NotificationEvent event) {
1013
+ if (event.getType() == 9) {
1014
+ try {
1015
+ publishedUnderLock |=
1016
+ Thread.holdsLock(getPrivateStatic("sLifecycleLock"))
1017
+ || Thread.holdsLock(getPrivateStatic("sLock"));
1018
+ } catch (Exception e) {
1019
+ throw new AssertionError(e);
1020
+ }
1021
+ SliceAShadowService.order.add("event");
1022
+ events.add(event);
1023
+ }
1024
+ }
1025
+
1026
+ @Subscribe(threadMode = ThreadMode.POSTING)
1027
+ public void log(app.notifee.core.event.LogEvent event) {
1028
+ logs.add(event);
1029
+ SliceAShadowService.order.add("log");
1030
+ }
1031
+ }
1032
+
1033
+ @Implements(Service.class)
1034
+ public static class SliceAShadowService extends ShadowService {
1035
+ static final List<String> order = new ArrayList<>();
1036
+ static RuntimeException promotionFailure, demotionFailure, stopFailure;
1037
+ static Runnable beforeDemotion, afterDemotion, afterStop;
1038
+ static boolean stopResult;
1039
+ int accepted, delivered, promotions, type;
1040
+ boolean foreground, stopped;
1041
+
1042
+ static void reset() {
1043
+ order.clear();
1044
+ promotionFailure = demotionFailure = stopFailure = null;
1045
+ beforeDemotion = afterDemotion = afterStop = null;
1046
+ stopResult = true;
1047
+ }
1048
+
1049
+ @Implementation
1050
+ protected void startForeground(int id, Notification notification, int foregroundServiceType) {
1051
+ if (promotionFailure != null) throw promotionFailure;
1052
+ promotions++;
1053
+ foreground = true;
1054
+ stopped = false;
1055
+ type = foregroundServiceType;
1056
+ super.startForeground(id, notification, foregroundServiceType);
1057
+ }
1058
+
1059
+ @Implementation
1060
+ protected void stopForeground(int flags) {
1061
+ if (beforeDemotion != null) {
1062
+ Runnable hook = beforeDemotion;
1063
+ beforeDemotion = null;
1064
+ hook.run();
1065
+ }
1066
+ order.add("demote");
1067
+ assertEquals(Service.STOP_FOREGROUND_REMOVE, flags);
1068
+ if (demotionFailure != null) throw demotionFailure;
1069
+ foreground = false;
1070
+ type = 0;
1071
+ super.stopForeground(flags);
1072
+ if (afterDemotion != null) {
1073
+ Runnable hook = afterDemotion;
1074
+ afterDemotion = null;
1075
+ hook.run();
1076
+ }
1077
+ }
1078
+
1079
+ @Implementation
1080
+ protected boolean stopSelfResult(int startId) {
1081
+ order.add("stop:" + startId);
1082
+ if (stopFailure != null) throw stopFailure;
1083
+ boolean result = stopResult && startId == accepted;
1084
+ if (result) {
1085
+ stopped = true;
1086
+ foreground = false;
1087
+ super.stopSelfResult(startId);
1088
+ }
1089
+ if (afterStop != null) {
1090
+ Runnable hook = afterStop;
1091
+ afterStop = null;
1092
+ hook.run();
1093
+ }
1094
+ return result;
1095
+ }
1096
+
1097
+ @Implementation
1098
+ protected void stopSelf(int startId) {
1099
+ order.add("forbidden-stop:" + startId);
1100
+ throw new AssertionError("timeout must use observable conditional stopSelfResult");
1101
+ }
1102
+
1103
+ @Implementation
1104
+ protected void stopSelf() {
1105
+ throw new AssertionError("indiscriminate stop is forbidden");
1106
+ }
1107
+ }
1108
+
1109
+ // Slice D uses the real UUID reservation/dispatch path. API 36/37 below are policy-boundary
1110
+ // simulations on the API 35 Robolectric runtime; they are not Android 16/17 framework proof.
1111
+ @Test
1112
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1113
+ public void issue82_initialTrackedDenial_isSelectiveAndNonFatal() throws Exception {
1114
+ for (String message :
1115
+ new String[] {
1116
+ "Time limit already exhausted for foreground service type dataSync",
1117
+ "Background start not allowed",
1118
+ "opaque",
1119
+ ""
1120
+ }) {
1121
+ RecordingServiceContext context = useRecordingContext();
1122
+ ForegroundService service = issue81Service();
1123
+ Issue81ShadowService.failure = new ForegroundServiceStartNotAllowedException(message);
1124
+ issue81ObserveRecoveryStop(service);
1125
+ Intent a = issue82Dispatch(context, "A", 810);
1126
+ String id = a.getStringExtra(ISSUE81_REQUEST_ID);
1127
+ Object request = issue81Pending().get(id);
1128
+ FgsRunnerCapture capture = new FgsRunnerCapture();
1129
+ EventBus.register(capture);
1130
+ try {
1131
+ int result;
1132
+ try {
1133
+ result = service.onStartCommand(a, 0, 7);
1134
+ } catch (RuntimeException denial) {
1135
+ assertSame(Issue81ShadowService.failure, denial);
1136
+ assertEquals(java.util.Collections.singletonList(810), Issue81ShadowService.promotions);
1137
+ assertEquals(
1138
+ java.util.Collections.singletonList(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
1139
+ Issue81ShadowService.types);
1140
+ assertSame(request, issue81Pending().get(id));
1141
+ issue81AssertUnpromoted(service);
1142
+ throw denial; // causal RED: actual tracked initial platform promotion was attempted
1143
+ }
1144
+ assertEquals(Service.START_NOT_STICKY, result);
1145
+ assertTrue(issue81Pending().isEmpty());
1146
+ assertTrue((boolean) issue82RequestField(request, "accepted"));
1147
+ assertTrue((boolean) issue82RequestField(request, "promotionDenied"));
1148
+ issue81AssertUnpromoted(service);
1149
+ assertTrue(capture.events.isEmpty());
1150
+ assertEquals(java.util.Arrays.asList("promote:810", "stop:7"), Issue81ShadowService.order);
1151
+ issue82AssertDiagnostic(id, "accepted");
1152
+ } finally {
1153
+ EventBus.unregister(capture);
1154
+ }
1155
+ issue82DestroyServices();
1156
+ }
1157
+ }
1158
+
1159
+ @Test
1160
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1161
+ public void issue82_api36And37_policyBoundarySimulation() throws Exception {
1162
+ try {
1163
+ for (int api : new int[] {36, 37}) {
1164
+ org.robolectric.util.ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", api);
1165
+ RecordingServiceContext context = useRecordingContext();
1166
+ ForegroundService service = issue81Service();
1167
+ Intent a = issue82Dispatch(context, "A", 810);
1168
+ assertEquals(Service.START_NOT_STICKY, service.onStartCommand(a, 0, 7));
1169
+ issue81AssertUnpromoted(service);
1170
+ assertTrue(issue81Pending().isEmpty());
1171
+ assertEquals(java.util.Collections.singletonList(7), Issue81ShadowService.stops);
1172
+ issue82AssertDiagnostic(a.getStringExtra(ISSUE81_REQUEST_ID), "accepted");
1173
+ issue82DestroyServices();
1174
+ }
1175
+ } finally {
1176
+ org.robolectric.util.ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", 35);
1177
+ }
1178
+ }
1179
+
1180
+ @Test
1181
+ @Config(
1182
+ sdk = {31, 32, 33, 34},
1183
+ shadows = Issue81ShadowService.class)
1184
+ public void issue82_api31Through34_trackedDenialRemainsFatal() throws Exception {
1185
+ RecordingServiceContext context = useRecordingContext();
1186
+ ForegroundService service = issue81Service();
1187
+ Intent a = issue82Dispatch(context, "A", 810);
1188
+ Object request = issue81Pending().get(a.getStringExtra(ISSUE81_REQUEST_ID));
1189
+ assertSame(
1190
+ Issue81ShadowService.failure,
1191
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(a, 0, 7)));
1192
+ assertSame(request, issue81Pending().get(a.getStringExtra(ISSUE81_REQUEST_ID)));
1193
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1194
+ issue81AssertUnpromoted(service);
1195
+ }
1196
+
1197
+ @Test
1198
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1199
+ public void issue82_otherPromotionExceptionsRemainFatal() throws Exception {
1200
+ for (RuntimeException failure :
1201
+ new RuntimeException[] {
1202
+ new SecurityException("permission"), new MissingForegroundServiceTypeException("missing"),
1203
+ new InvalidForegroundServiceTypeException("invalid"),
1204
+ new IllegalArgumentException("argument"),
1205
+ new IllegalStateException("state"), new RuntimeException("other")
1206
+ }) {
1207
+ RecordingServiceContext context = useRecordingContext();
1208
+ ForegroundService service = issue81Service();
1209
+ Intent a = issue82Dispatch(context, "A", 810);
1210
+ Object request = issue81Pending().get(a.getStringExtra(ISSUE81_REQUEST_ID));
1211
+ Issue81ShadowService.failure = failure;
1212
+ assertSame(
1213
+ failure, assertThrows(RuntimeException.class, () -> service.onStartCommand(a, 0, 7)));
1214
+ assertSame(request, issue81Pending().get(a.getStringExtra(ISSUE81_REQUEST_ID)));
1215
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1216
+ issue81AssertUnpromoted(service);
1217
+ issue82DestroyServices();
1218
+ }
1219
+ }
1220
+
1221
+ @Test
1222
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1223
+ public void issue82_untrackedAndWrongActionRemainFatal_incompletePayloadDoesNotPromote()
1224
+ throws Exception {
1225
+ RecordingServiceContext context = useRecordingContext();
1226
+ ForegroundService service = issue81Service();
1227
+ assertSame(
1228
+ Issue81ShadowService.failure,
1229
+ assertThrows(
1230
+ RuntimeException.class, () -> service.onStartCommand(issue81UnknownStart(), 0, 1)));
1231
+ Intent a = issue82Dispatch(context, "A", 810);
1232
+ a.setAction("another.START");
1233
+ assertSame(
1234
+ Issue81ShadowService.failure,
1235
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(a, 0, 2)));
1236
+ a.setAction(ForegroundService.START_FOREGROUND_SERVICE_ACTION);
1237
+ a.removeExtra("notification");
1238
+ service.onStartCommand(a, 0, 3);
1239
+ assertEquals(2, Issue81ShadowService.promotions.size());
1240
+ assertEquals(1, issue81Pending().size());
1241
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1242
+ }
1243
+
1244
+ @Test
1245
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1246
+ public void issue82_deniedA_preservesAcceptedAndDeliveredUnfulfilledB() throws Exception {
1247
+ for (boolean delivered : new boolean[] {false, true}) {
1248
+ RecordingServiceContext context = useRecordingContext();
1249
+ ForegroundService service = issue81Service();
1250
+ Intent a = issue82Dispatch(context, "A", 810);
1251
+ Intent b = issue82Dispatch(context, "B", 812);
1252
+ Object request = issue81Pending().get(b.getStringExtra(ISSUE81_REQUEST_ID));
1253
+ if (delivered) {
1254
+ Intent incomplete = new Intent(b);
1255
+ incomplete.removeExtra("notification");
1256
+ service.onStartCommand(incomplete, 0, 6);
1257
+ }
1258
+ assertEquals(Service.START_NOT_STICKY, service.onStartCommand(a, 0, 7));
1259
+ assertSame(request, issue81Pending().get(b.getStringExtra(ISSUE81_REQUEST_ID)));
1260
+ assertEquals(1, issue81Pending().size());
1261
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1262
+ issue81AssertUnpromoted(service);
1263
+ assertNotNull(issue81Field(service, "mDeniedStartRecovery"));
1264
+ issue82AssertDiagnostic(a.getStringExtra(ISSUE81_REQUEST_ID), "deferred");
1265
+ service.onStartCommand(b, 0, 8);
1266
+ idleMain();
1267
+ assertCurrentState("B", 812, "B");
1268
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1269
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
1270
+ issue82DestroyServices();
1271
+ }
1272
+ }
1273
+
1274
+ @Test
1275
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1276
+ public void issue82_pendingB_definiteRejectionReevaluatesRecovery_unknownDoesNot()
1277
+ throws Exception {
1278
+ for (RuntimeException reply :
1279
+ new RuntimeException[] {
1280
+ null, new SecurityException("reject"), new IllegalStateException("Binder UNKNOWN")
1281
+ }) {
1282
+ RecordingServiceContext context = useRecordingContext();
1283
+ ForegroundService service = issue81Service();
1284
+ Intent a = issue82Dispatch(context, "A", 810);
1285
+ try (Issue81LateReply b = new Issue81LateReply(reply, false)) {
1286
+ b.awaitDispatch();
1287
+ Object request = issue81Pending().get(b.intent.getStringExtra(ISSUE81_REQUEST_ID));
1288
+ assertFalse((boolean) issue82RequestField(request, "accepted"));
1289
+ assertEquals(Service.START_NOT_STICKY, service.onStartCommand(a, 0, 7));
1290
+ assertSame(request, issue81Pending().get(b.intent.getStringExtra(ISSUE81_REQUEST_ID)));
1291
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1292
+ issue82AssertDiagnostic(a.getStringExtra(ISSUE81_REQUEST_ID), "deferred");
1293
+ b.settle();
1294
+ assertSame(reply, b.failure.get());
1295
+ idleMain();
1296
+ if (reply instanceof IllegalStateException) {
1297
+ assertSame(request, issue81Pending().get(b.intent.getStringExtra(ISSUE81_REQUEST_ID)));
1298
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1299
+ assertNotNull(issue81Field(service, "mDeniedStartRecovery"));
1300
+ service.onStartCommand(b.intent, 0, 8);
1301
+ assertCurrentState("B", 812, "B");
1302
+ } else {
1303
+ assertTrue(issue81Pending().isEmpty());
1304
+ assertEquals(java.util.Collections.singletonList(7), Issue81ShadowService.stops);
1305
+ issue82AssertDiagnostic(a.getStringExtra(ISSUE81_REQUEST_ID), "accepted");
1306
+ }
1307
+ }
1308
+ issue82DestroyServices();
1309
+ }
1310
+ }
1311
+
1312
+ @Test
1313
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1314
+ public void issue82_pendingB_acceptanceInvalidatesRecovery() throws Exception {
1315
+ RecordingServiceContext context = useRecordingContext();
1316
+ ForegroundService service = issue81Service();
1317
+ Intent a = issue82Dispatch(context, "A", 810);
1318
+ try (Issue81LateReply b = new Issue81LateReply(null, true)) {
1319
+ b.awaitDispatch();
1320
+ service.onStartCommand(a, 0, 7);
1321
+ b.settle();
1322
+ idleMain();
1323
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
1324
+ assertEquals(1, issue81Pending().size());
1325
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1326
+ service.onStartCommand(b.intent, 0, 8);
1327
+ assertCurrentState("B", 812, "B");
1328
+ }
1329
+ }
1330
+
1331
+ @Test
1332
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1333
+ public void issue82_lateAReply_doesNotResurrectOrCancelLaterStop() throws Exception {
1334
+ for (RuntimeException reply :
1335
+ new RuntimeException[] {
1336
+ null, new SecurityException("late negative"), new IllegalStateException("late UNKNOWN")
1337
+ }) {
1338
+ for (boolean positive : new boolean[] {false, true}) {
1339
+ ForegroundService service = issue81Service();
1340
+ Issue81ShadowService.stopResult = false;
1341
+ try (Issue81LateReply a = new Issue81LateReply(reply, positive, "A", 810)) {
1342
+ a.awaitDispatch();
1343
+ String id = a.intent.getStringExtra(ISSUE81_REQUEST_ID);
1344
+ Object request = issue81Pending().get(id);
1345
+ service.onStartCommand(a.intent, 0, 7);
1346
+ assertTrue(issue81Pending().isEmpty());
1347
+ assertTrue((boolean) issue82RequestField(request, "accepted"));
1348
+ ForegroundService.stop();
1349
+ Object stop = getPrivateStatic("sStopRequest");
1350
+ a.settle();
1351
+ assertSame(reply, a.failure.get());
1352
+ assertTrue(issue81Pending().isEmpty());
1353
+ assertSame(stop, getPrivateStatic("sStopRequest"));
1354
+ idleMain();
1355
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
1356
+ }
1357
+ issue82DestroyServices();
1358
+ }
1359
+ }
1360
+ }
1361
+
1362
+ @Test
1363
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1364
+ public void issue82_lateAReply_doesNotInvalidateNewRecovery() throws Exception {
1365
+ for (RuntimeException reply :
1366
+ new RuntimeException[] {
1367
+ null, new SecurityException("late negative"), new IllegalStateException("late UNKNOWN")
1368
+ }) {
1369
+ for (boolean positive : new boolean[] {false, true}) {
1370
+ RecordingServiceContext context = useRecordingContext();
1371
+ ForegroundService service = issue81Service();
1372
+ Intent b = issue82Dispatch(context, "B", 812);
1373
+ try (Issue81LateReply a = new Issue81LateReply(reply, positive, "A", 810)) {
1374
+ a.awaitDispatch();
1375
+ service.onStartCommand(a.intent, 0, 7);
1376
+ Object first = issue81Field(service, "mDeniedStartRecovery");
1377
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 8);
1378
+ Object newer = issue81Field(service, "mDeniedStartRecovery");
1379
+ assertNotNull(newer);
1380
+ assertFalse(first == newer);
1381
+ a.settle();
1382
+ assertSame(reply, a.failure.get());
1383
+ idleMain();
1384
+ assertSame(newer, issue81Field(service, "mDeniedStartRecovery"));
1385
+ assertEquals(1, issue81Pending().size());
1386
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1387
+ service.onStartCommand(b, 0, 9);
1388
+ assertCurrentState("B", 812, "B");
1389
+ }
1390
+ issue82DestroyServices();
1391
+ }
1392
+ }
1393
+ }
1394
+
1395
+ @Test
1396
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1397
+ public void issue82_stopRefusedOrThrows_doesNotCleanOrRetry() throws Exception {
1398
+ for (boolean throwsStop : new boolean[] {false, true}) {
1399
+ RecordingServiceContext context = useRecordingContext();
1400
+ ForegroundService service = issue81Service();
1401
+ Intent a = issue82Dispatch(context, "A", 810);
1402
+ Issue81ShadowService.stopResult = false;
1403
+ if (throwsStop) {
1404
+ Issue81ShadowService.stopFailure = new SecurityException("stop failed");
1405
+ assertSame(
1406
+ Issue81ShadowService.stopFailure,
1407
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(a, 0, 7)));
1408
+ } else {
1409
+ assertEquals(Service.START_NOT_STICKY, service.onStartCommand(a, 0, 7));
1410
+ }
1411
+ assertTrue(issue81Pending().isEmpty());
1412
+ issue81AssertUnpromoted(service);
1413
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
1414
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
1415
+ issue82AssertDiagnostic(
1416
+ a.getStringExtra(ISSUE81_REQUEST_ID), throwsStop ? "failed" : "refused");
1417
+ idleMain();
1418
+ assertEquals(java.util.Collections.singletonList(7), Issue81ShadowService.stops);
1419
+ issue82DestroyServices();
1420
+ }
1421
+ }
1422
+
1423
+ @Test
1424
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1425
+ public void issue82_failedReplacement_preservesPromotedBAndRunner() throws Exception {
1426
+ RecordingServiceContext context = useRecordingContext();
1427
+ ForegroundService service = issue81Service();
1428
+ FgsRunnerCapture capture = new FgsRunnerCapture();
1429
+ EventBus.register(capture);
1430
+ try {
1431
+ Intent b = issue82Dispatch(context, "B", 812);
1432
+ service.onStartCommand(b, 0, 1);
1433
+ Object runner = issue81Field(service, "mRunnerToken");
1434
+ Object bundle = getPrivateStatic("mCurrentNotificationBundle");
1435
+ ForegroundService.stop();
1436
+ Intent a = issue82Dispatch(context, "A", 810);
1437
+ assertSame(
1438
+ Issue81ShadowService.failure,
1439
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(a, 0, 2)));
1440
+ assertCurrentState("B", 812, "B");
1441
+ assertSame(bundle, getPrivateStatic("mCurrentNotificationBundle"));
1442
+ assertEquals(
1443
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
1444
+ ForegroundService.mCurrentForegroundServiceType);
1445
+ assertSame(runner, issue81Field(service, "mRunnerToken"));
1446
+ assertTrue(ForegroundService.repostIfActive("B"));
1447
+ assertEquals(1, capture.events.size());
1448
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1449
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
1450
+ capture.events.get(0).setCompletionResult();
1451
+ idleMain();
1452
+ assertCurrentState("B", 812, "B");
1453
+ } finally {
1454
+ EventBus.unregister(capture);
1455
+ }
1456
+ }
1457
+
1458
+ @Test
1459
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1460
+ public void issue82_postTimeoutSameInstance_isExplicitlyExcluded() throws Exception {
1461
+ RecordingServiceContext context = useRecordingContext();
1462
+ ForegroundService service = issue81Service();
1463
+ Intent b = issue82Dispatch(context, "B", 812);
1464
+ service.onStartCommand(b, 0, 1);
1465
+ service.onTimeout(1, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
1466
+ assertTrue((boolean) issue81Field(service, "mStartForegroundCalled"));
1467
+ assertEquals(1, issue81Field(service, "mRunFirstStartId"));
1468
+ Intent a = issue82Dispatch(context, "A", 810);
1469
+ assertSame(
1470
+ Issue81ShadowService.failure,
1471
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(a, 0, 2)));
1472
+ assertEquals(java.util.Arrays.asList(812, 810), Issue81ShadowService.promotions);
1473
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
1474
+ }
1475
+
1476
+ @Test
1477
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1478
+ public void issue82_eligibilityRecheckedAfterPromotionAttempt() throws Exception {
1479
+ for (String fieldName :
1480
+ new String[] {
1481
+ "sInstance", "sStateOwner", "sRunGeneration", "mRunGeneration", "mLastStartId"
1482
+ }) {
1483
+ RecordingServiceContext context = useRecordingContext();
1484
+ ForegroundService service = issue81Service();
1485
+ Intent a = issue82Dispatch(context, "A", 810);
1486
+ Field field = ForegroundService.class.getDeclaredField(fieldName);
1487
+ field.setAccessible(true);
1488
+ Object owner = fieldName.startsWith("s") ? null : service;
1489
+ Object original = field.get(owner);
1490
+ Issue81ShadowService.beforePromotion =
1491
+ () -> {
1492
+ try {
1493
+ if (fieldName.endsWith("Generation")) field.set(owner, ((Long) original) + 1);
1494
+ else if (fieldName.equals("mLastStartId")) field.set(owner, 99);
1495
+ else field.set(owner, new ForegroundService());
1496
+ } catch (Exception e) {
1497
+ throw new AssertionError(e);
1498
+ }
1499
+ };
1500
+ try {
1501
+ assertSame(
1502
+ Issue81ShadowService.failure,
1503
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(a, 0, 7)));
1504
+ assertEquals(1, issue81Pending().size());
1505
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1506
+ } finally {
1507
+ field.set(owner, original);
1508
+ Issue81ShadowService.beforePromotion = null;
1509
+ }
1510
+ issue82DestroyServices();
1511
+ }
1512
+ }
1513
+
1514
+ @Test
1515
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1516
+ public void issue82_deferredCallback_revalidatesAllAuthority() throws Exception {
1517
+ for (String fieldName :
1518
+ new String[] {
1519
+ "sInstance", "sStateOwner", "sRunGeneration", "mRunGeneration", "mLastStartId"
1520
+ }) {
1521
+ RecordingServiceContext context = useRecordingContext();
1522
+ ForegroundService service = issue81Service();
1523
+ Intent a = issue82Dispatch(context, "A", 810);
1524
+ try (Issue81LateReply b = new Issue81LateReply(null, false)) {
1525
+ b.awaitDispatch();
1526
+ service.onStartCommand(a, 0, 7);
1527
+ b.settle(); // queues recovery with A's identity, before we revoke one fence
1528
+ Field field = ForegroundService.class.getDeclaredField(fieldName);
1529
+ field.setAccessible(true);
1530
+ Object owner = fieldName.startsWith("s") ? null : service;
1531
+ Object original = field.get(owner);
1532
+ try {
1533
+ if (fieldName.endsWith("Generation")) field.set(owner, ((Long) original) + 1);
1534
+ else if (fieldName.equals("mLastStartId")) field.set(owner, 99);
1535
+ else field.set(owner, new ForegroundService());
1536
+ idleMain();
1537
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1538
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
1539
+ } finally {
1540
+ field.set(owner, original);
1541
+ }
1542
+ }
1543
+ issue82DestroyServices();
1544
+ }
1545
+ }
1546
+
1547
+ @Test
1548
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1549
+ public void issue82_trackedFrameworkFlags_useSliceD_notUntrackedRedeliveryPolicy()
1550
+ throws Exception {
1551
+ for (int flags :
1552
+ new int[] {
1553
+ Service.START_FLAG_RETRY,
1554
+ Service.START_FLAG_REDELIVERY,
1555
+ Service.START_FLAG_RETRY | Service.START_FLAG_REDELIVERY
1556
+ }) {
1557
+ RecordingServiceContext context = useRecordingContext();
1558
+ ForegroundService service = issue81Service();
1559
+ Intent a = issue82Dispatch(context, "A", 810);
1560
+ service.onStartCommand(a, flags, 7);
1561
+ assertTrue(issue81Pending().isEmpty());
1562
+ issue81AssertUnpromoted(service);
1563
+ assertEquals(java.util.Collections.singletonList(7), Issue81ShadowService.stops);
1564
+ assertTrue(
1565
+ ShadowLog.getLogsForTag("NOTIFEE").stream()
1566
+ .anyMatch(
1567
+ item ->
1568
+ item.msg.contains("PROMOTION_DENIED")
1569
+ && item.msg.contains("flags=" + flags)));
1570
+ issue82DestroyServices();
1571
+ }
1572
+ }
1573
+
1574
+ @Test
1575
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1576
+ public void issue82_manifestAndNoneDenialsRemainOutsideCatch() throws Exception {
1577
+ RecordingServiceContext context = useRecordingContext();
1578
+ ForegroundService service = issue81Service();
1579
+ Intent a = issue82Dispatch(context, "A", 810);
1580
+ a.getBundleExtra("notificationBundle").getBundle("android").remove("foregroundServiceTypes");
1581
+ PackageManager manager = org.mockito.Mockito.mock(PackageManager.class);
1582
+ org.mockito.Mockito.when(
1583
+ manager.getServiceInfo(
1584
+ org.mockito.ArgumentMatchers.any(ComponentName.class),
1585
+ org.mockito.ArgumentMatchers.anyInt()))
1586
+ .thenThrow(Issue81ShadowService.failure);
1587
+ ContextHolder.setApplicationContext(
1588
+ new ContextWrapper(RuntimeEnvironment.getApplication()) {
1589
+ @Override
1590
+ public PackageManager getPackageManager() {
1591
+ return manager;
1592
+ }
1593
+ });
1594
+ assertSame(
1595
+ Issue81ShadowService.failure,
1596
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(a, 0, 7)));
1597
+ assertTrue(Issue81ShadowService.promotions.isEmpty());
1598
+ assertEquals(1, issue81Pending().size());
1599
+ // Actual resolved NONE reaches the defensive placeholder, not the initial real promotion.
1600
+ ContextHolder.setApplicationContext(null);
1601
+ RuntimeException denial =
1602
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(a, 0, 8));
1603
+ assertSame(Issue81ShadowService.failure, denial.getCause());
1604
+ assertEquals(
1605
+ java.util.Collections.singletonList(Integer.MAX_VALUE - 1),
1606
+ Issue81ShadowService.promotions);
1607
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1608
+ assertEquals(1, issue81Pending().size());
1609
+ }
1610
+
1611
+ @Test
1612
+ @Config(
1613
+ sdk = 35,
1614
+ shadows = {Issue81ShadowService.class, Issue81NotificationShadowManager.class})
1615
+ public void issue82_trackedSameIdNotifyAndTypeChangeDenials_preserveState() throws Exception {
1616
+ RecordingServiceContext context = useRecordingContext();
1617
+ ForegroundService service = issue81Service();
1618
+ Intent b = issue82Dispatch(context, "B", 812);
1619
+ service.onStartCommand(b, 0, 1);
1620
+ Object runner = issue81Field(service, "mRunnerToken");
1621
+ Object bundle = getPrivateStatic("mCurrentNotificationBundle");
1622
+ for (int type :
1623
+ new int[] {
1624
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
1625
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
1626
+ }) {
1627
+ Intent update = buildStartIntent("B", 812, "A", type);
1628
+ ForegroundService.start(
1629
+ 812,
1630
+ update.getParcelableExtra("notification"),
1631
+ update.getBundleExtra("notificationBundle"));
1632
+ Intent delivered = context.starts.get(context.starts.size() - 1);
1633
+ assertSame(
1634
+ Issue81ShadowService.failure,
1635
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(delivered, 0, 2)));
1636
+ assertCurrentState("B", 812, "B");
1637
+ assertSame(bundle, getPrivateStatic("mCurrentNotificationBundle"));
1638
+ assertSame(runner, issue81Field(service, "mRunnerToken"));
1639
+ assertEquals(
1640
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
1641
+ ForegroundService.mCurrentForegroundServiceType);
1642
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1643
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
1644
+ }
1645
+ assertEquals(java.util.Arrays.asList(812, 812), Issue81ShadowService.promotions);
1646
+ }
1647
+
1648
+ @Test
1649
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1650
+ public void issue82_supersededTrackedDenial_isExcluded() throws Exception {
1651
+ RecordingServiceContext context = useRecordingContext();
1652
+ ForegroundService service = issue81Service();
1653
+ Intent a = issue82Dispatch(context, "A", 810);
1654
+ ForegroundService.stop();
1655
+ Intent b = issue82Dispatch(context, "B", 812);
1656
+ FgsRunnerCapture capture = new FgsRunnerCapture();
1657
+ EventBus.register(capture);
1658
+ try {
1659
+ service.onStartCommand(b, 0, 1);
1660
+ capture.events.get(0).setCompletionResult();
1661
+ assertSame(
1662
+ Issue81ShadowService.failure,
1663
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(a, 0, 2)));
1664
+ assertEquals(1, issue81Pending().size());
1665
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1666
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
1667
+ } finally {
1668
+ EventBus.unregister(capture);
1669
+ }
1670
+ }
1671
+
1672
+ @Test
1673
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1674
+ public void issue82_requestIdentityAndOwnership_fencedBeforeAndAfterAttempt() throws Exception {
1675
+ for (boolean duringAttempt : new boolean[] {false, true}) {
1676
+ for (String authority : new String[] {"sInstance", "sStateOwner", "request"}) {
1677
+ RecordingServiceContext context = useRecordingContext();
1678
+ ForegroundService service = issue81Service();
1679
+ Intent a = issue82Dispatch(context, "A", 810);
1680
+ String id = a.getStringExtra(ISSUE81_REQUEST_ID);
1681
+ Field field =
1682
+ ForegroundService.class.getDeclaredField(
1683
+ authority.equals("request") ? "sPendingStarts" : authority);
1684
+ field.setAccessible(true);
1685
+ Object original = field.get(null);
1686
+ Runnable revoke =
1687
+ () -> {
1688
+ try {
1689
+ if (authority.equals("request")) issue81Pending().remove(id);
1690
+ else field.set(null, new ForegroundService());
1691
+ } catch (Exception e) {
1692
+ throw new AssertionError(e);
1693
+ }
1694
+ };
1695
+ if (duringAttempt) Issue81ShadowService.beforePromotion = revoke;
1696
+ else revoke.run();
1697
+ try {
1698
+ assertSame(
1699
+ Issue81ShadowService.failure,
1700
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(a, 0, 7)));
1701
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1702
+ } finally {
1703
+ if (!authority.equals("request")) field.set(null, original);
1704
+ Issue81ShadowService.beforePromotion = null;
1705
+ }
1706
+ issue82DestroyServices();
1707
+ }
1708
+ }
1709
+ }
1710
+
1711
+ @Test
1712
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1713
+ public void issue82_deferredRecovery_newAdmissionStopAndDestroyInvalidateStaleCallback()
1714
+ throws Exception {
1715
+ for (String action : new String[] {"admission", "stop", "destroy", "delivery"}) {
1716
+ RecordingServiceContext context = useRecordingContext();
1717
+ ForegroundService service = issue81Service();
1718
+ Intent a = issue82Dispatch(context, "A", 810);
1719
+ try (Issue81LateReply b = new Issue81LateReply(null, false)) {
1720
+ b.awaitDispatch();
1721
+ service.onStartCommand(a, 0, 7);
1722
+ Object recovery = issue81Field(service, "mDeniedStartRecovery");
1723
+ b.settle(); // queued callback carries recovery A; lifecycle changes happen before it runs
1724
+ if (action.equals("admission")) {
1725
+ Intent c = issue82Dispatch(useRecordingContext(), "C", 814);
1726
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
1727
+ idleMain();
1728
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1729
+ service.onStartCommand(c, 0, 8);
1730
+ assertCurrentState("C", 814, "C");
1731
+ } else if (action.equals("stop")) {
1732
+ ForegroundService.stop();
1733
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
1734
+ idleMain();
1735
+ assertEquals(java.util.Collections.singletonList(7), Issue81ShadowService.stops);
1736
+ assertFalse(
1737
+ ShadowLog.getLogsForTag("NOTIFEE").stream()
1738
+ .anyMatch(
1739
+ item ->
1740
+ item.msg.contains("PROMOTION_DENIED")
1741
+ && item.msg.contains("stop=accepted")));
1742
+ } else if (action.equals("destroy")) {
1743
+ issue82DestroyServices();
1744
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1745
+ } else {
1746
+ Intent incomplete = new Intent().setAction("incomplete");
1747
+ service.onStartCommand(incomplete, 0, 8);
1748
+ idleMain();
1749
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1750
+ }
1751
+ assertNotNull(recovery);
1752
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
1753
+ }
1754
+ issue82DestroyServices();
1755
+ }
1756
+ }
1757
+
1758
+ @Test
1759
+ @Config(sdk = 35, shadows = Issue81ShadowService.class)
1760
+ public void issue82_queuedRecovery_newPendingReservationDefersUntilDefiniteRejection()
1761
+ throws Exception {
1762
+ RecordingServiceContext context = useRecordingContext();
1763
+ ForegroundService service = issue81Service();
1764
+ Intent a = issue82Dispatch(context, "A", 810);
1765
+ try (Issue81LateReply b = new Issue81LateReply(null, false)) {
1766
+ b.awaitDispatch();
1767
+ service.onStartCommand(a, 0, 7);
1768
+ Object recovery = issue81Field(service, "mDeniedStartRecovery");
1769
+ b.settle();
1770
+ try (Issue81LateReply c = new Issue81LateReply(null, false, "C", 814)) {
1771
+ c.awaitDispatch();
1772
+ Object request = issue81Pending().get(c.intent.getStringExtra(ISSUE81_REQUEST_ID));
1773
+ idleMain();
1774
+ assertSame(recovery, issue81Field(service, "mDeniedStartRecovery"));
1775
+ assertSame(request, issue81Pending().get(c.intent.getStringExtra(ISSUE81_REQUEST_ID)));
1776
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1777
+ c.settle();
1778
+ idleMain();
1779
+ assertTrue(issue81Pending().isEmpty());
1780
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
1781
+ assertEquals(java.util.Collections.singletonList(7), Issue81ShadowService.stops);
1782
+ issue82AssertDiagnostic(a.getStringExtra(ISSUE81_REQUEST_ID), "accepted");
1783
+ }
1784
+ }
1785
+ }
1786
+
1787
+ private static Object issue82RequestField(Object request, String name) throws Exception {
1788
+ Field field = request.getClass().getDeclaredField(name);
1789
+ field.setAccessible(true);
1790
+ return field.get(request);
1791
+ }
1792
+
1793
+ private static Intent issue82Dispatch(RecordingServiceContext context, String id, int hash) {
1794
+ Intent start = buildStartIntent(id, hash, id, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
1795
+ ForegroundService.start(
1796
+ hash, start.getParcelableExtra("notification"), start.getBundleExtra("notificationBundle"));
1797
+ return context.starts.get(context.starts.size() - 1);
1798
+ }
1799
+
1800
+ private void issue82DestroyServices() {
1801
+ for (int i = controllers.size() - 1; i >= 0; i--) controllers.get(i).destroy();
1802
+ controllers.clear();
1803
+ idleMain();
1804
+ }
1805
+
1806
+ private static void issue82AssertDiagnostic(String id, String outcome) {
1807
+ assertTrue(
1808
+ ShadowLog.getLogsForTag("NOTIFEE").stream()
1809
+ .anyMatch(
1810
+ item ->
1811
+ item.type == android.util.Log.WARN
1812
+ && item.throwable == Issue81ShadowService.failure
1813
+ && item.msg.contains("PROMOTION_DENIED")
1814
+ && item.msg.contains("cause=UNKNOWN")
1815
+ && item.msg.contains("requestId=" + id)
1816
+ && item.msg.contains("notificationId=A")
1817
+ && item.msg.contains("api=" + Build.VERSION.SDK_INT)
1818
+ && item.msg.contains("type=1")
1819
+ && item.msg.contains("startId=7")
1820
+ && item.msg.contains("flags=0")
1821
+ && item.msg.contains("generation=")
1822
+ && item.msg.contains("stop=" + outcome)));
1823
+ }
1824
+
1825
+ @Test
1826
+ @Config(
1827
+ sdk = {31, 33, 34, 35},
1828
+ shadows = Issue81ShadowService.class)
1829
+ public void issue81_retryFreshDenied_recoversWithoutClaimingPromotion() throws Exception {
1830
+ issue81AssertFreshRecovery(Service.START_FLAG_RETRY);
1831
+ }
1832
+
1833
+ @Test
1834
+ @Config(
1835
+ sdk = {31, 33, 34, 35},
1836
+ shadows = Issue81ShadowService.class)
1837
+ public void issue81_redeliveryFreshDenied_recoversWithoutClaimingPromotion() throws Exception {
1838
+ issue81AssertFreshRecovery(Service.START_FLAG_REDELIVERY);
1839
+ }
1840
+
1841
+ @Test
1842
+ @Config(sdk = 33, shadows = Issue81ShadowService.class)
1843
+ public void issue81_bothFlagsAndUppercaseUuid_recovers() throws Exception {
1844
+ ForegroundService service = issue81Service();
1845
+ Intent intent = issue81UnknownStart();
1846
+ intent.putExtra(
1847
+ ISSUE81_REQUEST_ID,
1848
+ intent.getStringExtra(ISSUE81_REQUEST_ID).toUpperCase(java.util.Locale.ROOT));
1849
+ assertEquals(
1850
+ Service.START_NOT_STICKY,
1851
+ service.onStartCommand(
1852
+ intent, Service.START_FLAG_RETRY | Service.START_FLAG_REDELIVERY, 1));
1853
+ issue81AssertUnpromoted(service);
1854
+ assertEquals(java.util.Collections.singletonList(1), Issue81ShadowService.stops);
1855
+ }
1856
+
1857
+ @Test
1858
+ @Config(sdk = 33, shadows = Issue81ShadowService.class)
1859
+ public void issue81_trackedStartDenied_isFatalEvenWithFrameworkFlags() throws Exception {
1860
+ RecordingServiceContext context = useRecordingContext();
1861
+ ForegroundService service = issue81Service();
1862
+ Issue81ShadowService.rejectTitle = null;
1863
+ for (int flags :
1864
+ new int[] {
1865
+ 0,
1866
+ Service.START_FLAG_RETRY,
1867
+ Service.START_FLAG_REDELIVERY,
1868
+ Service.START_FLAG_RETRY | Service.START_FLAG_REDELIVERY
1869
+ }) {
1870
+ ForegroundService.start(811, buildNotification(), buildNotificationBundle("tracked"));
1871
+ Intent intent = context.starts.get(context.starts.size() - 1);
1872
+ String requestId = intent.getStringExtra(ISSUE81_REQUEST_ID);
1873
+ Object request = issue81Pending().get(requestId);
1874
+ assertNotNull(request);
1875
+ assertSame(
1876
+ Issue81ShadowService.failure,
1877
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(intent, flags, 1)));
1878
+ assertSame(request, issue81Pending().get(requestId));
1879
+ }
1880
+ assertEquals(4, Issue81ShadowService.promotions.size());
1881
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1882
+ issue81AssertUnpromoted(service);
1883
+ }
1884
+
1885
+ @Test
1886
+ @Config(sdk = 33, shadows = Issue81ShadowService.class)
1887
+ public void issue81_missingOrInvalidUuid_isFatal() throws Exception {
1888
+ ForegroundService service = issue81Service();
1889
+ for (String requestId :
1890
+ new String[] {
1891
+ null,
1892
+ "",
1893
+ "not-a-uuid",
1894
+ "1-1-1-1-1",
1895
+ "000000000-000-0000-0000-000000000000",
1896
+ " " + UUID.randomUUID(),
1897
+ "00000000-0000-0000-0000-00000000000z"
1898
+ }) {
1899
+ Intent intent = issue81UnknownStart();
1900
+ if (requestId == null) {
1901
+ intent.removeExtra(ISSUE81_REQUEST_ID);
1902
+ } else {
1903
+ intent.putExtra(ISSUE81_REQUEST_ID, requestId);
1904
+ }
1905
+ assertSame(
1906
+ Issue81ShadowService.failure,
1907
+ assertThrows(
1908
+ RuntimeException.class,
1909
+ () -> service.onStartCommand(intent, Service.START_FLAG_RETRY, 1)));
1910
+ }
1911
+ Intent wrongType = issue81UnknownStart().putExtra(ISSUE81_REQUEST_ID, 81);
1912
+ assertSame(
1913
+ Issue81ShadowService.failure,
1914
+ assertThrows(
1915
+ RuntimeException.class,
1916
+ () -> service.onStartCommand(wrongType, Service.START_FLAG_RETRY, 1)));
1917
+ assertEquals(8, Issue81ShadowService.promotions.size());
1918
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1919
+ }
1920
+
1921
+ @Test
1922
+ @Config(sdk = 33, shadows = Issue81ShadowService.class)
1923
+ public void issue81_missingFrameworkFlagsOrWrongAction_isFatal() throws Exception {
1924
+ ForegroundService service = issue81Service();
1925
+ Intent intentFlagsOnly = issue81UnknownStart().setFlags(0x4);
1926
+ assertSame(
1927
+ Issue81ShadowService.failure,
1928
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(intentFlagsOnly, 0, 1)));
1929
+ assertSame(
1930
+ Issue81ShadowService.failure,
1931
+ assertThrows(
1932
+ RuntimeException.class, () -> service.onStartCommand(issue81UnknownStart(), 0x4, 2)));
1933
+ for (String action : new String[] {null, "another.START"}) {
1934
+ Intent intent = issue81UnknownStart().setAction(action);
1935
+ assertSame(
1936
+ Issue81ShadowService.failure,
1937
+ assertThrows(
1938
+ RuntimeException.class,
1939
+ () -> service.onStartCommand(intent, Service.START_FLAG_RETRY, 3)));
1940
+ }
1941
+ assertEquals(4, Issue81ShadowService.promotions.size());
1942
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1943
+ }
1944
+
1945
+ @Test
1946
+ @Config(
1947
+ sdk = {24, 30, 31, 34},
1948
+ shadows = Issue81ShadowService.class)
1949
+ public void issue81_unrelatedPromotionFailures_remainFatal() throws Exception {
1950
+ ForegroundService service = issue81Service();
1951
+ List<RuntimeException> failures = new ArrayList<>();
1952
+ failures.add(new SecurityException("permission"));
1953
+ failures.add(new IllegalStateException("other denial"));
1954
+ failures.add(new RuntimeException("other failure"));
1955
+ if (Build.VERSION.SDK_INT >= 34) {
1956
+ failures.add(new MissingForegroundServiceTypeException("missing type"));
1957
+ failures.add(new InvalidForegroundServiceTypeException("invalid type"));
1958
+ }
1959
+ for (RuntimeException failure : failures) {
1960
+ Issue81ShadowService.failure = failure;
1961
+ assertSame(
1962
+ failure,
1963
+ assertThrows(
1964
+ RuntimeException.class,
1965
+ () -> service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 1)));
1966
+ }
1967
+ assertEquals(failures.size(), Issue81ShadowService.promotions.size());
1968
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1969
+ issue81AssertUnpromoted(service);
1970
+ }
1971
+
1972
+ @Test
1973
+ @Config(
1974
+ sdk = {33, 34},
1975
+ shadows = Issue81ShadowService.class)
1976
+ public void issue81_deniedA_preservesPendingBAndAllowsItsPromotion() throws Exception {
1977
+ RecordingServiceContext context = useRecordingContext();
1978
+ ForegroundService service = issue81Service();
1979
+ issue81StartB();
1980
+ Intent b = context.starts.get(0);
1981
+ Object request = issue81Pending().get(b.getStringExtra(ISSUE81_REQUEST_ID));
1982
+ FgsRunnerCapture capture = new FgsRunnerCapture();
1983
+ EventBus.register(capture);
1984
+ try {
1985
+ assertEquals(
1986
+ Service.START_NOT_STICKY,
1987
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 1));
1988
+ assertSame(request, issue81Pending().get(b.getStringExtra(ISSUE81_REQUEST_ID)));
1989
+ assertEquals(1, issue81Pending().size());
1990
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1991
+ assertTrue(capture.events.isEmpty());
1992
+ issue81AssertUnpromoted(service);
1993
+ assertFalse((boolean) issue81Field(service, "mStartCommandFailed"));
1994
+ service.onStartCommand(b, 0, 2);
1995
+ assertCurrentState("B", 812, "B");
1996
+ assertEquals(1, capture.events.size());
1997
+ assertTrue(issue81Pending().isEmpty());
1998
+ assertTrue(Issue81ShadowService.stops.isEmpty());
1999
+ } finally {
2000
+ EventBus.unregister(capture);
2001
+ }
2002
+ }
2003
+
2004
+ @Test
2005
+ @Config(sdk = 33, shadows = Issue81ShadowService.class)
2006
+ public void issue81_deliveredUnfulfilledB_isNotSettledByDeniedA() throws Exception {
2007
+ RecordingServiceContext context = useRecordingContext();
2008
+ ForegroundService service = issue81Service();
2009
+ issue81StartB();
2010
+ Intent b = context.starts.get(0);
2011
+ String requestId = b.getStringExtra(ISSUE81_REQUEST_ID);
2012
+ Object request = issue81Pending().get(requestId);
2013
+ Intent incomplete = new Intent(b);
2014
+ incomplete.removeExtra("notification");
2015
+ service.onStartCommand(incomplete, 0, 1);
2016
+ Field instance = request.getClass().getDeclaredField("instance");
2017
+ instance.setAccessible(true);
2018
+ assertSame(service, instance.get(request));
2019
+ assertTrue(Issue81ShadowService.promotions.isEmpty());
2020
+ assertEquals(
2021
+ Service.START_NOT_STICKY,
2022
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 2));
2023
+ assertSame(request, issue81Pending().get(requestId));
2024
+ assertEquals(1, issue81Pending().size());
2025
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2026
+ issue81AssertUnpromoted(service);
2027
+ service.onStartCommand(b, 0, 3);
2028
+ assertCurrentState("B", 812, "B");
2029
+ assertTrue(issue81Pending().isEmpty());
2030
+ }
2031
+
2032
+ @Test
2033
+ @Config(sdk = 33, shadows = Issue81ShadowService.class)
2034
+ public void issue81_deniedA_withQueuedStop_promotesBBeforeStopping() throws Exception {
2035
+ RecordingServiceContext context = useRecordingContext();
2036
+ ForegroundService service = issue81Service();
2037
+ issue81StartB();
2038
+ ForegroundService.stop();
2039
+ Object stopRequest = getPrivateStatic("sStopRequest");
2040
+ assertEquals(
2041
+ Service.START_NOT_STICKY,
2042
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 1));
2043
+ idleMain();
2044
+ assertSame(stopRequest, getPrivateStatic("sStopRequest"));
2045
+ assertEquals(1, issue81Pending().size());
2046
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2047
+ service.onStartCommand(context.starts.get(0), 0, 2);
2048
+ idleMain();
2049
+ assertEquals(
2050
+ java.util.Arrays.asList("promote:810", "promote:812", "stop:2"),
2051
+ Issue81ShadowService.order);
2052
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
2053
+ assertNull(ForegroundService.mCurrentNotificationId);
2054
+ }
2055
+
2056
+ @Test
2057
+ @Config(sdk = 33, shadows = Issue81ShadowService.class)
2058
+ public void issue81_refusedStop_preservesAdmissionForLaterStart() throws Exception {
2059
+ RecordingServiceContext context = useRecordingContext();
2060
+ ForegroundService service = issue81Service();
2061
+ Issue81ShadowService.stopResult = false;
2062
+ issue81ObserveRecoveryStop(service);
2063
+ assertEquals(
2064
+ Service.START_NOT_STICKY,
2065
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 1));
2066
+ idleMain();
2067
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
2068
+ issue81AssertUnpromoted(service);
2069
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
2070
+ assertEquals(java.util.Collections.singletonList(1), Issue81ShadowService.stops);
2071
+ issue81StartB();
2072
+ service.onStartCommand(context.starts.get(0), 0, 2);
2073
+ assertCurrentState("B", 812, "B");
2074
+ assertTrue(issue81Pending().isEmpty());
2075
+ assertEquals(1, Issue81ShadowService.stops.size());
2076
+ }
2077
+
2078
+ @Test
2079
+ @Config(sdk = 33, shadows = Issue81ShadowService.class)
2080
+ public void issue81_activeReplacementDenial_preservesStateAndFencesOldCompletion()
2081
+ throws Exception {
2082
+ RecordingServiceContext context = useRecordingContext();
2083
+ ForegroundService service = issue81Service();
2084
+ FgsRunnerCapture capture = new FgsRunnerCapture();
2085
+ EventBus.register(capture);
2086
+ try {
2087
+ service.onStartCommand(buildStartIntent("live", 813, "live"), 0, 1);
2088
+ Object runner = issue81Field(service, "mRunnerToken");
2089
+ ForegroundService.stop();
2090
+ issue81StartB();
2091
+ Object request =
2092
+ issue81Pending().get(context.starts.get(0).getStringExtra(ISSUE81_REQUEST_ID));
2093
+ assertSame(
2094
+ Issue81ShadowService.failure,
2095
+ assertThrows(
2096
+ RuntimeException.class,
2097
+ () -> service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 2)));
2098
+ assertCurrentState("live", 813, "live");
2099
+ assertSame(runner, issue81Field(service, "mRunnerToken"));
2100
+ assertSame(
2101
+ request, issue81Pending().get(context.starts.get(0).getStringExtra(ISSUE81_REQUEST_ID)));
2102
+ assertTrue(ForegroundService.repostIfActive("live"));
2103
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2104
+ service.onStartCommand(context.starts.get(0), 0, 3);
2105
+ capture.events.get(0).setCompletionResult();
2106
+ idleMain();
2107
+ assertCurrentState("B", 812, "B");
2108
+ assertEquals(2, capture.events.size());
2109
+ } finally {
2110
+ EventBus.unregister(capture);
2111
+ }
2112
+ }
2113
+
2114
+ @Test
2115
+ @Config(sdk = 33, shadows = Issue81ShadowService.class)
2116
+ public void issue81_priorPromotionAfterRunnerCompletion_isFatal() throws Exception {
2117
+ ForegroundService service = issue81Service();
2118
+ FgsRunnerCapture capture = new FgsRunnerCapture();
2119
+ EventBus.register(capture);
2120
+ try {
2121
+ service.onStartCommand(buildStartIntent("live", 813, "live"), 0, 1);
2122
+ capture.events.get(0).setCompletionResult();
2123
+ assertNull(ForegroundService.mCurrentNotificationId);
2124
+ assertNull(issue81Field(service, "mRunnerToken"));
2125
+ assertSame(
2126
+ Issue81ShadowService.failure,
2127
+ assertThrows(
2128
+ RuntimeException.class,
2129
+ () -> service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 2)));
2130
+ assertTrue((boolean) issue81Field(service, "mStartForegroundCalled"));
2131
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2132
+ } finally {
2133
+ EventBus.unregister(capture);
2134
+ }
2135
+ }
2136
+
2137
+ @Test
2138
+ @Config(sdk = 33, shadows = Issue81ShadowService.class)
2139
+ public void issue81_staleInstanceDenial_preservesCurrentOwner() throws Exception {
2140
+ ForegroundService old = issue81Service();
2141
+ ForegroundService current = buildService().create().get();
2142
+ current.onStartCommand(buildStartIntent("live", 813, "live"), 0, 1);
2143
+ Object runner = issue81Field(current, "mRunnerToken");
2144
+ ForegroundService.stop();
2145
+ assertSame(
2146
+ Issue81ShadowService.failure,
2147
+ assertThrows(
2148
+ RuntimeException.class,
2149
+ () -> old.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 2)));
2150
+ assertCurrentState("live", 813, "live");
2151
+ assertSame(current, getPrivateStatic("sInstance"));
2152
+ assertSame(current, getPrivateStatic("sStateOwner"));
2153
+ assertSame(runner, issue81Field(current, "mRunnerToken"));
2154
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2155
+ }
2156
+
2157
+ @Test
2158
+ @Config(sdk = 34, shadows = Issue81ShadowService.class)
2159
+ public void issue81_sameIdTypeChangeDenial_isFatalAndPreservesCache() throws Exception {
2160
+ ForegroundService service = issue81Service();
2161
+ service.onStartCommand(
2162
+ buildStartIntent("live", 813, "live", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC), 0, 1);
2163
+ Object runner = issue81Field(service, "mRunnerToken");
2164
+ Intent update =
2165
+ buildStartIntent("live", 813, "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
2166
+ .putExtra(ISSUE81_REQUEST_ID, UUID.randomUUID().toString());
2167
+ assertSame(
2168
+ Issue81ShadowService.failure,
2169
+ assertThrows(
2170
+ RuntimeException.class,
2171
+ () -> service.onStartCommand(update, Service.START_FLAG_RETRY, 2)));
2172
+ assertCurrentState("live", 813, "live");
2173
+ assertEquals(
2174
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
2175
+ ForegroundService.mCurrentForegroundServiceType);
2176
+ assertSame(runner, issue81Field(service, "mRunnerToken"));
2177
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2178
+ }
2179
+
2180
+ @Test
2181
+ @Config(sdk = 34, shadows = Issue81ShadowService.class)
2182
+ public void issue81_noneDefensiveDenial_remainsFatal() throws Exception {
2183
+ ForegroundService service = issue81Service();
2184
+ Intent none = issue81UnknownStart();
2185
+ Bundle android = none.getBundleExtra("notificationBundle").getBundle("android");
2186
+ android.remove("foregroundServiceTypes");
2187
+ ContextHolder.setApplicationContext(null);
2188
+ RuntimeException thrown =
2189
+ assertThrows(
2190
+ RuntimeException.class,
2191
+ () -> service.onStartCommand(none, Service.START_FLAG_RETRY, 1));
2192
+ assertSame(Issue81ShadowService.failure, thrown.getCause());
2193
+ assertEquals(
2194
+ java.util.Collections.singletonList(Integer.MAX_VALUE - 1),
2195
+ Issue81ShadowService.promotions);
2196
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2197
+ }
2198
+
2199
+ @Test
2200
+ @Config(sdk = 33, shadows = Issue81ShadowService.class)
2201
+ public void issue81_manifestLookupDenial_remainsFatal() throws Exception {
2202
+ ForegroundService service = issue81Service();
2203
+ Intent intent = issue81UnknownStart();
2204
+ intent
2205
+ .getBundleExtra("notificationBundle")
2206
+ .getBundle("android")
2207
+ .remove("foregroundServiceTypes");
2208
+ PackageManager manager = org.mockito.Mockito.mock(PackageManager.class);
2209
+ org.mockito.Mockito.when(
2210
+ manager.getServiceInfo(
2211
+ org.mockito.ArgumentMatchers.any(ComponentName.class),
2212
+ org.mockito.ArgumentMatchers.anyInt()))
2213
+ .thenThrow(Issue81ShadowService.failure);
2214
+ ContextHolder.setApplicationContext(
2215
+ new ContextWrapper(RuntimeEnvironment.getApplication()) {
2216
+ @Override
2217
+ public PackageManager getPackageManager() {
2218
+ return manager;
2219
+ }
2220
+ });
2221
+ assertSame(
2222
+ Issue81ShadowService.failure,
2223
+ assertThrows(
2224
+ RuntimeException.class,
2225
+ () -> service.onStartCommand(intent, Service.START_FLAG_RETRY, 1)));
2226
+ assertTrue(Issue81ShadowService.promotions.isEmpty());
2227
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2228
+ }
2229
+
2230
+ @Test
2231
+ @Config(
2232
+ sdk = 33,
2233
+ shadows = {Issue81ShadowService.class, Issue81NotificationShadowManager.class})
2234
+ public void issue81_sameIdNotifyDenial_remainsFatal() throws Exception {
2235
+ ForegroundService service = issue81Service();
2236
+ service.onStartCommand(buildStartIntent("live", 813, "live"), 0, 1);
2237
+ Intent update =
2238
+ buildStartIntent("live", 813, "A")
2239
+ .putExtra(ISSUE81_REQUEST_ID, UUID.randomUUID().toString());
2240
+ assertSame(
2241
+ Issue81ShadowService.failure,
2242
+ assertThrows(
2243
+ RuntimeException.class,
2244
+ () -> service.onStartCommand(update, Service.START_FLAG_RETRY, 2)));
2245
+ assertCurrentState("live", 813, "live");
2246
+ assertEquals(java.util.Collections.singletonList(813), Issue81ShadowService.promotions);
2247
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2248
+ }
2249
+
2250
+ @Test
2251
+ @Config(sdk = 33, shadows = Issue81ShadowService.class)
2252
+ public void issue81_stopOperationDenial_remainsFatal() throws Exception {
2253
+ ForegroundService service = issue81Service();
2254
+ Issue81ShadowService.stopFailure =
2255
+ new ForegroundServiceStartNotAllowedException("stop operation denied");
2256
+ assertSame(
2257
+ Issue81ShadowService.stopFailure,
2258
+ assertThrows(
2259
+ RuntimeException.class,
2260
+ () -> service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 1)));
2261
+ assertEquals(java.util.Collections.singletonList(1), Issue81ShadowService.stops);
2262
+ issue81AssertUnpromoted(service);
2263
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
2264
+ assertTrue(
2265
+ ShadowLog.getLogsForTag("NOTIFEE").stream()
2266
+ .anyMatch(
2267
+ item ->
2268
+ item.type == android.util.Log.WARN
2269
+ && item.throwable == Issue81ShadowService.failure
2270
+ && item.msg.contains("stop=failed")));
2271
+ }
2272
+
2273
+ @Test
2274
+ @Config(sdk = 33, shadows = Issue81ShadowService.class)
2275
+ public void issue81_pendingBLateReply_doesNotCancelStopAfterDelivery() throws Exception {
2276
+ CountDownLatch dispatched = new CountDownLatch(1);
2277
+ CountDownLatch reply = new CountDownLatch(1);
2278
+ List<Intent> starts = new ArrayList<>();
2279
+ ContextHolder.setApplicationContext(
2280
+ new ContextWrapper(RuntimeEnvironment.getApplication()) {
2281
+ @Override
2282
+ public ComponentName startForegroundService(Intent intent) {
2283
+ starts.add(new Intent(intent));
2284
+ dispatched.countDown();
2285
+ try {
2286
+ if (!reply.await(5, TimeUnit.SECONDS)) {
2287
+ throw new IllegalStateException("reply timed out");
2288
+ }
2289
+ } catch (InterruptedException e) {
2290
+ throw new RuntimeException(e);
2291
+ }
2292
+ return intent.getComponent();
2293
+ }
2294
+ });
2295
+ ForegroundService service = issue81Service();
2296
+ AtomicReference<Throwable> failure = new AtomicReference<>();
2297
+ Thread sender =
2298
+ new Thread(
2299
+ () -> {
2300
+ try {
2301
+ issue81StartB();
2302
+ } catch (Throwable e) {
2303
+ failure.set(e);
2304
+ }
2305
+ });
2306
+ sender.start();
2307
+ try {
2308
+ assertTrue(dispatched.await(5, TimeUnit.SECONDS));
2309
+ Object request = issue81Pending().get(starts.get(0).getStringExtra(ISSUE81_REQUEST_ID));
2310
+ assertEquals(
2311
+ Service.START_NOT_STICKY,
2312
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 1));
2313
+ assertSame(request, issue81Pending().get(starts.get(0).getStringExtra(ISSUE81_REQUEST_ID)));
2314
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2315
+ service.onStartCommand(starts.get(0), 0, 2);
2316
+ ForegroundService.stop();
2317
+ Object stop = getPrivateStatic("sStopRequest");
2318
+ reply.countDown();
2319
+ sender.join(5000);
2320
+ assertFalse(sender.isAlive());
2321
+ assertNull(failure.get());
2322
+ assertSame(stop, getPrivateStatic("sStopRequest"));
2323
+ idleMain();
2324
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
2325
+ assertEquals(java.util.Collections.singletonList(2), Issue81ShadowService.stops);
2326
+ } finally {
2327
+ reply.countDown();
2328
+ sender.join(5000);
2329
+ }
2330
+ }
2331
+
2332
+ @Test
2333
+ @Config(sdk = 33, shadows = Issue81ShadowService.class)
2334
+ public void issue81_successfulRetry_usesNormalRunnerAndSameIdUpdate() throws Exception {
2335
+ ForegroundService service = issue81Service();
2336
+ Issue81ShadowService.failure = null;
2337
+ FgsRunnerCapture capture = new FgsRunnerCapture();
2338
+ EventBus.register(capture);
2339
+ try {
2340
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 1);
2341
+ assertCurrentState("A", 810, "A");
2342
+ assertTrue((boolean) issue81Field(service, "mStartForegroundCalled"));
2343
+ assertEquals(1, capture.events.size());
2344
+ service.onStartCommand(
2345
+ buildStartIntent("A", 810, "updated", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
2346
+ 0,
2347
+ 2);
2348
+ assertCurrentState("A", 810, "updated");
2349
+ assertEquals(java.util.Collections.singletonList(810), Issue81ShadowService.promotions);
2350
+ assertEquals(1, capture.events.size());
2351
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2352
+ } finally {
2353
+ EventBus.unregister(capture);
2354
+ }
2355
+ }
2356
+
2357
+ @Test
2358
+ @Config(sdk = 33, shadows = Issue81ShadowService.class)
2359
+ public void issue81_retryWithoutPayload_doesNotAttemptRecovery() throws Exception {
2360
+ ForegroundService service = issue81Service();
2361
+ Intent intent = issue81UnknownStart();
2362
+ intent.removeExtra("notification");
2363
+ assertEquals(
2364
+ Service.START_NOT_STICKY, service.onStartCommand(intent, Service.START_FLAG_RETRY, 1));
2365
+ assertTrue(Issue81ShadowService.promotions.isEmpty());
2366
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2367
+ issue81AssertUnpromoted(service);
2368
+ }
2369
+
2370
+ private static final String ISSUE81_REQUEST_ID = "notifee.foregroundServiceStartRequestId";
2371
+
2372
+ @Test
2373
+ @Config(
2374
+ sdk = {31, 34},
2375
+ shadows = Issue81ShadowService.class)
2376
+ public void issue81_deferredRecovery_lateSecurityRejection_stops() throws Exception {
2377
+ issue81AssertLateRejection(new SecurityException("B rejected"));
2378
+ }
2379
+
2380
+ @Test
2381
+ @Config(
2382
+ sdk = {31, 34},
2383
+ shadows = Issue81ShadowService.class)
2384
+ public void issue81_deferredRecovery_lateFgsRejection_stops() throws Exception {
2385
+ issue81AssertLateRejection(new ForegroundServiceStartNotAllowedException("B rejected"));
2386
+ }
2387
+
2388
+ @Test
2389
+ @Config(
2390
+ sdk = {31, 34},
2391
+ shadows = Issue81ShadowService.class)
2392
+ public void issue81_deferredRecovery_lateNullComponent_stops() throws Exception {
2393
+ issue81AssertLateRejection(null);
2394
+ }
2395
+
2396
+ private void issue81AssertLateRejection(RuntimeException rejection) throws Exception {
2397
+ ForegroundService service = issue81Service();
2398
+ try (Issue81LateReply b = new Issue81LateReply(rejection, false)) {
2399
+ b.awaitDispatch();
2400
+ Object request = issue81Pending().get(b.intent.getStringExtra(ISSUE81_REQUEST_ID));
2401
+ assertNotNull(request);
2402
+ assertEquals(
2403
+ Service.START_NOT_STICKY,
2404
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11));
2405
+ idleMain();
2406
+ assertSame(request, issue81Pending().get(b.intent.getStringExtra(ISSUE81_REQUEST_ID)));
2407
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2408
+ issue81AssertUnpromoted(service);
2409
+ assertSame(service, getPrivateStatic("sInstance"));
2410
+ assertEquals(11, issue81Field(service, "mLastStartId"));
2411
+ assertNull(getPrivateStatic("sStopRequest"));
2412
+ b.settle();
2413
+ assertSame(rejection, b.failure.get());
2414
+ assertTrue(issue81Pending().isEmpty());
2415
+ issue81ObserveRecoveryStop(service);
2416
+ idleMain();
2417
+ assertEquals(
2418
+ "The last definite rejection must re-evaluate denied A's stop",
2419
+ java.util.Collections.singletonList(11),
2420
+ Issue81ShadowService.stops);
2421
+ assertTrue((boolean) issue81Field(service, "mStopInitiated"));
2422
+ issue81AssertUnpromoted(service);
2423
+ assertNull(getPrivateStatic("sStopRequest"));
2424
+ }
2425
+ }
2426
+
2427
+ @Test
2428
+ @Config(
2429
+ sdk = {31, 34},
2430
+ shadows = Issue81ShadowService.class)
2431
+ public void issue81_deferredRecovery_acceptedB_waitsForPromotion() throws Exception {
2432
+ ForegroundService service = issue81Service();
2433
+ try (Issue81LateReply b = new Issue81LateReply(null, true)) {
2434
+ b.awaitDispatch();
2435
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11);
2436
+ Object request = issue81Pending().get(b.intent.getStringExtra(ISSUE81_REQUEST_ID));
2437
+ b.settle();
2438
+ assertNull(b.failure.get());
2439
+ idleMain();
2440
+ assertSame(request, issue81Pending().get(b.intent.getStringExtra(ISSUE81_REQUEST_ID)));
2441
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2442
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
2443
+ service.onStartCommand(b.intent, 0, 12);
2444
+ idleMain();
2445
+ assertCurrentState("B", 812, "B");
2446
+ assertTrue(issue81Pending().isEmpty());
2447
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2448
+ assertNull(getPrivateStatic("sStopRequest"));
2449
+ }
2450
+ }
2451
+
2452
+ @Test
2453
+ @Config(
2454
+ sdk = {31, 34},
2455
+ shadows = Issue81ShadowService.class)
2456
+ public void issue81_deferredRecovery_deliveredB_lateNegativeReplyPreservesRun() throws Exception {
2457
+ ForegroundService service = issue81Service();
2458
+ RuntimeException rejection = new SecurityException("late B rejection after delivery");
2459
+ try (Issue81LateReply b = new Issue81LateReply(rejection, false)) {
2460
+ b.awaitDispatch();
2461
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11);
2462
+ service.onStartCommand(b.intent, 0, 12);
2463
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
2464
+ b.settle();
2465
+ assertSame(rejection, b.failure.get());
2466
+ idleMain();
2467
+ assertCurrentState("B", 812, "B");
2468
+ assertTrue((boolean) issue81Field(service, "mForegroundActive"));
2469
+ assertTrue(issue81Pending().isEmpty());
2470
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2471
+ assertNull(getPrivateStatic("sStopRequest"));
2472
+ }
2473
+ }
2474
+
2475
+ @Test
2476
+ @Config(
2477
+ sdk = {31, 34},
2478
+ shadows = Issue81ShadowService.class)
2479
+ public void issue81_deferredRecovery_ambiguousReply_keepsObligation() throws Exception {
2480
+ ForegroundService service = issue81Service();
2481
+ RuntimeException unknown = new IllegalStateException("lost B reply");
2482
+ try (Issue81LateReply b = new Issue81LateReply(unknown, false)) {
2483
+ b.awaitDispatch();
2484
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11);
2485
+ Object request = issue81Pending().get(b.intent.getStringExtra(ISSUE81_REQUEST_ID));
2486
+ b.settle();
2487
+ assertSame(unknown, b.failure.get());
2488
+ idleMain();
2489
+ assertSame(request, issue81Pending().get(b.intent.getStringExtra(ISSUE81_REQUEST_ID)));
2490
+ assertEquals(1, issue81Pending().size());
2491
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2492
+ issue81AssertUnpromoted(service);
2493
+ assertNotNull(issue81Field(service, "mDeniedStartRecovery"));
2494
+ service.onStartCommand(b.intent, 0, 12);
2495
+ idleMain();
2496
+ assertCurrentState("B", 812, "B");
2497
+ assertTrue(issue81Pending().isEmpty());
2498
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
2499
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2500
+ }
2501
+ }
2502
+
2503
+ @Test
2504
+ @Config(
2505
+ sdk = {31, 34},
2506
+ shadows = Issue81ShadowService.class)
2507
+ public void issue81_deferredRecovery_newAcceptedStart_invalidatesQueuedRecovery()
2508
+ throws Exception {
2509
+ ForegroundService service = issue81Service();
2510
+ try (Issue81LateReply b = new Issue81LateReply(null, false)) {
2511
+ b.awaitDispatch();
2512
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11);
2513
+ b.settle();
2514
+ RecordingServiceContext context = useRecordingContext();
2515
+ issue81StartB();
2516
+ idleMain();
2517
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2518
+ assertEquals(1, issue81Pending().size());
2519
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
2520
+ assertNull(getPrivateStatic("sStopRequest"));
2521
+ service.onStartCommand(context.starts.get(0), 0, 12);
2522
+ idleMain();
2523
+ assertCurrentState("B", 812, "B");
2524
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2525
+ }
2526
+ }
2527
+
2528
+ @Test
2529
+ @Config(
2530
+ sdk = {31, 34},
2531
+ shadows = Issue81ShadowService.class)
2532
+ public void issue81_deferredRecovery_newDelivery_invalidatesQueuedRecovery() throws Exception {
2533
+ ForegroundService service = issue81Service();
2534
+ try (Issue81LateReply b = new Issue81LateReply(null, false)) {
2535
+ b.awaitDispatch();
2536
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11);
2537
+ b.settle();
2538
+ // Even an incomplete new delivery supersedes the denied startId; it grants no promotion.
2539
+ Intent later = issue81UnknownStart();
2540
+ later.removeExtra("notification");
2541
+ service.onStartCommand(later, 0, 12);
2542
+ idleMain();
2543
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2544
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
2545
+ issue81AssertUnpromoted(service);
2546
+ }
2547
+ }
2548
+
2549
+ @Test
2550
+ @Config(
2551
+ sdk = {31, 34},
2552
+ shadows = Issue81ShadowService.class)
2553
+ public void issue81_deferredRecovery_refusedStop_isConsumedWithoutCleanupOrRetry()
2554
+ throws Exception {
2555
+ ForegroundService service = issue81Service();
2556
+ try (Issue81LateReply b = new Issue81LateReply(null, false)) {
2557
+ b.awaitDispatch();
2558
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11);
2559
+ b.settle();
2560
+ Issue81ShadowService.stopResult = false;
2561
+ issue81ObserveRecoveryStop(service);
2562
+ idleMain();
2563
+ assertEquals(java.util.Collections.singletonList(11), Issue81ShadowService.stops);
2564
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
2565
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
2566
+ issue81AssertUnpromoted(service);
2567
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
2568
+ issue81RejectAnotherDispatch();
2569
+ idleMain();
2570
+ assertEquals(1, Issue81ShadowService.stops.size());
2571
+ }
2572
+ }
2573
+
2574
+ @Test
2575
+ @Config(
2576
+ sdk = {31, 34},
2577
+ shadows = Issue81ShadowService.class)
2578
+ public void issue81_deferredRecovery_throwingStop_preservesErrorAndDiagnostic() throws Exception {
2579
+ ForegroundService service = issue81Service();
2580
+ try (Issue81LateReply b = new Issue81LateReply(null, false)) {
2581
+ b.awaitDispatch();
2582
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11);
2583
+ b.settle();
2584
+ Issue81ShadowService.stopFailure = new SecurityException("recovery stop failed");
2585
+ issue81ObserveRecoveryStop(service);
2586
+ assertSame(
2587
+ Issue81ShadowService.stopFailure, assertThrows(RuntimeException.class, () -> idleMain()));
2588
+ assertEquals(java.util.Collections.singletonList(11), Issue81ShadowService.stops);
2589
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
2590
+ issue81AssertUnpromoted(service);
2591
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
2592
+ assertTrue(
2593
+ ShadowLog.getLogsForTag("NOTIFEE").stream()
2594
+ .anyMatch(
2595
+ item ->
2596
+ item.throwable == Issue81ShadowService.failure
2597
+ && item.msg.contains("startId=11")
2598
+ && item.msg.contains("stop=failed")));
2599
+ issue81RejectAnotherDispatch();
2600
+ idleMain();
2601
+ assertEquals(1, Issue81ShadowService.stops.size());
2602
+ }
2603
+ }
2604
+
2605
+ @Test
2606
+ @Config(
2607
+ sdk = {31, 34},
2608
+ shadows = Issue81ShadowService.class)
2609
+ public void issue81_deferredRecovery_staleGenerations_doNotStop() throws Exception {
2610
+ ForegroundService service = issue81Service();
2611
+ for (String generation : new String[] {"sRunGeneration", "mRunGeneration"}) {
2612
+ try (Issue81LateReply b = new Issue81LateReply(null, false)) {
2613
+ b.awaitDispatch();
2614
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11);
2615
+ assertNotNull(issue81Field(service, "mDeniedStartRecovery"));
2616
+ // Isolate each immutable generation fence without introducing a separate client STOP.
2617
+ Field field = ForegroundService.class.getDeclaredField(generation);
2618
+ field.setAccessible(true);
2619
+ synchronized (getPrivateStatic("sLifecycleLock")) {
2620
+ Object owner = generation.startsWith("s") ? null : service;
2621
+ field.setLong(owner, field.getLong(owner) + 1);
2622
+ }
2623
+ b.settle();
2624
+ idleMain();
2625
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2626
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
2627
+ }
2628
+ }
2629
+ }
2630
+
2631
+ @Test
2632
+ @Config(
2633
+ sdk = {31, 34},
2634
+ shadows = Issue81ShadowService.class)
2635
+ public void issue81_deferredRecovery_replacementAndOldDestroy_preserveNewOwner()
2636
+ throws Exception {
2637
+ ForegroundService old = issue81Service();
2638
+ try (Issue81LateReply b = new Issue81LateReply(null, false)) {
2639
+ b.awaitDispatch();
2640
+ old.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11);
2641
+ b.settle();
2642
+ ForegroundService current = buildService().create().get();
2643
+ current.onStartCommand(buildStartIntent("new", 814, "new"), 0, 12);
2644
+ controllers.get(0).destroy();
2645
+ controllers.remove(0);
2646
+ idleMain();
2647
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2648
+ assertSame(current, getPrivateStatic("sInstance"));
2649
+ assertCurrentState("new", 814, "new");
2650
+ assertNull(issue81Field(old, "mDeniedStartRecovery"));
2651
+ }
2652
+ }
2653
+
2654
+ @Test
2655
+ @Config(
2656
+ sdk = {31, 34},
2657
+ shadows = Issue81ShadowService.class)
2658
+ public void issue81_deferredRecovery_destroyBeforeLateAcceptance_preservesUndeliveredB()
2659
+ throws Exception {
2660
+ ForegroundService old = issue81Service();
2661
+ try (Issue81LateReply b = new Issue81LateReply(null, true)) {
2662
+ b.awaitDispatch();
2663
+ old.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11);
2664
+ controllers.get(0).destroy();
2665
+ controllers.remove(0);
2666
+ assertNull(issue81Field(old, "mDeniedStartRecovery"));
2667
+ b.settle();
2668
+ idleMain();
2669
+ assertNull(b.failure.get());
2670
+ assertEquals(1, issue81Pending().size());
2671
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2672
+ ForegroundService current = buildService().create().get();
2673
+ current.onStartCommand(b.intent, 0, 12);
2674
+ idleMain();
2675
+ assertCurrentState("B", 812, "B");
2676
+ assertTrue(issue81Pending().isEmpty());
2677
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2678
+ }
2679
+ }
2680
+
2681
+ @Test
2682
+ @Config(
2683
+ sdk = {31, 34},
2684
+ shadows = Issue81ShadowService.class)
2685
+ public void issue81_deferredRecovery_previousFailedCommand_doesNotSuppressNewRecovery()
2686
+ throws Exception {
2687
+ ForegroundService service = issue81Service();
2688
+ assertSame(
2689
+ Issue81ShadowService.failure,
2690
+ assertThrows(
2691
+ RuntimeException.class, () -> service.onStartCommand(issue81UnknownStart(), 0, 10)));
2692
+ assertTrue((boolean) issue81Field(service, "mStartCommandFailed"));
2693
+ try (Issue81LateReply b = new Issue81LateReply(null, false)) {
2694
+ b.awaitDispatch();
2695
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11);
2696
+ b.settle();
2697
+ idleMain();
2698
+ assertEquals(java.util.Collections.singletonList(11), Issue81ShadowService.stops);
2699
+ }
2700
+ }
2701
+
2702
+ private void issue81RejectAnotherDispatch() {
2703
+ RecordingServiceContext context = useRecordingContext();
2704
+ context.startFailure = new SecurityException("another definite rejection");
2705
+ assertSame(context.startFailure, assertThrows(RuntimeException.class, () -> issue81StartB()));
2706
+ }
2707
+
2708
+ @Test
2709
+ @Config(
2710
+ sdk = {31, 34},
2711
+ shadows = Issue81ShadowService.class)
2712
+ public void issue81_deferredRecovery_newPendingStart_defersUntilItsDefiniteRejection()
2713
+ throws Exception {
2714
+ ForegroundService service = issue81Service();
2715
+ try (Issue81LateReply b = new Issue81LateReply(null, false)) {
2716
+ b.awaitDispatch();
2717
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11);
2718
+ b.settle();
2719
+ try (Issue81LateReply c = new Issue81LateReply(null, false)) {
2720
+ c.awaitDispatch();
2721
+ Object request = issue81Pending().get(c.intent.getStringExtra(ISSUE81_REQUEST_ID));
2722
+ idleMain();
2723
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2724
+ assertSame(request, issue81Pending().get(c.intent.getStringExtra(ISSUE81_REQUEST_ID)));
2725
+ assertNotNull(issue81Field(service, "mDeniedStartRecovery"));
2726
+ c.settle();
2727
+ idleMain();
2728
+ assertTrue(issue81Pending().isEmpty());
2729
+ assertEquals(java.util.Collections.singletonList(11), Issue81ShadowService.stops);
2730
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
2731
+ }
2732
+ }
2733
+ }
2734
+
2735
+ @Test
2736
+ @Config(
2737
+ sdk = {31, 34},
2738
+ shadows = Issue81ShadowService.class)
2739
+ public void issue81_deferredRecovery_terminalDelivery_replacesRecoveryWithExplicitStop()
2740
+ throws Exception {
2741
+ for (Intent terminal : new Intent[] {buildStopIntent(), null}) {
2742
+ ForegroundService service = issue81Service();
2743
+ try (Issue81LateReply b = new Issue81LateReply(null, false)) {
2744
+ b.awaitDispatch();
2745
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11);
2746
+ service.onStartCommand(terminal, 0, 12);
2747
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
2748
+ Object explicitStop = getPrivateStatic("sStopRequest");
2749
+ assertNotNull(explicitStop);
2750
+ idleMain();
2751
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2752
+ assertEquals(1, issue81Pending().size());
2753
+ b.settle();
2754
+ assertSame(explicitStop, getPrivateStatic("sStopRequest"));
2755
+ idleMain();
2756
+ assertEquals(java.util.Collections.singletonList(12), Issue81ShadowService.stops);
2757
+ assertNull(getPrivateStatic("sStopRequest"));
2758
+ }
2759
+ }
2760
+ }
2761
+
2762
+ @Test
2763
+ @Config(
2764
+ sdk = {31, 34},
2765
+ shadows = Issue81ShadowService.class)
2766
+ public void issue81_deferredRecovery_lateOldRunnerCompletion_doesNotCancelRecovery()
2767
+ throws Exception {
2768
+ ForegroundService old = issue81Service();
2769
+ FgsRunnerCapture capture = new FgsRunnerCapture();
2770
+ EventBus.register(capture);
2771
+ try {
2772
+ old.onStartCommand(buildStartIntent("old", 814, "old"), 0, 1);
2773
+ ForegroundService service = buildService().create().get();
2774
+ try (Issue81LateReply b = new Issue81LateReply(null, false)) {
2775
+ b.awaitDispatch();
2776
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11);
2777
+ Object recovery = issue81Field(service, "mDeniedStartRecovery");
2778
+ capture.events.get(0).setCompletionResult();
2779
+ assertSame(recovery, issue81Field(service, "mDeniedStartRecovery"));
2780
+ issue81AssertUnpromoted(service);
2781
+ b.settle();
2782
+ idleMain();
2783
+ assertEquals(java.util.Collections.singletonList(11), Issue81ShadowService.stops);
2784
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
2785
+ }
2786
+ } finally {
2787
+ EventBus.unregister(capture);
2788
+ }
2789
+ }
2790
+
2791
+ @Test
2792
+ @Config(
2793
+ sdk = {31, 34},
2794
+ shadows = Issue81ShadowService.class)
2795
+ public void issue81_deferredRecovery_staleOwnershipOrStartId_rejectsQueuedCallback()
2796
+ throws Exception {
2797
+ for (String authority : new String[] {"sInstance", "sStateOwner", "mLastStartId"}) {
2798
+ ForegroundService service = issue81Service();
2799
+ try (Issue81LateReply b = new Issue81LateReply(null, false)) {
2800
+ b.awaitDispatch();
2801
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11);
2802
+ b.settle();
2803
+ // Independently exercise callback revalidation after it has captured the original token.
2804
+ Field field = ForegroundService.class.getDeclaredField(authority);
2805
+ field.setAccessible(true);
2806
+ Object owner = authority.startsWith("s") ? null : service;
2807
+ Object original = field.get(owner);
2808
+ try {
2809
+ synchronized (getPrivateStatic("sLifecycleLock")) {
2810
+ field.set(owner, authority.equals("mLastStartId") ? 12 : new ForegroundService());
2811
+ }
2812
+ idleMain();
2813
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2814
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
2815
+ } finally {
2816
+ field.set(owner, original);
2817
+ }
2818
+ }
2819
+ }
2820
+ }
2821
+
2822
+ @Test
2823
+ @Config(
2824
+ sdk = {31, 34},
2825
+ shadows = Issue81ShadowService.class)
2826
+ public void issue81_deferredRecovery_deliveredUnfulfilledB_lateNullReplyKeepsObligation()
2827
+ throws Exception {
2828
+ issue81AssertUnfulfilledLateReply(null);
2829
+ }
2830
+
2831
+ @Test
2832
+ @Config(
2833
+ sdk = {31, 34},
2834
+ shadows = Issue81ShadowService.class)
2835
+ public void issue81_deferredRecovery_deliveredUnfulfilledB_lateSecurityReplyKeepsObligation()
2836
+ throws Exception {
2837
+ issue81AssertUnfulfilledLateReply(new SecurityException("B already delivered"));
2838
+ }
2839
+
2840
+ private void issue81AssertUnfulfilledLateReply(RuntimeException rejection) throws Exception {
2841
+ ForegroundService service = issue81Service();
2842
+ try (Issue81LateReply b = new Issue81LateReply(rejection, false)) {
2843
+ b.awaitDispatch();
2844
+ Intent incomplete = new Intent(b.intent);
2845
+ incomplete.removeExtra("notification");
2846
+ service.onStartCommand(incomplete, 0, 10);
2847
+ Object request = issue81Pending().get(b.intent.getStringExtra(ISSUE81_REQUEST_ID));
2848
+ assertNotNull(request);
2849
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11);
2850
+ b.settle();
2851
+ assertSame(rejection, b.failure.get());
2852
+ idleMain();
2853
+ assertSame(request, issue81Pending().get(b.intent.getStringExtra(ISSUE81_REQUEST_ID)));
2854
+ assertEquals(1, issue81Pending().size());
2855
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2856
+ issue81AssertUnpromoted(service);
2857
+ service.onStartCommand(b.intent, 0, 12);
2858
+ idleMain();
2859
+ assertCurrentState("B", 812, "B");
2860
+ assertTrue(issue81Pending().isEmpty());
2861
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2862
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
2863
+ assertNull(getPrivateStatic("sStopRequest"));
2864
+ }
2865
+ }
2866
+
2867
+ @Test
2868
+ @Config(
2869
+ sdk = {31, 34},
2870
+ shadows = Issue81ShadowService.class)
2871
+ public void issue81_deferredRecovery_explicitStop_invalidatesRecoveryWhileBPending()
2872
+ throws Exception {
2873
+ ForegroundService service = issue81Service();
2874
+ try (Issue81LateReply b = new Issue81LateReply(null, false)) {
2875
+ b.awaitDispatch();
2876
+ service.onStartCommand(issue81UnknownStart(), Service.START_FLAG_RETRY, 11);
2877
+ assertNotNull(issue81Field(service, "mDeniedStartRecovery"));
2878
+ ForegroundService.stop();
2879
+ assertNull(issue81Field(service, "mDeniedStartRecovery"));
2880
+ Object explicitStop = getPrivateStatic("sStopRequest");
2881
+ assertNotNull(explicitStop);
2882
+ idleMain();
2883
+ assertTrue(Issue81ShadowService.stops.isEmpty());
2884
+ assertEquals(1, issue81Pending().size());
2885
+ b.settle();
2886
+ assertSame(explicitStop, getPrivateStatic("sStopRequest"));
2887
+ idleMain();
2888
+ assertEquals(java.util.Collections.singletonList(11), Issue81ShadowService.stops);
2889
+ assertNull(getPrivateStatic("sStopRequest"));
2890
+ }
2891
+ }
2892
+
2893
+ // Suspend the actual START sender's platform reply, without blocking lifecycle delivery.
2894
+ private static final class Issue81LateReply implements AutoCloseable {
2895
+ final CountDownLatch dispatched = new CountDownLatch(1);
2896
+ final CountDownLatch reply = new CountDownLatch(1);
2897
+ final AtomicReference<Throwable> failure = new AtomicReference<>();
2898
+ final Thread sender;
2899
+ Intent intent;
2900
+
2901
+ Issue81LateReply(RuntimeException rejection, boolean accepted) {
2902
+ this(rejection, accepted, "B", 812);
2903
+ }
2904
+
2905
+ Issue81LateReply(RuntimeException rejection, boolean accepted, String id, int hash) {
2906
+ ContextHolder.setApplicationContext(
2907
+ new ContextWrapper(RuntimeEnvironment.getApplication()) {
2908
+ @Override
2909
+ public ComponentName startForegroundService(Intent start) {
2910
+ intent = new Intent(start);
2911
+ dispatched.countDown();
2912
+ try {
2913
+ if (!reply.await(5, TimeUnit.SECONDS)) {
2914
+ throw new IllegalStateException("B reply timed out");
2915
+ }
2916
+ } catch (InterruptedException e) {
2917
+ Thread.currentThread().interrupt();
2918
+ throw new IllegalStateException(e);
2919
+ }
2920
+ if (rejection != null) {
2921
+ throw rejection;
2922
+ }
2923
+ return accepted ? start.getComponent() : null;
2924
+ }
2925
+ });
2926
+ sender =
2927
+ new Thread(
2928
+ () -> {
2929
+ try {
2930
+ Intent start =
2931
+ buildStartIntent(id, hash, id, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
2932
+ ForegroundService.start(
2933
+ hash,
2934
+ start.getParcelableExtra("notification"),
2935
+ start.getBundleExtra("notificationBundle"));
2936
+ } catch (Throwable e) {
2937
+ failure.set(e);
2938
+ }
2939
+ });
2940
+ sender.start();
2941
+ }
2942
+
2943
+ void awaitDispatch() throws Exception {
2944
+ assertTrue(dispatched.await(5, TimeUnit.SECONDS));
2945
+ }
2946
+
2947
+ void settle() throws Exception {
2948
+ reply.countDown();
2949
+ sender.join(5000);
2950
+ assertFalse(sender.isAlive());
2951
+ }
2952
+
2953
+ @Override
2954
+ public void close() throws Exception {
2955
+ settle();
2956
+ }
2957
+ }
2958
+
2959
+ private ForegroundService issue81Service() throws Exception {
2960
+ Issue81ShadowService.failure =
2961
+ Build.VERSION.SDK_INT >= 31
2962
+ ? new ForegroundServiceStartNotAllowedException("initial A promotion denied")
2963
+ : new IllegalStateException("initial A promotion denied");
2964
+ Issue81ShadowService.stopFailure = null;
2965
+ Issue81ShadowService.rejectTitle = "A";
2966
+ Issue81ShadowService.stopResult = true;
2967
+ Issue81ShadowService.beforeStop = null;
2968
+ Issue81ShadowService.beforePromotion = null;
2969
+ Issue81ShadowService.promotions.clear();
2970
+ Issue81ShadowService.types.clear();
2971
+ Issue81ShadowService.stops.clear();
2972
+ Issue81ShadowService.order.clear();
2973
+ ShadowLog.clear();
2974
+ if (Build.VERSION.SDK_INT >= 34) {
2975
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
2976
+ }
2977
+ if (Build.VERSION.SDK_INT >= 26) {
2978
+ createChannel();
2979
+ }
2980
+ return buildService().create().get();
2981
+ }
2982
+
2983
+ private static Intent issue81UnknownStart() {
2984
+ return buildStartIntent("A", 810, "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
2985
+ .putExtra(ISSUE81_REQUEST_ID, UUID.randomUUID().toString());
2986
+ }
2987
+
2988
+ private static void issue81StartB() {
2989
+ Intent b = buildStartIntent("B", 812, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
2990
+ ForegroundService.start(
2991
+ 812, b.getParcelableExtra("notification"), b.getBundleExtra("notificationBundle"));
2992
+ }
2993
+
2994
+ private void issue81AssertFreshRecovery(int flags) throws Exception {
2995
+ ForegroundService service = issue81Service();
2996
+ issue81ObserveRecoveryStop(service);
2997
+ FgsRunnerCapture capture = new FgsRunnerCapture();
2998
+ EventBus.register(capture);
2999
+ try {
3000
+ int result;
3001
+ try {
3002
+ result = service.onStartCommand(issue81UnknownStart(), flags, 7);
3003
+ } catch (RuntimeException denied) {
3004
+ // RED is admissible only after the real handler attempted the real notification/type.
3005
+ assertSame(Issue81ShadowService.failure, denied);
3006
+ assertEquals(java.util.Collections.singletonList(810), Issue81ShadowService.promotions);
3007
+ assertEquals(
3008
+ java.util.Collections.singletonList(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
3009
+ Issue81ShadowService.types);
3010
+ issue81AssertUnpromoted(service);
3011
+ throw denied;
3012
+ }
3013
+ assertEquals(Service.START_NOT_STICKY, result);
3014
+ assertEquals(java.util.Collections.singletonList(810), Issue81ShadowService.promotions);
3015
+ assertEquals(
3016
+ java.util.Collections.singletonList(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
3017
+ Issue81ShadowService.types);
3018
+ assertEquals(java.util.Arrays.asList("promote:810", "stop:7"), Issue81ShadowService.order);
3019
+ issue81AssertUnpromoted(service);
3020
+ assertTrue((boolean) issue81Field(service, "mStopInitiated"));
3021
+ assertTrue(capture.events.isEmpty());
3022
+ assertTrue(issue81Pending().isEmpty());
3023
+ assertTrue(
3024
+ ShadowLog.getLogsForTag("NOTIFEE").stream()
3025
+ .anyMatch(
3026
+ item ->
3027
+ item.type == android.util.Log.WARN
3028
+ && item.throwable == Issue81ShadowService.failure
3029
+ && item.msg.contains("startId=7")
3030
+ && item.msg.contains("flags=" + flags)));
3031
+ ForegroundService current = buildService().create().get();
3032
+ current.onStartCommand(buildStartIntent("new", 814, "new"), 0, 8);
3033
+ controllers.get(0).destroy();
3034
+ controllers.remove(0);
3035
+ assertCurrentState("new", 814, "new");
3036
+ assertSame(current, getPrivateStatic("sInstance"));
3037
+ } finally {
3038
+ EventBus.unregister(capture);
3039
+ }
3040
+ }
3041
+
3042
+ private static void issue81AssertUnpromoted(ForegroundService service) throws Exception {
3043
+ assertFalse((boolean) issue81Field(service, "mStartForegroundCalled"));
3044
+ assertFalse((boolean) issue81Field(service, "mForegroundActive"));
3045
+ assertFalse((boolean) issue81Field(service, "mStartCommandSatisfied"));
3046
+ assertNull(issue81Field(service, "mRunnerToken"));
3047
+ assertEquals(0, issue81Field(service, "mRunFirstStartId"));
3048
+ assertNull(ForegroundService.mCurrentNotificationId);
3049
+ assertEquals(-1, ForegroundService.mCurrentForegroundServiceType);
3050
+ assertNull(getPrivateStatic("mCurrentNotificationBundle"));
3051
+ assertNull(getPrivateStatic("mCurrentNotification"));
3052
+ assertEquals(0, getPrivateStatic("mCurrentHashCode"));
3053
+ }
3054
+
3055
+ private static void issue81ObserveRecoveryStop(ForegroundService service) {
3056
+ Issue81ShadowService.beforeStop =
3057
+ () -> {
3058
+ try {
3059
+ assertTrue(Thread.holdsLock(getPrivateStatic("sLifecycleLock")));
3060
+ assertFalse(Thread.holdsLock(getPrivateStatic("sLock")));
3061
+ issue81AssertUnpromoted(service);
3062
+ assertFalse((boolean) issue81Field(service, "mStopInitiated"));
3063
+ } catch (Exception e) {
3064
+ throw new AssertionError(e);
3065
+ }
3066
+ };
3067
+ }
3068
+
3069
+ private static Object issue81Field(Object instance, String name) throws Exception {
3070
+ Field field = ForegroundService.class.getDeclaredField(name);
3071
+ field.setAccessible(true);
3072
+ return field.get(instance);
3073
+ }
3074
+
3075
+ @SuppressWarnings("unchecked")
3076
+ private static Map<String, ?> issue81Pending() throws Exception {
3077
+ return (Map<String, ?>) getPrivateStatic("sPendingStarts");
3078
+ }
3079
+
3080
+ @Implements(Service.class)
3081
+ public static class Issue81ShadowService extends ShadowService {
3082
+ static RuntimeException failure;
3083
+ static RuntimeException stopFailure;
3084
+ static String rejectTitle;
3085
+ static boolean stopResult;
3086
+ static Runnable beforeStop;
3087
+ static Runnable beforePromotion;
3088
+ static final List<Integer> promotions = new ArrayList<>();
3089
+ static final List<Integer> types = new ArrayList<>();
3090
+ static final List<Integer> stops = new ArrayList<>();
3091
+ static final List<String> order = new ArrayList<>();
3092
+ private boolean forwardingTyped;
3093
+
3094
+ private void recordPromotion(int id, Notification notification, int type) {
3095
+ promotions.add(id);
3096
+ types.add(type);
3097
+ order.add("promote:" + id);
3098
+ if (beforePromotion != null) beforePromotion.run();
3099
+ if (failure != null
3100
+ && (id == Integer.MAX_VALUE - 1
3101
+ || rejectTitle == null
3102
+ || rejectTitle.equals(notification.extras.getString(Notification.EXTRA_TITLE)))) {
3103
+ throw failure;
3104
+ }
3105
+ }
3106
+
3107
+ @Implementation
3108
+ protected void startForeground(int id, Notification notification) {
3109
+ if (!forwardingTyped) {
3110
+ recordPromotion(id, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE);
3111
+ }
3112
+ super.startForeground(id, notification);
3113
+ }
3114
+
3115
+ @Implementation
3116
+ protected void startForeground(int id, Notification notification, int type) {
3117
+ recordPromotion(id, notification, type);
3118
+ // ShadowService's typed overload delegates to the two-argument shadow. Record only the
3119
+ // actual platform invocation so the witness cannot count that delegation as a second call.
3120
+ forwardingTyped = true;
3121
+ try {
3122
+ super.startForeground(id, notification, type);
3123
+ } finally {
3124
+ forwardingTyped = false;
3125
+ }
3126
+ }
3127
+
3128
+ @Implementation
3129
+ protected boolean stopSelfResult(int startId) {
3130
+ if (beforeStop != null) {
3131
+ beforeStop.run();
3132
+ }
3133
+ stops.add(startId);
3134
+ order.add("stop:" + startId);
3135
+ if (stopFailure != null) {
3136
+ throw stopFailure;
3137
+ }
3138
+ return stopResult && super.stopSelfResult(startId);
3139
+ }
3140
+ }
3141
+
3142
+ @Implements(NotificationManager.class)
3143
+ public static class Issue81NotificationShadowManager extends ShadowNotificationManager {
3144
+ @Implementation
3145
+ protected void notify(String tag, int id, Notification notification) {
3146
+ if ("A".equals(notification.extras.getString(Notification.EXTRA_TITLE))) {
3147
+ throw Issue81ShadowService.failure;
3148
+ }
3149
+ super.notify(tag, id, notification);
3150
+ }
83
3151
  }
84
3152
 
85
3153
  @Test
@@ -1983,7 +5051,8 @@ public class ForegroundServiceTest {
1983
5051
  ForegroundService service = controller.create().get();
1984
5052
 
1985
5053
  String id = "fgs-timeout-api34";
1986
- seedActiveForegroundServiceState(id);
5054
+ seedActiveForegroundServiceState(
5055
+ service, id, 42, ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
1987
5056
 
1988
5057
  FgsEventCapture capture = new FgsEventCapture();
1989
5058
  EventBus.register(capture);
@@ -2025,7 +5094,7 @@ public class ForegroundServiceTest {
2025
5094
  ForegroundService service = controller.create().get();
2026
5095
 
2027
5096
  String id = "fgs-timeout-api35";
2028
- seedActiveForegroundServiceState(id);
5097
+ seedActiveForegroundServiceState(service, id, 7, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
2029
5098
 
2030
5099
  FgsEventCapture capture = new FgsEventCapture();
2031
5100
  EventBus.register(capture);
@@ -2173,19 +5242,12 @@ public class ForegroundServiceTest {
2173
5242
  return (int) method.invoke(shadowService);
2174
5243
  }
2175
5244
 
2176
- /**
2177
- * Populates the service's private static tracking fields directly, simulating the post-START
2178
- * state that onTimeout expects to observe. Using reflection here (rather than running a full
2179
- * START first) keeps the onTimeout tests SDK-independent — the START path would trip the API 34+
2180
- * manifest-type check, but onTimeout itself is sdk-agnostic because its body only uses pre-API-34
2181
- * primitives.
2182
- */
2183
- private static void seedActiveForegroundServiceState(String id) throws Exception {
2184
- ForegroundService.mCurrentNotificationId = id;
2185
- ForegroundService.mCurrentForegroundServiceType = 1;
2186
- setPrivateStatic("mCurrentNotificationBundle", buildNotificationBundle(id));
2187
- setPrivateStatic("mCurrentNotification", buildNotification());
2188
- setPrivateStatic("mCurrentHashCode", id.hashCode());
5245
+ // Exercise timeout against an actual successful START, including its lifecycle authority.
5246
+ private static void seedActiveForegroundServiceState(
5247
+ ForegroundService service, String id, int startId, int type) throws Exception {
5248
+ createChannel();
5249
+ declareForegroundServiceTypes(type);
5250
+ service.onStartCommand(buildStartIntent(id, id.hashCode(), "FGS test", type), 0, startId);
2189
5251
  }
2190
5252
 
2191
5253
  private static void setPrivateStatic(String name, Object value) throws Exception {