react-native-notify-kit 10.7.1 → 10.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/android/src/main/java/app/notifee/core/ForegroundService.java +408 -103
  2. package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +1761 -44
  3. package/android/src/test/java/app/notifee/core/NotificationManagerActionRoutingTest.java +37 -0
  4. package/dist/fcm/parseFcmPayload.d.ts +1 -0
  5. package/dist/fcm/parseFcmPayload.js.map +1 -1
  6. package/dist/fcm/reconstructNotification.js +96 -8
  7. package/dist/fcm/reconstructNotification.js.map +1 -1
  8. package/dist/validators/validateAndroidPressAction.js +3 -0
  9. package/dist/validators/validateAndroidPressAction.js.map +1 -1
  10. package/dist/validators/validateIOSNotification.js +5 -2
  11. package/dist/validators/validateIOSNotification.js.map +1 -1
  12. package/dist/version.d.ts +1 -1
  13. package/dist/version.js +1 -1
  14. package/ios/NotifeeCore/NotifeeCore.m +36 -3
  15. package/ios/NotifeeCore/NotifeeCoreExtensionHelper.m +82 -3
  16. package/ios/NotifeeCore/NotifeeCoreFcmIdentity.h +91 -0
  17. package/package.json +1 -1
  18. package/server/README.md +5 -3
  19. package/server/dist/buildPayload.d.ts.map +1 -1
  20. package/server/dist/buildPayload.js +2 -0
  21. package/server/dist/buildPayload.js.map +1 -1
  22. package/server/dist/ios.d.ts.map +1 -1
  23. package/server/dist/ios.js +8 -1
  24. package/server/dist/ios.js.map +1 -1
  25. package/server/dist/serialize.d.ts +2 -0
  26. package/server/dist/serialize.d.ts.map +1 -1
  27. package/server/dist/serialize.js +6 -0
  28. package/server/dist/serialize.js.map +1 -1
  29. package/server/dist/types.d.ts +1 -1
  30. package/server/dist/types.d.ts.map +1 -1
  31. package/server/dist/validation.d.ts.map +1 -1
  32. package/server/dist/validation.js +160 -6
  33. package/server/dist/validation.js.map +1 -1
  34. package/server/src/buildPayload.ts +2 -0
  35. package/server/src/ios.ts +8 -1
  36. package/server/src/serialize.ts +8 -0
  37. package/server/src/types.ts +1 -0
  38. package/server/src/validation.ts +187 -6
  39. package/src/fcm/parseFcmPayload.ts +1 -0
  40. package/src/fcm/reconstructNotification.ts +105 -10
  41. package/src/internal/fcmContract.d.ts +57 -12
  42. package/src/validators/validateAndroidPressAction.ts +4 -0
  43. package/src/validators/validateIOSNotification.ts +7 -2
  44. package/src/version.ts +1 -1
@@ -1,27 +1,42 @@
1
1
  package app.notifee.core;
2
2
 
3
3
  import static org.junit.Assert.assertEquals;
4
+ import static org.junit.Assert.assertFalse;
4
5
  import static org.junit.Assert.assertNotNull;
5
6
  import static org.junit.Assert.assertNull;
7
+ import static org.junit.Assert.assertThrows;
6
8
  import static org.junit.Assert.assertTrue;
7
9
  import static org.junit.Assert.fail;
8
10
 
11
+ import android.app.ForegroundServiceStartNotAllowedException;
12
+ import android.app.InvalidForegroundServiceTypeException;
13
+ import android.app.MissingForegroundServiceTypeException;
9
14
  import android.app.Notification;
10
15
  import android.app.NotificationChannel;
11
16
  import android.app.NotificationManager;
12
17
  import android.app.Service;
13
18
  import android.content.ComponentName;
14
19
  import android.content.Context;
20
+ import android.content.ContextWrapper;
15
21
  import android.content.Intent;
16
22
  import android.content.pm.PackageManager;
17
23
  import android.content.pm.ServiceInfo;
24
+ import android.os.Build;
18
25
  import android.os.Bundle;
26
+ import android.os.Looper;
19
27
  import androidx.core.app.NotificationCompat;
28
+ import app.notifee.core.event.ForegroundServiceEvent;
20
29
  import app.notifee.core.event.NotificationEvent;
21
30
  import java.lang.reflect.Field;
22
31
  import java.lang.reflect.Method;
23
32
  import java.util.ArrayList;
24
33
  import java.util.List;
34
+ import java.util.concurrent.CountDownLatch;
35
+ import java.util.concurrent.ExecutorService;
36
+ import java.util.concurrent.Executors;
37
+ import java.util.concurrent.Future;
38
+ import java.util.concurrent.TimeUnit;
39
+ import java.util.concurrent.atomic.AtomicReference;
25
40
  import org.greenrobot.eventbus.Subscribe;
26
41
  import org.greenrobot.eventbus.ThreadMode;
27
42
  import org.junit.After;
@@ -35,27 +50,1193 @@ import org.robolectric.android.controller.ServiceController;
35
50
  import org.robolectric.annotation.Config;
36
51
  import org.robolectric.annotation.Implementation;
37
52
  import org.robolectric.annotation.Implements;
53
+ import org.robolectric.shadows.ShadowLog;
54
+ import org.robolectric.shadows.ShadowNotificationManager;
38
55
  import org.robolectric.shadows.ShadowService;
39
56
 
40
57
  @RunWith(RobolectricTestRunner.class)
41
58
  public class ForegroundServiceTest {
42
59
 
60
+ private final List<ServiceController<ForegroundService>> controllers = new ArrayList<>();
61
+
43
62
  @Before
44
63
  public void setUp() {
45
64
  // Initialize ContextHolder so the service can access the application context
46
65
  ContextHolder.setApplicationContext(RuntimeEnvironment.getApplication());
47
66
  }
48
67
 
49
- @After
50
- public void tearDown() throws Exception {
51
- // Reset public and private static fields to prevent cross-test pollution. The three private
52
- // statics are cleared via reflection here so that tests which seed them directly (to exercise
53
- // onTimeout without running the full START path) cannot leak state into neighbouring tests.
54
- ForegroundService.mCurrentNotificationId = null;
55
- ForegroundService.mCurrentForegroundServiceType = -1;
56
- setPrivateStatic("mCurrentNotificationBundle", null);
57
- setPrivateStatic("mCurrentNotification", null);
58
- setPrivateStatic("mCurrentHashCode", 0);
68
+ @After
69
+ public void tearDown() throws Exception {
70
+ for (int i = controllers.size() - 1; i >= 0; i--) {
71
+ controllers.get(i).destroy();
72
+ }
73
+ org.robolectric.Shadows.shadowOf(Looper.getMainLooper()).idle();
74
+ ContextHolder.setApplicationContext(RuntimeEnvironment.getApplication());
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
+ ForegroundService.mCurrentNotificationId = null;
79
+ ForegroundService.mCurrentForegroundServiceType = -1;
80
+ setPrivateStatic("mCurrentNotificationBundle", null);
81
+ setPrivateStatic("mCurrentNotification", null);
82
+ setPrivateStatic("mCurrentHashCode", 0);
83
+ }
84
+
85
+ @Test
86
+ @Config(sdk = {24, 26, 30, 33, 34})
87
+ public void stop_withoutInstance_doesNotStartServiceOrEmitRunner() {
88
+ RecordingServiceContext context = useRecordingContext();
89
+ FgsRunnerCapture capture = new FgsRunnerCapture();
90
+ EventBus.register(capture);
91
+ try {
92
+ ForegroundService.stop();
93
+ idleMain();
94
+ assertTrue(context.starts.isEmpty());
95
+ assertEquals(0, context.stops);
96
+ assertTrue(capture.events.isEmpty());
97
+ assertNull(ForegroundService.mCurrentNotificationId);
98
+ } finally {
99
+ EventBus.unregister(capture);
100
+ }
101
+ }
102
+
103
+ @Test
104
+ @Config(sdk = 33)
105
+ public void stop_startStopDestroySecondStop_doesNotRecreateService() {
106
+ RecordingServiceContext context = useRecordingContext();
107
+ ServiceController<ForegroundService> controller = buildService();
108
+ ForegroundService service = controller.create().get();
109
+ createChannel();
110
+ service.onStartCommand(buildStartIntent("first", 101), 0, 1);
111
+
112
+ ForegroundService.stop();
113
+ idleMain();
114
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
115
+ controller.destroy();
116
+ controllers.remove(controller);
117
+ ForegroundService.stop();
118
+ idleMain();
119
+ assertTrue(context.starts.isEmpty());
120
+ assertNull(ForegroundService.mCurrentNotificationId);
121
+ }
122
+
123
+ @Test
124
+ @Config(sdk = 33)
125
+ public void stop_nullNotificationCache_stillStopsLiveInstance() throws Exception {
126
+ RecordingServiceContext context = useRecordingContext();
127
+ ForegroundService service = buildService().create().get();
128
+ createChannel();
129
+ service.onStartCommand(buildStartIntent("live", 102), 0, 1);
130
+ ForegroundService.mCurrentNotificationId = null;
131
+ ForegroundService.mCurrentForegroundServiceType = -1;
132
+ setPrivateStatic("mCurrentNotificationBundle", null);
133
+ setPrivateStatic("mCurrentNotification", null);
134
+ setPrivateStatic("mCurrentHashCode", 0);
135
+
136
+ ForegroundService.stop();
137
+ idleMain();
138
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
139
+ assertTrue(context.starts.isEmpty());
140
+ }
141
+
142
+ @Test
143
+ @Config(sdk = 33)
144
+ public void stop_concurrentDestroy_doesNotDispatchNewGeneration() throws Exception {
145
+ RecordingServiceContext context = useRecordingContext();
146
+ ServiceController<ForegroundService> controller = buildService();
147
+ ForegroundService service = controller.create().get();
148
+ createChannel();
149
+ service.onStartCommand(buildStartIntent("old", 103), 0, 1);
150
+ CountDownLatch begin = new CountDownLatch(1);
151
+ AtomicReference<Throwable> failure = new AtomicReference<>();
152
+ Thread stop =
153
+ new Thread(
154
+ () -> {
155
+ try {
156
+ assertTrue(begin.await(5, TimeUnit.SECONDS));
157
+ ForegroundService.stop();
158
+ } catch (Throwable e) {
159
+ failure.set(e);
160
+ }
161
+ });
162
+ stop.start();
163
+ begin.countDown();
164
+ controller.destroy();
165
+ controllers.remove(controller);
166
+ stop.join(5000);
167
+ assertFalse(stop.isAlive());
168
+ assertNull(failure.get());
169
+ idleMain();
170
+ assertTrue(context.starts.isEmpty());
171
+ }
172
+
173
+ @Test
174
+ @Config(sdk = 33)
175
+ public void onDestroy_oldInstance_preservesNewInstanceAndAllNotificationState() throws Exception {
176
+ RecordingServiceContext context = useRecordingContext();
177
+ ServiceController<ForegroundService> oldController = buildService();
178
+ ForegroundService old = oldController.create().get();
179
+ createChannel();
180
+ old.onStartCommand(buildStartIntent("old", 104), 0, 1);
181
+ ForegroundService current = buildService().create().get();
182
+ current.onStartCommand(buildStartIntent("new", 105, "new title"), 0, 2);
183
+ oldController.destroy();
184
+ controllers.remove(oldController);
185
+
186
+ assertCurrentState("new", 105, "new title");
187
+ ForegroundService.stop();
188
+ idleMain();
189
+ assertTrue(org.robolectric.Shadows.shadowOf(current).isStoppedBySelf());
190
+ assertTrue(context.starts.isEmpty());
191
+ }
192
+
193
+ @Test
194
+ @Config(sdk = 33)
195
+ public void runnerCompletion_oldInstance_doesNotDemoteOrClearNewInstance() throws Exception {
196
+ useRecordingContext();
197
+ FgsRunnerCapture capture = new FgsRunnerCapture();
198
+ EventBus.register(capture);
199
+ try {
200
+ ForegroundService old = buildService().create().get();
201
+ createChannel();
202
+ old.onStartCommand(buildStartIntent("old", 106), 0, 1);
203
+ ForegroundService current = buildService().create().get();
204
+ current.onStartCommand(buildStartIntent("new", 107, "new title"), 0, 2);
205
+ assertEquals(2, capture.events.size());
206
+
207
+ capture.events.get(0).setCompletionResult();
208
+ idleMain();
209
+ assertCurrentState("new", 107, "new title");
210
+ assertFalse(org.robolectric.Shadows.shadowOf(current).isForegroundStopped());
211
+ } finally {
212
+ EventBus.unregister(capture);
213
+ }
214
+ }
215
+
216
+ @Test
217
+ @Config(sdk = 33)
218
+ public void runnerCompletion_oldRunOnSameInstance_preservesNewRun() throws Exception {
219
+ useRecordingContext();
220
+ FgsRunnerCapture capture = new FgsRunnerCapture();
221
+ EventBus.register(capture);
222
+ try {
223
+ ForegroundService service = buildService().create().get();
224
+ createChannel();
225
+ service.onStartCommand(buildStartIntent("old", 108), 0, 1);
226
+ service.onStartCommand(buildStopIntent(), 0, 2);
227
+ service.onStartCommand(buildStartIntent("new", 109, "new title"), 0, 3);
228
+ assertEquals(2, capture.events.size());
229
+
230
+ capture.events.get(0).setCompletionResult();
231
+ idleMain();
232
+ assertCurrentState("new", 109, "new title");
233
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
234
+ capture.events.get(1).setCompletionResult();
235
+ idleMain();
236
+ assertNull(ForegroundService.mCurrentNotificationId);
237
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
238
+ } finally {
239
+ EventBus.unregister(capture);
240
+ }
241
+ }
242
+
243
+ @Test
244
+ @Config(sdk = 33)
245
+ public void stop_acceptedPendingStart_fulfillsRealContractBeforeStopping() {
246
+ RecordingServiceContext context = useRecordingContext();
247
+ createChannel();
248
+ ForegroundService.start(110, buildNotification(), buildNotificationBundle("pending"));
249
+ ForegroundService.stop();
250
+ idleMain();
251
+ assertEquals(1, context.starts.size());
252
+ ForegroundService service = buildService().create().get();
253
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
254
+ service.onStartCommand(context.starts.get(0), 0, 1);
255
+ idleMain();
256
+ assertEquals(110, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
257
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
258
+ assertNull(ForegroundService.mCurrentNotificationId);
259
+ assertEquals(1, context.starts.size());
260
+ }
261
+
262
+ @Test
263
+ @Config(sdk = 33)
264
+ public void stop_betweenCreateAndPendingDelivery_doesNotStopBeforeRealPromotion() {
265
+ RecordingServiceContext context = useRecordingContext();
266
+ createChannel();
267
+ ForegroundService.start(111, buildNotification(), buildNotificationBundle("pending"));
268
+ ForegroundService service = buildService().create().get();
269
+ ForegroundService.stop();
270
+ idleMain();
271
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
272
+ assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
273
+
274
+ service.onStartCommand(context.starts.get(0), 0, 1);
275
+ idleMain();
276
+ assertEquals(111, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
277
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
278
+ assertEquals(1, context.starts.size());
279
+ }
280
+
281
+ @Test
282
+ @Config(sdk = 33)
283
+ public void stop_newStartAcceptedBeforeQueuedStop_doesNotCancelLaterStart() throws Exception {
284
+ RecordingServiceContext context = useRecordingContext();
285
+ createChannel();
286
+ ForegroundService service = buildService().create().get();
287
+ service.onStartCommand(buildStartIntent("same", 112, "A"), 0, 1);
288
+ ForegroundService.stop();
289
+ Intent later = buildStartIntent("same", 112, "B");
290
+ ForegroundService.start(
291
+ 112, later.getParcelableExtra("notification"), later.getBundleExtra("notificationBundle"));
292
+ idleMain();
293
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
294
+ service.onStartCommand(context.starts.get(0), 0, 2);
295
+ idleMain();
296
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
297
+ assertCurrentState("same", 112, "B");
298
+ }
299
+
300
+ @Test
301
+ @Config(
302
+ sdk = {28, 33, 34},
303
+ shadows = {RejectBStartForegroundShadowService.class, RejectBNotificationShadowManager.class})
304
+ public void stop_failedRestart_preservesPreviousSubmittedStateAndRunner() throws Exception {
305
+ RecordingServiceContext context = useRecordingContext();
306
+ if (Build.VERSION.SDK_INT >= 34) {
307
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
308
+ }
309
+ createChannel();
310
+ FgsRunnerCapture capture = new FgsRunnerCapture();
311
+ EventBus.register(capture);
312
+ try {
313
+ ForegroundService service = buildService().create().get();
314
+ Intent a = buildStartIntent("same", 177, "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
315
+ ForegroundService.start(
316
+ 177, a.getParcelableExtra("notification"), a.getBundleExtra("notificationBundle"));
317
+ service.onStartCommand(context.starts.get(0), 0, 1);
318
+ Field tokenField = ForegroundService.class.getDeclaredField("mRunnerToken");
319
+ tokenField.setAccessible(true);
320
+ Object tokenA = tokenField.get(service);
321
+ int typeA = ForegroundService.mCurrentForegroundServiceType;
322
+
323
+ ForegroundService.stop();
324
+ Intent b = buildStartIntent("same", 177, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
325
+ ForegroundService.start(
326
+ 177, b.getParcelableExtra("notification"), b.getBundleExtra("notificationBundle"));
327
+ SecurityException failure =
328
+ assertThrows(
329
+ SecurityException.class, () -> service.onStartCommand(context.starts.get(1), 0, 2));
330
+
331
+ assertEquals("posting B rejected", failure.getMessage());
332
+ assertEquals(
333
+ "A",
334
+ org.robolectric.Shadows.shadowOf(
335
+ (NotificationManager)
336
+ RuntimeEnvironment.getApplication()
337
+ .getSystemService(Context.NOTIFICATION_SERVICE))
338
+ .getNotification(177)
339
+ .extras
340
+ .getString(Notification.EXTRA_TITLE));
341
+ assertCurrentState("same", 177, "A");
342
+ assertEquals(typeA, ForegroundService.mCurrentForegroundServiceType);
343
+ assertTrue(tokenA == tokenField.get(service));
344
+ assertEquals(1, capture.events.size());
345
+ assertTrue(ForegroundService.repostIfActive("same"));
346
+ idleMain();
347
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
348
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
349
+ capture.events.get(0).setCompletionResult();
350
+ idleMain();
351
+ assertCurrentState("same", 177, "A");
352
+ assertTrue(tokenA == tokenField.get(service));
353
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
354
+
355
+ // A failed reservation must still be replaced by a fresh runner when C succeeds in the
356
+ // reserved session, even if C uses a different notification ID.
357
+ Intent c = buildStartIntent("new", 178, "C", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
358
+ ForegroundService.start(
359
+ 178, c.getParcelableExtra("notification"), c.getBundleExtra("notificationBundle"));
360
+ service.onStartCommand(context.starts.get(2), 0, 3);
361
+ assertCurrentState("new", 178, "C");
362
+ assertFalse(tokenA == tokenField.get(service));
363
+ assertEquals(2, capture.events.size());
364
+ capture.events.get(0).setCompletionResult();
365
+ idleMain();
366
+ assertCurrentState("new", 178, "C");
367
+ capture.events.get(1).setCompletionResult();
368
+ idleMain();
369
+ assertNull(ForegroundService.mCurrentNotificationId);
370
+ } finally {
371
+ EventBus.unregister(capture);
372
+ }
373
+ }
374
+
375
+ @Test
376
+ @Config(sdk = 33)
377
+ public void onStartCommand_stopAndNullWithPendingStart_doNotSuppressRealStart() {
378
+ RecordingServiceContext context = useRecordingContext();
379
+ createChannel();
380
+ ForegroundService.start(113, buildNotification(), buildNotificationBundle("pending"));
381
+ ForegroundService service = buildService().create().get();
382
+ service.onStartCommand(buildStopIntent(), 0, 1);
383
+ service.onStartCommand(null, 0, 2);
384
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
385
+ assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
386
+
387
+ service.onStartCommand(context.starts.get(0), 0, 3);
388
+ idleMain();
389
+ assertEquals(113, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
390
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
391
+ }
392
+
393
+ @Test
394
+ @Config(
395
+ sdk = {31, 33, 34, 35},
396
+ shadows = DefensiveFailureShadowService.class)
397
+ public void onStartCommand_stopAndNullDenied_areNonFatalAndDoNotClaimPromotion()
398
+ throws Exception {
399
+ if (Build.VERSION.SDK_INT >= 34) {
400
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
401
+ }
402
+ DefensiveFailureShadowService.failure = new ForegroundServiceStartNotAllowedException("denied");
403
+ DefensiveFailureShadowService.attempts = 0;
404
+ ShadowLog.clear();
405
+ ForegroundService service = buildService().create().get();
406
+ service.onStartCommand(buildStopIntent(), 0, 1);
407
+ service.onStartCommand(null, 0, 2);
408
+
409
+ assertEquals(2, DefensiveFailureShadowService.attempts);
410
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
411
+ assertTrue(
412
+ ShadowLog.getLogsForTag("NOTIFEE").stream()
413
+ .anyMatch(item -> item.throwable == DefensiveFailureShadowService.failure));
414
+ }
415
+
416
+ @Test
417
+ @Config(sdk = 31, shadows = DefensiveFailureShadowService.class)
418
+ public void onStartCommand_stopDeniedApi31_isNonFatal() {
419
+ DefensiveFailureShadowService.failure = new ForegroundServiceStartNotAllowedException("denied");
420
+ ForegroundService service = buildService().create().get();
421
+ service.onStartCommand(buildStopIntent(), 0, 1);
422
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
423
+ }
424
+
425
+ @Test
426
+ @Config(
427
+ sdk = {24, 30, 33},
428
+ shadows = DefensiveFailureShadowService.class)
429
+ public void onStartCommand_stopGenericIllegalState_remainsObservable() {
430
+ assertTerminalFailureObservable(new IllegalStateException("unrelated ISE"));
431
+ }
432
+
433
+ @Test
434
+ @Config(sdk = 34, shadows = DefensiveFailureShadowService.class)
435
+ public void onStartCommand_stopMissingType_remainsObservable() throws Exception {
436
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
437
+ assertTerminalFailureObservable(new MissingForegroundServiceTypeException("missing type"));
438
+ }
439
+
440
+ @Test
441
+ @Config(sdk = 34, shadows = DefensiveFailureShadowService.class)
442
+ public void onStartCommand_stopInvalidType_remainsObservable() throws Exception {
443
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
444
+ assertTerminalFailureObservable(new InvalidForegroundServiceTypeException("invalid type"));
445
+ }
446
+
447
+ @Test
448
+ @Config(sdk = 34, shadows = DefensiveFailureShadowService.class)
449
+ public void onStartCommand_noneDenied_isNotSoftSuppressed() throws Exception {
450
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
451
+ DefensiveFailureShadowService.failure =
452
+ new ForegroundServiceStartNotAllowedException("NONE denied");
453
+ ForegroundService service = buildService().create().get();
454
+ Intent none = buildStartIntent("none", 114, "none", ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE);
455
+ // The model normalizes NONE to MANIFEST on API 34. Removing only the application context
456
+ // makes manifest resolution return NONE; the Service still has its declared defensive type.
457
+ ContextHolder.setApplicationContext(null);
458
+ RuntimeException thrown =
459
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(none, 0, 1));
460
+ assertTrue(
461
+ thrown == DefensiveFailureShadowService.failure
462
+ || thrown.getCause() == DefensiveFailureShadowService.failure);
463
+ assertEquals(Integer.MAX_VALUE - 1, DefensiveFailureShadowService.lastId);
464
+ assertEquals(
465
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC, DefensiveFailureShadowService.lastType);
466
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
467
+ }
468
+
469
+ @Test
470
+ @Config(sdk = 33, shadows = SecurityDefensiveChannelShadowManager.class)
471
+ public void onStartCommand_terminalChannelSecurityException_remainsObservable() {
472
+ for (Intent terminal : new Intent[] {buildStopIntent(), null}) {
473
+ ForegroundService service = buildService().create().get();
474
+ SecurityException thrown =
475
+ assertThrows(SecurityException.class, () -> service.onStartCommand(terminal, 0, 1));
476
+ assertEquals("defensive channel failure", thrown.getMessage());
477
+ assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
478
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
479
+ }
480
+ }
481
+
482
+ @Test
483
+ @Config(sdk = 33, shadows = SecurityStopForegroundShadowService.class)
484
+ public void onStartCommand_terminalTeardownSecurityException_remainsObservable() {
485
+ for (Intent terminal : new Intent[] {buildStopIntent(), null}) {
486
+ ForegroundService service = buildService().create().get();
487
+ SecurityException thrown =
488
+ assertThrows(SecurityException.class, () -> service.onStartCommand(terminal, 0, 1));
489
+ assertEquals("defensive teardown failure", thrown.getMessage());
490
+ assertEquals(
491
+ Integer.MAX_VALUE - 1,
492
+ org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
493
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
494
+ }
495
+ }
496
+
497
+ @Test
498
+ @Config(sdk = 33, shadows = DefensiveFailureShadowService.class)
499
+ public void onStartCommand_pendingStartDenied_isNotSoftSuppressedByStop() {
500
+ RecordingServiceContext context = useRecordingContext();
501
+ ForegroundService.start(115, buildNotification(), buildNotificationBundle("pending"));
502
+ ForegroundService.stop();
503
+ DefensiveFailureShadowService.failure =
504
+ new ForegroundServiceStartNotAllowedException("real START denied");
505
+ ForegroundService service = buildService().create().get();
506
+ assertThrows(
507
+ ForegroundServiceStartNotAllowedException.class,
508
+ () -> service.onStartCommand(context.starts.get(0), 0, 1));
509
+ idleMain();
510
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
511
+ }
512
+
513
+ private void assertTerminalFailureObservable(RuntimeException failure) {
514
+ DefensiveFailureShadowService.failure = failure;
515
+ ForegroundService service = buildService().create().get();
516
+ RuntimeException thrown =
517
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(buildStopIntent(), 0, 1));
518
+ assertTrue(thrown == failure || thrown.getCause() == failure);
519
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
520
+ }
521
+
522
+ @Test
523
+ @Config(sdk = 33)
524
+ public void runnerCompletion_stopSupersededByNewStart_preservesLaterRun() throws Exception {
525
+ RecordingServiceContext context = useRecordingContext();
526
+ FgsRunnerCapture capture = new FgsRunnerCapture();
527
+ EventBus.register(capture);
528
+ try {
529
+ ForegroundService service = buildService().create().get();
530
+ createChannel();
531
+ service.onStartCommand(buildStartIntent("same", 116, "A"), 0, 1);
532
+ ForegroundService.stop();
533
+ Intent later = buildStartIntent("same", 116, "B");
534
+ ForegroundService.start(
535
+ 116,
536
+ later.getParcelableExtra("notification"),
537
+ later.getBundleExtra("notificationBundle"));
538
+ service.onStartCommand(context.starts.get(0), 0, 2);
539
+ capture.events.get(0).setCompletionResult();
540
+ idleMain();
541
+
542
+ assertCurrentState("same", 116, "B");
543
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
544
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
545
+ assertEquals(2, capture.events.size());
546
+ capture.events.get(1).setCompletionResult();
547
+ idleMain();
548
+ assertNull(ForegroundService.mCurrentNotificationId);
549
+ } finally {
550
+ EventBus.unregister(capture);
551
+ }
552
+ }
553
+
554
+ @Test
555
+ @Config(sdk = 33)
556
+ public void stop_createdButUnpublishedStart_waitsForRealStartCommand() {
557
+ RecordingServiceContext context = useRecordingContext();
558
+ ForegroundService service = buildService().create().get();
559
+ ForegroundService.stop();
560
+ idleMain();
561
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
562
+ assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
563
+ createChannel();
564
+ service.onStartCommand(buildStartIntent("unknown", 117), 0, 1);
565
+ idleMain();
566
+ assertEquals(117, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
567
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
568
+ assertTrue(context.starts.isEmpty());
569
+ }
570
+
571
+ @Test
572
+ @Config(sdk = 33)
573
+ public void stop_multiplePendingStarts_waitsForAllRealCommands() {
574
+ RecordingServiceContext context = useRecordingContext();
575
+ createChannel();
576
+ ForegroundService.start(118, buildNotification(), buildNotificationBundle("pending"));
577
+ ForegroundService.start(118, buildNotification(), buildNotificationBundle("pending"));
578
+ ForegroundService.stop();
579
+ ForegroundService service = buildService().create().get();
580
+ service.onStartCommand(context.starts.get(0), 0, 1);
581
+ idleMain();
582
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
583
+ service.onStartCommand(context.starts.get(1), 0, 2);
584
+ idleMain();
585
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
586
+ assertEquals(2, org.robolectric.Shadows.shadowOf(service).getStopSelfResultId());
587
+ assertEquals(2, context.starts.size());
588
+ }
589
+
590
+ @Test
591
+ @Config(sdk = 33)
592
+ public void onStartCommand_pendingWithoutPromotion_doesNotAcquireStopAuthority() {
593
+ RecordingServiceContext context = useRecordingContext();
594
+ // Accepted START, but its delivery has no notification. STOP must not turn this into a
595
+ // successful/absent START obligation, even though malformed START handling is out of scope.
596
+ ForegroundService.start(119, null, buildNotificationBundle("pending"));
597
+ ForegroundService.stop();
598
+ ForegroundService service = buildService().create().get();
599
+ service.onStartCommand(context.starts.get(0), 0, 1);
600
+ idleMain();
601
+ ForegroundService.stop();
602
+ service.onStartCommand(null, 0, 2);
603
+ idleMain();
604
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
605
+ assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
606
+ assertEquals(1, context.starts.size());
607
+ }
608
+
609
+ @Test
610
+ @Config(sdk = 33)
611
+ public void start_dispatchFailure_doesNotCancelEarlierStop() {
612
+ RecordingServiceContext context = useRecordingContext();
613
+ createChannel();
614
+ ForegroundService service = buildService().create().get();
615
+ service.onStartCommand(buildStartIntent("live", 120), 0, 1);
616
+ ForegroundService.stop();
617
+ context.startFailure = new ForegroundServiceStartNotAllowedException("dispatch rejected");
618
+ assertThrows(
619
+ ForegroundServiceStartNotAllowedException.class,
620
+ () -> ForegroundService.start(121, buildNotification(), buildNotificationBundle("failed")));
621
+ idleMain();
622
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
623
+ assertNull(ForegroundService.mCurrentNotificationId);
624
+ assertEquals(1, context.starts.size());
625
+ }
626
+
627
+ @Test
628
+ @Config(sdk = 33, shadows = RefuseStopSelfShadowService.class)
629
+ public void stop_frameworkRefusesTokenStop_preservesCurrentState() throws Exception {
630
+ RecordingServiceContext context = useRecordingContext();
631
+ ForegroundService service = buildService().create().get();
632
+ createChannel();
633
+ service.onStartCommand(buildStartIntent("live", 122, "live title"), 0, 1);
634
+ ForegroundService.stop();
635
+ idleMain();
636
+ assertCurrentState("live", 122, "live title");
637
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
638
+ assertTrue(context.starts.isEmpty());
639
+ }
640
+
641
+ @Test
642
+ @Config(sdk = 34)
643
+ public void onTimeout_oldInstance_doesNotClearNewStateOrEmitNewInstanceEvent() throws Exception {
644
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
645
+ createChannel();
646
+ ForegroundService old = buildService().create().get();
647
+ old.onStartCommand(
648
+ buildStartIntent("old", 123, "old", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
649
+ 0,
650
+ 1);
651
+ ForegroundService current = buildService().create().get();
652
+ current.onStartCommand(
653
+ buildStartIntent(
654
+ "new", 124, "new title", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
655
+ 0,
656
+ 2);
657
+ FgsEventCapture capture = new FgsEventCapture();
658
+ EventBus.register(capture);
659
+ try {
660
+ old.onTimeout(1);
661
+ assertCurrentState("new", 124, "new title");
662
+ assertEquals(
663
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
664
+ ForegroundService.mCurrentForegroundServiceType);
665
+ assertTrue(capture.events.isEmpty());
666
+ } finally {
667
+ EventBus.unregister(capture);
668
+ }
669
+ }
670
+
671
+ @Test
672
+ @Config(sdk = 33, shadows = FailStopForegroundShadowService.class)
673
+ public void onStartCommand_denialOutsidePromotion_isStillFatal() {
674
+ ForegroundService service = buildService().create().get();
675
+ RuntimeException thrown =
676
+ assertThrows(RuntimeException.class, () -> service.onStartCommand(buildStopIntent(), 0, 1));
677
+ assertTrue(
678
+ thrown instanceof ForegroundServiceStartNotAllowedException
679
+ || thrown.getCause() instanceof ForegroundServiceStartNotAllowedException);
680
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
681
+ }
682
+
683
+ @Test
684
+ @Config(sdk = 33)
685
+ public void stop_duringStartAcceptance_preservesAndThenStopsAcceptedStart() throws Exception {
686
+ CountDownLatch dispatchEntered = new CountDownLatch(1);
687
+ CountDownLatch releaseDispatch = new CountDownLatch(1);
688
+ CountDownLatch stopAttempted = new CountDownLatch(1);
689
+ RecordingServiceContext context =
690
+ new RecordingServiceContext(RuntimeEnvironment.getApplication()) {
691
+ @Override
692
+ public ComponentName startForegroundService(Intent intent) {
693
+ dispatchEntered.countDown();
694
+ try {
695
+ assertTrue(releaseDispatch.await(5, TimeUnit.SECONDS));
696
+ } catch (InterruptedException e) {
697
+ throw new AssertionError(e);
698
+ }
699
+ return super.startForegroundService(intent);
700
+ }
701
+ };
702
+ ContextHolder.setApplicationContext(context);
703
+ createChannel();
704
+ Notification notification = buildNotification();
705
+ Bundle bundle = buildNotificationBundle("pending");
706
+ ExecutorService workers = Executors.newFixedThreadPool(2);
707
+ try {
708
+ Future<?> start = workers.submit(() -> ForegroundService.start(125, notification, bundle));
709
+ assertTrue(dispatchEntered.await(5, TimeUnit.SECONDS));
710
+ Future<?> stop =
711
+ workers.submit(
712
+ () -> {
713
+ stopAttempted.countDown();
714
+ ForegroundService.stop();
715
+ });
716
+ assertTrue(stopAttempted.await(5, TimeUnit.SECONDS));
717
+ releaseDispatch.countDown();
718
+ start.get(5, TimeUnit.SECONDS);
719
+ stop.get(5, TimeUnit.SECONDS);
720
+
721
+ ForegroundService service = buildService().create().get();
722
+ service.onStartCommand(context.starts.get(0), 0, 1);
723
+ idleMain();
724
+ assertEquals(
725
+ 125, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
726
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
727
+ assertEquals(1, context.starts.size());
728
+ } finally {
729
+ releaseDispatch.countDown();
730
+ workers.shutdownNow();
731
+ }
732
+ }
733
+
734
+ @Test
735
+ @Config(sdk = 33)
736
+ public void stop_queuedForDestroyedInstance_doesNotTouchLaterInstance() throws Exception {
737
+ RecordingServiceContext context = useRecordingContext();
738
+ createChannel();
739
+ ServiceController<ForegroundService> oldController = buildService();
740
+ ForegroundService old = oldController.create().get();
741
+ old.onStartCommand(buildStartIntent("old", 126), 0, 1);
742
+ ForegroundService.stop();
743
+ oldController.destroy();
744
+ controllers.remove(oldController);
745
+ ForegroundService current = buildService().create().get();
746
+ current.onStartCommand(buildStartIntent("new", 127, "new title"), 0, 2);
747
+ idleMain();
748
+ assertCurrentState("new", 127, "new title");
749
+ assertFalse(org.robolectric.Shadows.shadowOf(current).isStoppedBySelf());
750
+ assertTrue(context.starts.isEmpty());
751
+ }
752
+
753
+ @Test
754
+ @Config(sdk = 33)
755
+ public void stop_liveInstanceWithUnavailableApplicationContext_stillReachesInstance() {
756
+ RecordingServiceContext context = useRecordingContext();
757
+ createChannel();
758
+ ForegroundService service = buildService().create().get();
759
+ service.onStartCommand(buildStartIntent("live", 128), 0, 1);
760
+ ContextHolder.setApplicationContext(null);
761
+ ForegroundService.stop();
762
+ idleMain();
763
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
764
+ assertTrue(context.starts.isEmpty());
765
+ }
766
+
767
+ @Test
768
+ @Config(sdk = 24)
769
+ public void stop_pendingStartBeforeApi26_preservesPlainStartContract() {
770
+ RecordingServiceContext context = useRecordingContext();
771
+ ForegroundService.start(129, buildNotification(), buildNotificationBundle("pending"));
772
+ ForegroundService.stop();
773
+ ForegroundService service = buildService().create().get();
774
+ service.onStartCommand(context.starts.get(0), 0, 1);
775
+ idleMain();
776
+ assertEquals(129, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
777
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
778
+ assertEquals(1, context.starts.size());
779
+ }
780
+
781
+ @Test
782
+ @Config(sdk = 33)
783
+ public void runnerCompletion_afterStopBeforeNewDelivery_doesNotDemotePendingRun() {
784
+ RecordingServiceContext context = useRecordingContext();
785
+ createChannel();
786
+ FgsRunnerCapture capture = new FgsRunnerCapture();
787
+ EventBus.register(capture);
788
+ try {
789
+ ForegroundService service = buildService().create().get();
790
+ service.onStartCommand(buildStartIntent("same", 130), 0, 1);
791
+ ForegroundService.stop();
792
+ ForegroundService.start(130, buildNotification(), buildNotificationBundle("same"));
793
+ capture.events.get(0).setCompletionResult();
794
+ idleMain();
795
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
796
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
797
+ service.onStartCommand(context.starts.get(0), 0, 2);
798
+ assertEquals(2, capture.events.size());
799
+ assertEquals("same", ForegroundService.mCurrentNotificationId);
800
+ } finally {
801
+ EventBus.unregister(capture);
802
+ }
803
+ }
804
+
805
+ @Test
806
+ @Config(sdk = 33)
807
+ public void stop_laterRealPromotion_settlesEarlierDeliveredUnknownContract() {
808
+ RecordingServiceContext context = useRecordingContext();
809
+ ForegroundService.start(131, null, buildNotificationBundle("unknown"));
810
+ ForegroundService.stop();
811
+ ForegroundService service = buildService().create().get();
812
+ service.onStartCommand(context.starts.get(0), 0, 1);
813
+ idleMain();
814
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
815
+ createChannel();
816
+ ForegroundService.start(132, buildNotification(), buildNotificationBundle("valid"));
817
+ service.onStartCommand(context.starts.get(1), 0, 2);
818
+ ForegroundService.stop();
819
+ idleMain();
820
+ assertEquals(132, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
821
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
822
+ assertNull(ForegroundService.mCurrentNotificationId);
823
+ assertEquals(2, context.starts.size());
824
+ }
825
+
826
+ @Test
827
+ @Config(sdk = 28, shadows = RejectBStartForegroundShadowService.class)
828
+ public void stop_failedUpdateOnRealForegroundInstance_doesNotFenceStopForever() throws Exception {
829
+ RecordingServiceContext context = useRecordingContext();
830
+ createChannel();
831
+ ForegroundService service = buildService().create().get();
832
+ service.onStartCommand(buildStartIntent("same", 133, "A"), 0, 1);
833
+ Intent update = buildStartIntent("same", 133, "B");
834
+ ForegroundService.start(
835
+ 133,
836
+ update.getParcelableExtra("notification"),
837
+ update.getBundleExtra("notificationBundle"));
838
+ assertThrows(
839
+ SecurityException.class, () -> service.onStartCommand(context.starts.get(0), 0, 2));
840
+ assertCurrentState("same", 133, "A");
841
+ ForegroundService.stop();
842
+ idleMain();
843
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
844
+ assertNull(ForegroundService.mCurrentNotificationId);
845
+ }
846
+
847
+ @Test
848
+ @Config(sdk = 34)
849
+ public void onTimeout_oldRunOnSameInstance_preservesNewRun() throws Exception {
850
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
851
+ createChannel();
852
+ ForegroundService service = buildService().create().get();
853
+ service.onStartCommand(
854
+ buildStartIntent("old", 134, "old", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
855
+ 0,
856
+ 1);
857
+ service.onStartCommand(buildStopIntent(), 0, 2);
858
+ service.onStartCommand(
859
+ buildStartIntent(
860
+ "new", 135, "new title", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
861
+ 0,
862
+ 3);
863
+ FgsEventCapture capture = new FgsEventCapture();
864
+ EventBus.register(capture);
865
+ try {
866
+ service.onTimeout(1);
867
+ assertCurrentState("new", 135, "new title");
868
+ assertEquals(
869
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
870
+ ForegroundService.mCurrentForegroundServiceType);
871
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
872
+ assertTrue(capture.events.isEmpty());
873
+ } finally {
874
+ EventBus.unregister(capture);
875
+ }
876
+ }
877
+
878
+ @Test
879
+ @Config(sdk = 34)
880
+ public void onTimeout_sameRunEarlierStartId_preservesLatestUpdateTimeoutBehavior()
881
+ throws Exception {
882
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
883
+ createChannel();
884
+ ForegroundService service = buildService().create().get();
885
+ service.onStartCommand(
886
+ buildStartIntent("same", 136, "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
887
+ 0,
888
+ 1);
889
+ service.onStartCommand(
890
+ buildStartIntent("same", 136, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
891
+ 0,
892
+ 2);
893
+ FgsEventCapture capture = new FgsEventCapture();
894
+ EventBus.register(capture);
895
+ try {
896
+ service.onTimeout(1);
897
+ assertNull(ForegroundService.mCurrentNotificationId);
898
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
899
+ assertEquals(1, capture.events.size());
900
+ assertEquals("B", capture.events.get(0).getNotification().toBundle().getString("title"));
901
+ } finally {
902
+ EventBus.unregister(capture);
903
+ }
904
+ }
905
+
906
+ @Test
907
+ @Config(sdk = 33)
908
+ public void start_serviceDeliveryBeforeDispatchReturns_canFulfillRealContract() throws Exception {
909
+ CountDownLatch dispatchEntered = new CountDownLatch(1);
910
+ CountDownLatch promotionObserved = new CountDownLatch(1);
911
+ AtomicReference<Boolean> blocked = new AtomicReference<>(false);
912
+ RecordingServiceContext context =
913
+ new RecordingServiceContext(RuntimeEnvironment.getApplication()) {
914
+ @Override
915
+ public ComponentName startForegroundService(Intent intent) {
916
+ ComponentName result = super.startForegroundService(intent);
917
+ dispatchEntered.countDown();
918
+ try {
919
+ // The timeout releases the simulated Binder reply even for the broken candidate.
920
+ blocked.set(!promotionObserved.await(2, TimeUnit.SECONDS));
921
+ } catch (InterruptedException e) {
922
+ throw new AssertionError(e);
923
+ }
924
+ return result;
925
+ }
926
+ };
927
+ ContextHolder.setApplicationContext(context);
928
+ createChannel();
929
+ Notification notification = buildNotification();
930
+ Bundle bundle = buildNotificationBundle("pending");
931
+ ExecutorService worker = Executors.newSingleThreadExecutor();
932
+ try {
933
+ Future<?> start = worker.submit(() -> ForegroundService.start(137, notification, bundle));
934
+ assertTrue(dispatchEntered.await(5, TimeUnit.SECONDS));
935
+ ForegroundService service = buildService().create().get();
936
+ service.onStartCommand(context.starts.get(0), 0, 1);
937
+ promotionObserved.countDown();
938
+ start.get(5, TimeUnit.SECONDS);
939
+ assertFalse("Service promotion waited for the dispatch caller", blocked.get());
940
+ assertEquals(
941
+ 137, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
942
+ } finally {
943
+ promotionObserved.countDown();
944
+ worker.shutdownNow();
945
+ }
946
+ }
947
+
948
+ @Test
949
+ @Config(sdk = 33)
950
+ public void start_ambiguousDispatchFailure_retainsRealObligationUntilDelivery() {
951
+ RecordingServiceContext context = useRecordingContext();
952
+ context.startFailure = new IllegalStateException("Binder outcome unknown");
953
+ assertThrows(
954
+ IllegalStateException.class,
955
+ () ->
956
+ ForegroundService.start(138, buildNotification(), buildNotificationBundle("pending")));
957
+ ForegroundService.stop();
958
+ ForegroundService service = buildService().create().get();
959
+ service.onStartCommand(null, 0, 1);
960
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
961
+ assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
962
+ createChannel();
963
+ service.onStartCommand(context.starts.get(0), 0, 2);
964
+ idleMain();
965
+ assertEquals(138, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
966
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
967
+ }
968
+
969
+ @Test
970
+ @Config(sdk = 33)
971
+ public void onStartCommand_oldAmbiguousDelivery_preservesNewForegroundRun() throws Exception {
972
+ RecordingServiceContext context = useRecordingContext();
973
+ createChannel();
974
+ context.startFailure = new IllegalStateException("old reply unknown");
975
+ assertThrows(
976
+ IllegalStateException.class,
977
+ () -> ForegroundService.start(139, buildNotification(), buildNotificationBundle("old")));
978
+ ForegroundService.stop();
979
+ context.startFailure = null;
980
+ Intent fresh = buildStartIntent("new", 140, "new title");
981
+ ForegroundService.start(
982
+ 140, fresh.getParcelableExtra("notification"), fresh.getBundleExtra("notificationBundle"));
983
+ FgsRunnerCapture capture = new FgsRunnerCapture();
984
+ EventBus.register(capture);
985
+ try {
986
+ ForegroundService service = buildService().create().get();
987
+ service.onStartCommand(context.starts.get(1), 0, 1);
988
+ service.onStartCommand(context.starts.get(0), 0, 2);
989
+ idleMain();
990
+ assertCurrentState("new", 140, "new title");
991
+ assertEquals(1, capture.events.size());
992
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
993
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
994
+ capture.events.get(0).setCompletionResult();
995
+ assertNull(ForegroundService.mCurrentNotificationId);
996
+ } finally {
997
+ EventBus.unregister(capture);
998
+ }
999
+ }
1000
+
1001
+ @Test
1002
+ @Config(sdk = 33)
1003
+ public void onStartCommand_oldAmbiguousDeliveryAfterNewRunnerEnds_fulfillsThenStopsOnlyOldWork() {
1004
+ RecordingServiceContext context = useRecordingContext();
1005
+ createChannel();
1006
+ context.startFailure = new IllegalStateException("old reply unknown");
1007
+ assertThrows(
1008
+ IllegalStateException.class,
1009
+ () -> ForegroundService.start(141, buildNotification(), buildNotificationBundle("old")));
1010
+ ForegroundService.stop();
1011
+ context.startFailure = null;
1012
+ ForegroundService.start(142, buildNotification(), buildNotificationBundle("new"));
1013
+ FgsRunnerCapture capture = new FgsRunnerCapture();
1014
+ EventBus.register(capture);
1015
+ try {
1016
+ ForegroundService service = buildService().create().get();
1017
+ service.onStartCommand(context.starts.get(1), 0, 1);
1018
+ capture.events.get(0).setCompletionResult();
1019
+ service.onStartCommand(context.starts.get(0), 0, 2);
1020
+ idleMain();
1021
+ assertEquals(
1022
+ 141, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
1023
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
1024
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
1025
+ assertNull(ForegroundService.mCurrentNotificationId);
1026
+ assertEquals(1, capture.events.size());
1027
+ } finally {
1028
+ EventBus.unregister(capture);
1029
+ }
1030
+ }
1031
+
1032
+ @Test
1033
+ @Config(sdk = 33)
1034
+ public void start_lateDispatchReply_doesNotCancelStopAfterRealDelivery() throws Exception {
1035
+ CountDownLatch dispatchEntered = new CountDownLatch(1);
1036
+ CountDownLatch releaseReply = new CountDownLatch(1);
1037
+ RecordingServiceContext context =
1038
+ new RecordingServiceContext(RuntimeEnvironment.getApplication()) {
1039
+ @Override
1040
+ public ComponentName startForegroundService(Intent intent) {
1041
+ ComponentName result = super.startForegroundService(intent);
1042
+ dispatchEntered.countDown();
1043
+ try {
1044
+ assertTrue(releaseReply.await(5, TimeUnit.SECONDS));
1045
+ } catch (InterruptedException e) {
1046
+ throw new AssertionError(e);
1047
+ }
1048
+ return result;
1049
+ }
1050
+ };
1051
+ ContextHolder.setApplicationContext(context);
1052
+ createChannel();
1053
+ ExecutorService worker = Executors.newSingleThreadExecutor();
1054
+ Notification notification = buildNotification();
1055
+ Bundle bundle = buildNotificationBundle("pending");
1056
+ try {
1057
+ Future<?> start = worker.submit(() -> ForegroundService.start(143, notification, bundle));
1058
+ assertTrue(dispatchEntered.await(5, TimeUnit.SECONDS));
1059
+ ForegroundService service = buildService().create().get();
1060
+ service.onStartCommand(context.starts.get(0), 0, 1);
1061
+ ForegroundService.stop();
1062
+ releaseReply.countDown();
1063
+ start.get(5, TimeUnit.SECONDS);
1064
+ idleMain();
1065
+ assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
1066
+ assertNull(ForegroundService.mCurrentNotificationId);
1067
+ } finally {
1068
+ releaseReply.countDown();
1069
+ worker.shutdownNow();
1070
+ }
1071
+ }
1072
+
1073
+ @Test
1074
+ @Config(
1075
+ sdk = {33, 34},
1076
+ shadows = DenyOldNotificationShadowService.class)
1077
+ public void onStartCommand_supersededStartDenial_remainsARealStartFailure() throws Exception {
1078
+ if (Build.VERSION.SDK_INT >= 34) {
1079
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
1080
+ }
1081
+ RecordingServiceContext context = useRecordingContext();
1082
+ createChannel();
1083
+ Intent old = buildStartIntent("old", 144, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
1084
+ context.startFailure = new IllegalStateException("old reply unknown");
1085
+ assertThrows(
1086
+ IllegalStateException.class,
1087
+ () ->
1088
+ ForegroundService.start(
1089
+ 144,
1090
+ old.getParcelableExtra("notification"),
1091
+ old.getBundleExtra("notificationBundle")));
1092
+ ForegroundService.stop();
1093
+ context.startFailure = null;
1094
+ ForegroundService.start(145, buildNotification(), buildNotificationBundle("new"));
1095
+ FgsRunnerCapture capture = new FgsRunnerCapture();
1096
+ EventBus.register(capture);
1097
+ try {
1098
+ ForegroundService service = buildService().create().get();
1099
+ service.onStartCommand(context.starts.get(1), 0, 1);
1100
+ capture.events.get(0).setCompletionResult();
1101
+ assertThrows(
1102
+ ForegroundServiceStartNotAllowedException.class,
1103
+ () -> service.onStartCommand(context.starts.get(0), 0, 2));
1104
+ idleMain();
1105
+ assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
1106
+ assertNull(ForegroundService.mCurrentNotificationId);
1107
+ } finally {
1108
+ EventBus.unregister(capture);
1109
+ }
1110
+ }
1111
+
1112
+ @Implements(Service.class)
1113
+ public static class DenyOldNotificationShadowService extends ShadowService {
1114
+ @Implementation
1115
+ protected void startForeground(int id, Notification notification) {
1116
+ if ("B".equals(notification.extras.getString(Notification.EXTRA_TITLE))) {
1117
+ throw new ForegroundServiceStartNotAllowedException("real old START denied");
1118
+ }
1119
+ super.startForeground(id, notification);
1120
+ }
1121
+ }
1122
+
1123
+ @Implements(Service.class)
1124
+ public static class RefuseStopSelfShadowService extends ShadowService {
1125
+ @Implementation
1126
+ protected boolean stopSelfResult(int startId) {
1127
+ return false;
1128
+ }
1129
+ }
1130
+
1131
+ @Implements(Service.class)
1132
+ public static class FailStopForegroundShadowService extends ShadowService {
1133
+ @Implementation
1134
+ protected void stopForeground(int flags) {
1135
+ throw new ForegroundServiceStartNotAllowedException("not from startForeground");
1136
+ }
1137
+ }
1138
+
1139
+ private static void assertCurrentState(String id, int hash, String title) throws Exception {
1140
+ assertEquals(id, ForegroundService.mCurrentNotificationId);
1141
+ assertEquals(hash, getPrivateStatic("mCurrentHashCode"));
1142
+ assertEquals(
1143
+ title, ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
1144
+ assertEquals(
1145
+ title,
1146
+ ((Notification) getPrivateStatic("mCurrentNotification"))
1147
+ .extras.getString(Notification.EXTRA_TITLE));
1148
+ }
1149
+
1150
+ private static void idleMain() {
1151
+ org.robolectric.Shadows.shadowOf(Looper.getMainLooper()).idle();
1152
+ }
1153
+
1154
+ private RecordingServiceContext useRecordingContext() {
1155
+ RecordingServiceContext context =
1156
+ new RecordingServiceContext(RuntimeEnvironment.getApplication());
1157
+ ContextHolder.setApplicationContext(context);
1158
+ return context;
1159
+ }
1160
+
1161
+ private ServiceController<ForegroundService> buildService() {
1162
+ ServiceController<ForegroundService> controller =
1163
+ Robolectric.buildService(ForegroundService.class);
1164
+ controllers.add(controller);
1165
+ return controller;
1166
+ }
1167
+
1168
+ private static class RecordingServiceContext extends ContextWrapper {
1169
+ final List<Intent> starts = new ArrayList<>();
1170
+ int stops;
1171
+ RuntimeException startFailure;
1172
+
1173
+ RecordingServiceContext(Context base) {
1174
+ super(base);
1175
+ }
1176
+
1177
+ @Override
1178
+ public ComponentName startService(Intent intent) {
1179
+ starts.add(new Intent(intent));
1180
+ return intent.getComponent();
1181
+ }
1182
+
1183
+ @Override
1184
+ public ComponentName startForegroundService(Intent intent) {
1185
+ starts.add(new Intent(intent));
1186
+ if (startFailure != null) {
1187
+ throw startFailure;
1188
+ }
1189
+ return intent.getComponent();
1190
+ }
1191
+
1192
+ @Override
1193
+ public boolean stopService(Intent intent) {
1194
+ stops++;
1195
+ return true;
1196
+ }
1197
+ }
1198
+
1199
+ @Implements(Service.class)
1200
+ public static class DefensiveFailureShadowService extends ShadowService {
1201
+ static RuntimeException failure;
1202
+ static int attempts;
1203
+ static int lastId;
1204
+ static int lastType;
1205
+
1206
+ @Implementation
1207
+ protected void startForeground(int id, Notification notification) {
1208
+ attempts++;
1209
+ lastId = id;
1210
+ lastType = ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE;
1211
+ throw failure;
1212
+ }
1213
+
1214
+ @Implementation
1215
+ protected void startForeground(int id, Notification notification, int foregroundServiceType) {
1216
+ attempts++;
1217
+ lastId = id;
1218
+ lastType = foregroundServiceType;
1219
+ throw failure;
1220
+ }
1221
+ }
1222
+
1223
+ @Implements(NotificationManager.class)
1224
+ public static class SecurityDefensiveChannelShadowManager extends ShadowNotificationManager {
1225
+ @Implementation
1226
+ protected void createNotificationChannel(NotificationChannel channel) {
1227
+ if ("notifee_fg_default".equals(channel.getId())) {
1228
+ throw new SecurityException("defensive channel failure");
1229
+ }
1230
+ super.createNotificationChannel(channel);
1231
+ }
1232
+ }
1233
+
1234
+ @Implements(Service.class)
1235
+ public static class SecurityStopForegroundShadowService extends ShadowService {
1236
+ @Implementation
1237
+ protected void stopForeground(int flags) {
1238
+ throw new SecurityException("defensive teardown failure");
1239
+ }
59
1240
  }
60
1241
 
61
1242
  /**
@@ -65,8 +1246,7 @@ public class ForegroundServiceTest {
65
1246
  */
66
1247
  @Test
67
1248
  public void onStartCommand_stopIntentBeforeStart_doesNotCrash() {
68
- ServiceController<ForegroundService> controller =
69
- Robolectric.buildService(ForegroundService.class);
1249
+ ServiceController<ForegroundService> controller = buildService();
70
1250
  ForegroundService service = controller.create().get();
71
1251
 
72
1252
  Intent stopIntent = new Intent();
@@ -83,8 +1263,7 @@ public class ForegroundServiceTest {
83
1263
  */
84
1264
  @Test
85
1265
  public void onStartCommand_nullIntent_doesNotCrash() {
86
- ServiceController<ForegroundService> controller =
87
- Robolectric.buildService(ForegroundService.class);
1266
+ ServiceController<ForegroundService> controller = buildService();
88
1267
  ForegroundService service = controller.create().get();
89
1268
 
90
1269
  // Null intent simulates service recreation after process kill
@@ -100,8 +1279,7 @@ public class ForegroundServiceTest {
100
1279
  */
101
1280
  @Test
102
1281
  public void onStartCommand_stopIntent_resetsPublicStaticState() {
103
- ServiceController<ForegroundService> controller =
104
- Robolectric.buildService(ForegroundService.class);
1282
+ ServiceController<ForegroundService> controller = buildService();
105
1283
  ForegroundService service = controller.create().get();
106
1284
 
107
1285
  // Set some stale state to simulate a prior invocation
@@ -129,8 +1307,7 @@ public class ForegroundServiceTest {
129
1307
  public void onStartCommand_stopIntentApi34NoManifestType_throwsRuntimeException() {
130
1308
  // Robolectric's default shadow PackageManager returns FOREGROUND_SERVICE_TYPE_NONE (0)
131
1309
  // for services without an explicit foregroundServiceType in the test manifest.
132
- ServiceController<ForegroundService> controller =
133
- Robolectric.buildService(ForegroundService.class);
1310
+ ServiceController<ForegroundService> controller = buildService();
134
1311
  ForegroundService service = controller.create().get();
135
1312
 
136
1313
  Intent stopIntent = new Intent();
@@ -145,8 +1322,7 @@ public class ForegroundServiceTest {
145
1322
  @Test
146
1323
  @Config(sdk = 34)
147
1324
  public void onStartCommand_stopIntentApi34NoManifestType_messageContainsDocUrl() {
148
- ServiceController<ForegroundService> controller =
149
- Robolectric.buildService(ForegroundService.class);
1325
+ ServiceController<ForegroundService> controller = buildService();
150
1326
  ForegroundService service = controller.create().get();
151
1327
 
152
1328
  Intent stopIntent = new Intent();
@@ -168,8 +1344,7 @@ public class ForegroundServiceTest {
168
1344
  @Test
169
1345
  @Config(sdk = 33)
170
1346
  public void onStartCommand_stopIntentApiBelow34_defensivePathRunsNormally() {
171
- ServiceController<ForegroundService> controller =
172
- Robolectric.buildService(ForegroundService.class);
1347
+ ServiceController<ForegroundService> controller = buildService();
173
1348
  ForegroundService service = controller.create().get();
174
1349
 
175
1350
  Intent stopIntent = new Intent();
@@ -189,8 +1364,7 @@ public class ForegroundServiceTest {
189
1364
  @Test
190
1365
  @Config(sdk = 33)
191
1366
  public void onStartCommand_stopIntentAfterSuccessfulStart_skipsDefensivePath() {
192
- ServiceController<ForegroundService> controller =
193
- Robolectric.buildService(ForegroundService.class);
1367
+ ServiceController<ForegroundService> controller = buildService();
194
1368
  ForegroundService service = controller.create().get();
195
1369
 
196
1370
  Intent stopIntent = new Intent();
@@ -211,8 +1385,7 @@ public class ForegroundServiceTest {
211
1385
  declareForegroundServiceTypes(
212
1386
  ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
213
1387
  | ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
214
- ServiceController<ForegroundService> controller =
215
- Robolectric.buildService(ForegroundService.class);
1388
+ ServiceController<ForegroundService> controller = buildService();
216
1389
  ForegroundService service = controller.create().get();
217
1390
 
218
1391
  int result = service.onStartCommand(buildStopIntent(), 0, 1);
@@ -228,8 +1401,7 @@ public class ForegroundServiceTest {
228
1401
  throws Exception {
229
1402
  declareForegroundServiceTypes(
230
1403
  ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA | ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
231
- ServiceController<ForegroundService> controller =
232
- Robolectric.buildService(ForegroundService.class);
1404
+ ServiceController<ForegroundService> controller = buildService();
233
1405
  ForegroundService service = controller.create().get();
234
1406
 
235
1407
  int result = service.onStartCommand(buildStopIntent(), 0, 1);
@@ -247,8 +1419,7 @@ public class ForegroundServiceTest {
247
1419
  declareForegroundServiceTypes(
248
1420
  ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
249
1421
  | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
250
- ServiceController<ForegroundService> controller =
251
- Robolectric.buildService(ForegroundService.class);
1422
+ ServiceController<ForegroundService> controller = buildService();
252
1423
  ForegroundService service = controller.create().get();
253
1424
 
254
1425
  int result = service.onStartCommand(buildStopIntent(), 0, 1);
@@ -263,8 +1434,7 @@ public class ForegroundServiceTest {
263
1434
  public void onStartCommand_stopIntentApi34MicrophoneOnly_doesNotUseUndeclaredShortService()
264
1435
  throws Exception {
265
1436
  declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
266
- ServiceController<ForegroundService> controller =
267
- Robolectric.buildService(ForegroundService.class);
1437
+ ServiceController<ForegroundService> controller = buildService();
268
1438
  ForegroundService service = controller.create().get();
269
1439
 
270
1440
  int result = service.onStartCommand(buildStopIntent(), 0, 1);
@@ -278,8 +1448,7 @@ public class ForegroundServiceTest {
278
1448
  @Config(sdk = 34, shadows = ThrowingStartForegroundShadowService.class)
279
1449
  public void onStartCommand_stopIntentApi34SecurityException_doesNotCrash() throws Exception {
280
1450
  declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
281
- ServiceController<ForegroundService> controller =
282
- Robolectric.buildService(ForegroundService.class);
1451
+ ServiceController<ForegroundService> controller = buildService();
283
1452
  ForegroundService service = controller.create().get();
284
1453
 
285
1454
  int result = service.onStartCommand(buildStopIntent(), 0, 1);
@@ -305,8 +1474,7 @@ public class ForegroundServiceTest {
305
1474
  public void onStartCommand_startIntent_setsCurrentNotificationIdAndCallsStartForeground()
306
1475
  throws Exception {
307
1476
  createChannel();
308
- ServiceController<ForegroundService> controller =
309
- Robolectric.buildService(ForegroundService.class);
1477
+ ServiceController<ForegroundService> controller = buildService();
310
1478
  ForegroundService service = controller.create().get();
311
1479
 
312
1480
  String id = "fgs-start-happy";
@@ -328,6 +1496,473 @@ public class ForegroundServiceTest {
328
1496
  assertEquals(id.hashCode(), hashField.getInt(null));
329
1497
  }
330
1498
 
1499
+ @Test
1500
+ @Config(sdk = 28)
1501
+ public void onStartCommand_sameIdUpdate_refreshesRegisteredNotificationAndCachedState()
1502
+ throws Exception {
1503
+ createChannel();
1504
+ ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
1505
+ String id = "live";
1506
+ FgsRunnerCapture runnerCapture = new FgsRunnerCapture();
1507
+ EventBus.register(runnerCapture);
1508
+ try {
1509
+ service.onStartCommand(buildStartIntent(id, id.hashCode(), "A"), 0, 1);
1510
+ service.onStartCommand(buildStartIntent(id, id.hashCode(), "B"), 0, 2);
1511
+
1512
+ Notification registered =
1513
+ org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification();
1514
+ NotificationManager manager =
1515
+ (NotificationManager)
1516
+ RuntimeEnvironment.getApplication().getSystemService(Context.NOTIFICATION_SERVICE);
1517
+ Notification visible =
1518
+ org.robolectric.Shadows.shadowOf(manager).getNotification(id.hashCode());
1519
+ assertEquals("B", visible.extras.getString(Notification.EXTRA_TITLE));
1520
+ assertEquals(1, org.robolectric.Shadows.shadowOf(manager).size());
1521
+ assertEquals("B", registered.extras.getString(Notification.EXTRA_TITLE));
1522
+ assertTrue((registered.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0);
1523
+ assertEquals(
1524
+ id.hashCode(),
1525
+ org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
1526
+ assertEquals(
1527
+ "B",
1528
+ ((Notification) getPrivateStatic("mCurrentNotification"))
1529
+ .extras.getString(Notification.EXTRA_TITLE));
1530
+ assertEquals(
1531
+ "B", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
1532
+ assertEquals(id, ForegroundService.mCurrentNotificationId);
1533
+ assertEquals(id.hashCode(), getPrivateStatic("mCurrentHashCode"));
1534
+ assertEquals(1, runnerCapture.events.size());
1535
+ } finally {
1536
+ EventBus.unregister(runnerCapture);
1537
+ }
1538
+ }
1539
+
1540
+ @Test
1541
+ @Config(sdk = 29)
1542
+ public void onStartCommand_sameIdUpdateApi29_preservesExplicitNoneTypeAndRegistration()
1543
+ throws Exception {
1544
+ createChannel();
1545
+ ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
1546
+ String id = "live";
1547
+
1548
+ service.onStartCommand(
1549
+ buildStartIntent(id, id.hashCode(), "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE), 0, 1);
1550
+ service.onStartCommand(
1551
+ buildStartIntent(id, id.hashCode(), "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE), 0, 2);
1552
+
1553
+ assertEquals(
1554
+ "B",
1555
+ org.robolectric.Shadows.shadowOf(service)
1556
+ .getLastForegroundNotification()
1557
+ .extras
1558
+ .getString(Notification.EXTRA_TITLE));
1559
+ assertEquals(ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE, getLastForegroundServiceType(service));
1560
+ assertEquals(
1561
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE, ForegroundService.mCurrentForegroundServiceType);
1562
+ assertEquals("B", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
1563
+ }
1564
+
1565
+ @Test
1566
+ @Config(sdk = 30)
1567
+ public void onStartCommand_sameIdUpdateApi30_keepsTypedForegroundService() throws Exception {
1568
+ createChannel();
1569
+ ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
1570
+ String id = "live";
1571
+ service.onStartCommand(
1572
+ buildStartIntent(id, id.hashCode(), "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
1573
+ 0,
1574
+ 1);
1575
+ service.onStartCommand(
1576
+ buildStartIntent(id, id.hashCode(), "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
1577
+ 0,
1578
+ 2);
1579
+
1580
+ assertEquals(
1581
+ "B",
1582
+ org.robolectric.Shadows.shadowOf(service)
1583
+ .getLastForegroundNotification()
1584
+ .extras
1585
+ .getString(Notification.EXTRA_TITLE));
1586
+ assertEquals(
1587
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC, getLastForegroundServiceType(service));
1588
+ assertEquals(
1589
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
1590
+ ForegroundService.mCurrentForegroundServiceType);
1591
+ }
1592
+
1593
+ @Test
1594
+ @Config(sdk = 29)
1595
+ public void onStartCommand_sameIdUpdateApi29_preservesManifestFallbackType() throws Exception {
1596
+ createChannel();
1597
+ ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
1598
+ String id = "live";
1599
+ service.onStartCommand(buildStartIntent(id, id.hashCode(), "A"), 0, 1);
1600
+ service.onStartCommand(buildStartIntent(id, id.hashCode(), "B"), 0, 2);
1601
+
1602
+ assertEquals(
1603
+ "B",
1604
+ org.robolectric.Shadows.shadowOf(service)
1605
+ .getLastForegroundNotification()
1606
+ .extras
1607
+ .getString(Notification.EXTRA_TITLE));
1608
+ assertEquals(
1609
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST, getLastForegroundServiceType(service));
1610
+ assertEquals(
1611
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST,
1612
+ ForegroundService.mCurrentForegroundServiceType);
1613
+ }
1614
+
1615
+ @Test
1616
+ @Config(sdk = 34)
1617
+ public void onStartCommand_sameIdTypeChange_usesLatestTypedNotificationAndCache()
1618
+ throws Exception {
1619
+ createChannel();
1620
+ declareForegroundServiceTypes(
1621
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
1622
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
1623
+ ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
1624
+ String id = "live";
1625
+
1626
+ service.onStartCommand(
1627
+ buildStartIntent(id, id.hashCode(), "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
1628
+ 0,
1629
+ 1);
1630
+ service.onStartCommand(
1631
+ buildStartIntent(
1632
+ id, id.hashCode(), "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK),
1633
+ 0,
1634
+ 2);
1635
+
1636
+ assertEquals(
1637
+ "B",
1638
+ org.robolectric.Shadows.shadowOf(service)
1639
+ .getLastForegroundNotification()
1640
+ .extras
1641
+ .getString(Notification.EXTRA_TITLE));
1642
+ assertEquals(
1643
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK, getLastForegroundServiceType(service));
1644
+ assertEquals(
1645
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK,
1646
+ ForegroundService.mCurrentForegroundServiceType);
1647
+ assertEquals(
1648
+ "B",
1649
+ ((Notification) getPrivateStatic("mCurrentNotification"))
1650
+ .extras.getString(Notification.EXTRA_TITLE));
1651
+ assertEquals("B", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
1652
+ }
1653
+
1654
+ @Test
1655
+ @Config(sdk = 33)
1656
+ public void receiverDeleteAfterSameIdUpdate_repostsLatestNotificationWithoutDismissEvent()
1657
+ throws Exception {
1658
+ createChannel();
1659
+ ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
1660
+ String id = "live";
1661
+ service.onStartCommand(buildStartIntent(id, id.hashCode(), "A"), 0, 1);
1662
+ service.onStartCommand(buildStartIntent(id, id.hashCode(), "B"), 0, 2);
1663
+
1664
+ NotificationManager manager =
1665
+ (NotificationManager)
1666
+ RuntimeEnvironment.getApplication().getSystemService(Context.NOTIFICATION_SERVICE);
1667
+ manager.cancel(id.hashCode());
1668
+ ReceiverService receiver = Robolectric.buildService(ReceiverService.class).create().get();
1669
+ Intent delete = new Intent();
1670
+ delete.setAction(ReceiverService.DELETE_INTENT);
1671
+ delete.putExtra("notification", buildNotificationBundle(id));
1672
+ FgsEventCapture capture = new FgsEventCapture();
1673
+ EventBus.register(capture);
1674
+ try {
1675
+ receiver.onStartCommand(delete, 0, 3);
1676
+ Notification reposted =
1677
+ org.robolectric.Shadows.shadowOf(manager).getNotification(id.hashCode());
1678
+ assertNotNull(reposted);
1679
+ assertEquals("B", reposted.extras.getString(Notification.EXTRA_TITLE));
1680
+ assertEquals(1, org.robolectric.Shadows.shadowOf(manager).size());
1681
+ assertEquals(0, capture.events.size());
1682
+ } finally {
1683
+ EventBus.unregister(capture);
1684
+ }
1685
+ }
1686
+
1687
+ @Test
1688
+ @Config(sdk = 34)
1689
+ public void onTimeout_afterShortServiceSameIdUpdate_emitsLatestBundleWithoutRestart()
1690
+ throws Exception {
1691
+ createChannel();
1692
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
1693
+ ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
1694
+ String id = "live";
1695
+ service.onStartCommand(
1696
+ buildStartIntent(id, id.hashCode(), "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
1697
+ 0,
1698
+ 1);
1699
+ service.onStartCommand(
1700
+ buildStartIntent(id, id.hashCode(), "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
1701
+ 0,
1702
+ 2);
1703
+
1704
+ NotificationManager manager =
1705
+ (NotificationManager)
1706
+ RuntimeEnvironment.getApplication().getSystemService(Context.NOTIFICATION_SERVICE);
1707
+ assertEquals(
1708
+ "B",
1709
+ org.robolectric.Shadows.shadowOf(manager)
1710
+ .getNotification(id.hashCode())
1711
+ .extras
1712
+ .getString(Notification.EXTRA_TITLE));
1713
+ // Robolectric does not model Android 12+ notify-to-FGS synchronization. Its service shadow
1714
+ // remains at A here, which also guards against resetting shortService's timer via a repeat
1715
+ // startForeground call.
1716
+ assertEquals(
1717
+ "A",
1718
+ org.robolectric.Shadows.shadowOf(service)
1719
+ .getLastForegroundNotification()
1720
+ .extras
1721
+ .getString(Notification.EXTRA_TITLE));
1722
+ FgsEventCapture capture = new FgsEventCapture();
1723
+ EventBus.register(capture);
1724
+ try {
1725
+ service.onTimeout(42);
1726
+ assertEquals(1, capture.events.size());
1727
+ assertEquals(NotificationEvent.TYPE_FG_TIMEOUT, capture.events.get(0).getType());
1728
+ assertEquals("B", capture.events.get(0).getNotification().getTitle());
1729
+ } finally {
1730
+ EventBus.unregister(capture);
1731
+ }
1732
+ }
1733
+
1734
+ @Test
1735
+ @Config(sdk = 33)
1736
+ public void onStartCommand_differentId_emitsAlreadyExistsAndKeepsActiveState() throws Exception {
1737
+ createChannel();
1738
+ ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
1739
+ String id = "live";
1740
+ service.onStartCommand(buildStartIntent(id, id.hashCode(), "A"), 0, 1);
1741
+ FgsEventCapture capture = new FgsEventCapture();
1742
+ EventBus.register(capture);
1743
+ try {
1744
+ service.onStartCommand(buildStartIntent("other", "other".hashCode(), "B"), 0, 2);
1745
+ assertEquals(1, capture.events.size());
1746
+ assertEquals(NotificationEvent.TYPE_FG_ALREADY_EXIST, capture.events.get(0).getType());
1747
+ assertEquals(id, ForegroundService.mCurrentNotificationId);
1748
+ assertEquals(
1749
+ "A", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
1750
+ NotificationManager manager =
1751
+ (NotificationManager)
1752
+ RuntimeEnvironment.getApplication().getSystemService(Context.NOTIFICATION_SERVICE);
1753
+ assertEquals(1, org.robolectric.Shadows.shadowOf(manager).size());
1754
+ } finally {
1755
+ EventBus.unregister(capture);
1756
+ }
1757
+ }
1758
+
1759
+ @Test
1760
+ @Config(sdk = 28, shadows = RejectBStartForegroundShadowService.class)
1761
+ public void onStartCommand_sameTypeStartForegroundFailure_keepsPreviousCache() throws Exception {
1762
+ createChannel();
1763
+ ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
1764
+ String id = "live";
1765
+ service.onStartCommand(buildStartIntent(id, id.hashCode(), "A"), 0, 1);
1766
+
1767
+ SecurityException failure =
1768
+ assertThrows(
1769
+ SecurityException.class,
1770
+ () -> service.onStartCommand(buildStartIntent(id, id.hashCode(), "B"), 0, 2));
1771
+ assertEquals("posting B rejected", failure.getMessage());
1772
+
1773
+ assertEquals(id, ForegroundService.mCurrentNotificationId);
1774
+ assertEquals(id.hashCode(), getPrivateStatic("mCurrentHashCode"));
1775
+ assertEquals("A", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
1776
+ assertEquals(
1777
+ "A",
1778
+ ((Notification) getPrivateStatic("mCurrentNotification"))
1779
+ .extras.getString(Notification.EXTRA_TITLE));
1780
+ }
1781
+
1782
+ @Test
1783
+ @Config(sdk = 28, shadows = RejectBStartForegroundShadowService.class)
1784
+ public void onStartCommand_initialStartForegroundFailure_doesNotClaimActiveService()
1785
+ throws Exception {
1786
+ createChannel();
1787
+ ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
1788
+ String id = "live";
1789
+ FgsRunnerCapture runnerCapture = new FgsRunnerCapture();
1790
+ EventBus.register(runnerCapture);
1791
+ try {
1792
+ SecurityException failure =
1793
+ assertThrows(
1794
+ SecurityException.class,
1795
+ () -> service.onStartCommand(buildStartIntent(id, id.hashCode(), "B"), 0, 1));
1796
+ assertEquals("posting B rejected", failure.getMessage());
1797
+ assertNull(ForegroundService.mCurrentNotificationId);
1798
+ assertNull(getPrivateStatic("mCurrentNotification"));
1799
+ assertNull(getPrivateStatic("mCurrentNotificationBundle"));
1800
+ assertEquals(0, getPrivateStatic("mCurrentHashCode"));
1801
+ assertEquals(0, runnerCapture.events.size());
1802
+
1803
+ service.onStartCommand(buildStartIntent(id, id.hashCode(), "A"), 0, 2);
1804
+ assertEquals(1, runnerCapture.events.size());
1805
+ assertEquals(
1806
+ "A", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
1807
+ } finally {
1808
+ EventBus.unregister(runnerCapture);
1809
+ }
1810
+ }
1811
+
1812
+ @Test
1813
+ @Config(sdk = 34, shadows = RejectBStartForegroundShadowService.class)
1814
+ public void onStartCommand_typeChangeStartForegroundFailure_keepsPreviousTypeAndCache()
1815
+ throws Exception {
1816
+ createChannel();
1817
+ declareForegroundServiceTypes(
1818
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
1819
+ | ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
1820
+ ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
1821
+ String id = "live";
1822
+ service.onStartCommand(
1823
+ buildStartIntent(id, id.hashCode(), "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC),
1824
+ 0,
1825
+ 1);
1826
+
1827
+ SecurityException failure =
1828
+ assertThrows(
1829
+ SecurityException.class,
1830
+ () ->
1831
+ service.onStartCommand(
1832
+ buildStartIntent(
1833
+ id, id.hashCode(), "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK),
1834
+ 0,
1835
+ 2));
1836
+ assertEquals("posting B rejected", failure.getMessage());
1837
+
1838
+ assertEquals(
1839
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
1840
+ ForegroundService.mCurrentForegroundServiceType);
1841
+ assertEquals("A", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
1842
+ assertEquals(
1843
+ "A",
1844
+ ((Notification) getPrivateStatic("mCurrentNotification"))
1845
+ .extras.getString(Notification.EXTRA_TITLE));
1846
+ }
1847
+
1848
+ @Test
1849
+ @Config(sdk = 33, shadows = RejectBNotificationShadowManager.class)
1850
+ public void onStartCommand_notifyFailure_keepsPreviousCache() throws Exception {
1851
+ createChannel();
1852
+ ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
1853
+ String id = "live";
1854
+ service.onStartCommand(buildStartIntent(id, id.hashCode(), "A"), 0, 1);
1855
+
1856
+ SecurityException failure =
1857
+ assertThrows(
1858
+ SecurityException.class,
1859
+ () -> service.onStartCommand(buildStartIntent(id, id.hashCode(), "B"), 0, 2));
1860
+ assertEquals("posting B rejected", failure.getMessage());
1861
+
1862
+ assertEquals(id, ForegroundService.mCurrentNotificationId);
1863
+ assertEquals(id.hashCode(), getPrivateStatic("mCurrentHashCode"));
1864
+ assertEquals("A", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
1865
+ assertEquals(
1866
+ "A",
1867
+ ((Notification) getPrivateStatic("mCurrentNotification"))
1868
+ .extras.getString(Notification.EXTRA_TITLE));
1869
+ }
1870
+
1871
+ @Test
1872
+ @Config(sdk = 34, shadows = SilentlyRejectMissingChannelShadowManager.class)
1873
+ public void onStartCommand_notifyReturnsNormallyWithoutPosting_keepsLatestSubmittedState()
1874
+ throws Exception {
1875
+ createChannel();
1876
+ declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
1877
+ ForegroundService service = Robolectric.buildService(ForegroundService.class).create().get();
1878
+ String id = "live";
1879
+ service.onStartCommand(
1880
+ buildStartIntent(id, id.hashCode(), "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
1881
+ 0,
1882
+ 1);
1883
+
1884
+ Intent update =
1885
+ buildStartIntent(id, id.hashCode(), "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
1886
+ String missingChannelId = "missing-channel";
1887
+ Notification rejected =
1888
+ new NotificationCompat.Builder(RuntimeEnvironment.getApplication(), missingChannelId)
1889
+ .setSmallIcon(android.R.drawable.ic_dialog_info)
1890
+ .setContentTitle("B")
1891
+ .setOnlyAlertOnce(true)
1892
+ .build();
1893
+ update.putExtra("notification", rejected);
1894
+ update
1895
+ .getBundleExtra("notificationBundle")
1896
+ .getBundle("android")
1897
+ .putString("channelId", missingChannelId);
1898
+
1899
+ NotificationManager manager =
1900
+ (NotificationManager)
1901
+ RuntimeEnvironment.getApplication().getSystemService(Context.NOTIFICATION_SERVICE);
1902
+ assertNull(manager.getNotificationChannel(missingChannelId));
1903
+ SilentlyRejectMissingChannelShadowManager.rejectedPosts = 0;
1904
+ SilentlyRejectMissingChannelShadowManager.lastRejectedNotification = null;
1905
+ service.onStartCommand(update, 0, 2);
1906
+
1907
+ // This shadow models the platform's normal-return, no-post outcome. Robolectric does not
1908
+ // reproduce the separate Android foreground-service record update in this sequence.
1909
+ assertEquals(1, SilentlyRejectMissingChannelShadowManager.rejectedPosts);
1910
+ assertEquals(
1911
+ "B",
1912
+ SilentlyRejectMissingChannelShadowManager.lastRejectedNotification.extras.getString(
1913
+ Notification.EXTRA_TITLE));
1914
+ assertEquals(
1915
+ "A",
1916
+ org.robolectric.Shadows.shadowOf(manager)
1917
+ .getNotification(id.hashCode())
1918
+ .extras
1919
+ .getString(Notification.EXTRA_TITLE));
1920
+ // The simulated visible card remains A while NotifyKit's latest submitted state is B.
1921
+ assertEquals("B", ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
1922
+ assertEquals(
1923
+ "B",
1924
+ ((Notification) getPrivateStatic("mCurrentNotification"))
1925
+ .extras.getString(Notification.EXTRA_TITLE));
1926
+ assertEquals(
1927
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
1928
+ ForegroundService.mCurrentForegroundServiceType);
1929
+ assertEquals(id, ForegroundService.mCurrentNotificationId);
1930
+ assertEquals(id.hashCode(), getPrivateStatic("mCurrentHashCode"));
1931
+ // The service shadow remains at A because the unchanged-type path did not call
1932
+ // startForeground() again, which could extend a shortService timeout on Android.
1933
+ assertEquals(
1934
+ "A",
1935
+ org.robolectric.Shadows.shadowOf(service)
1936
+ .getLastForegroundNotification()
1937
+ .extras
1938
+ .getString(Notification.EXTRA_TITLE));
1939
+
1940
+ manager.cancel(id.hashCode());
1941
+ ReceiverService receiver = Robolectric.buildService(ReceiverService.class).create().get();
1942
+ Intent delete = new Intent();
1943
+ delete.setAction(ReceiverService.DELETE_INTENT);
1944
+ delete.putExtra("notification", buildNotificationBundle(id));
1945
+ FgsEventCapture capture = new FgsEventCapture();
1946
+ EventBus.register(capture);
1947
+ try {
1948
+ receiver.onStartCommand(delete, 0, 3);
1949
+ assertEquals(2, SilentlyRejectMissingChannelShadowManager.rejectedPosts);
1950
+ assertEquals(
1951
+ "B",
1952
+ SilentlyRejectMissingChannelShadowManager.lastRejectedNotification.extras.getString(
1953
+ Notification.EXTRA_TITLE));
1954
+ assertNull(org.robolectric.Shadows.shadowOf(manager).getNotification(id.hashCode()));
1955
+ assertEquals(0, capture.events.size());
1956
+
1957
+ service.onTimeout(42);
1958
+ assertEquals(1, capture.events.size());
1959
+ assertEquals(NotificationEvent.TYPE_FG_TIMEOUT, capture.events.get(0).getType());
1960
+ assertEquals("B", capture.events.get(0).getNotification().getTitle());
1961
+ } finally {
1962
+ EventBus.unregister(capture);
1963
+ }
1964
+ }
1965
+
331
1966
  /**
332
1967
  * Regression guard for the 9.1.13 {@code onTimeout(int)} fix (upstream invertase/notifee#703). On
333
1968
  * API 34, Android's single-argument {@code onTimeout} fires when a {@code shortService} FGS
@@ -344,8 +1979,7 @@ public class ForegroundServiceTest {
344
1979
  @Test
345
1980
  @Config(sdk = 34)
346
1981
  public void onTimeout_api34_emitsFgTimeoutEventWithStartIdAndSentinelFgsType() throws Exception {
347
- ServiceController<ForegroundService> controller =
348
- Robolectric.buildService(ForegroundService.class);
1982
+ ServiceController<ForegroundService> controller = buildService();
349
1983
  ForegroundService service = controller.create().get();
350
1984
 
351
1985
  String id = "fgs-timeout-api34";
@@ -387,8 +2021,7 @@ public class ForegroundServiceTest {
387
2021
  @Test
388
2022
  @Config(sdk = 35)
389
2023
  public void onTimeout_api35_emitsFgTimeoutEventWithExplicitFgsType() throws Exception {
390
- ServiceController<ForegroundService> controller =
391
- Robolectric.buildService(ForegroundService.class);
2024
+ ServiceController<ForegroundService> controller = buildService();
392
2025
  ForegroundService service = controller.create().get();
393
2026
 
394
2027
  String id = "fgs-timeout-api35";
@@ -424,8 +2057,7 @@ public class ForegroundServiceTest {
424
2057
  @Test
425
2058
  @Config(sdk = 34)
426
2059
  public void onTimeout_withNoActiveState_doesNotCrashOrEmitEvent() {
427
- ServiceController<ForegroundService> controller =
428
- Robolectric.buildService(ForegroundService.class);
2060
+ ServiceController<ForegroundService> controller = buildService();
429
2061
  ForegroundService service = controller.create().get();
430
2062
 
431
2063
  FgsEventCapture capture = new FgsEventCapture();
@@ -473,11 +2105,35 @@ public class ForegroundServiceTest {
473
2105
  }
474
2106
 
475
2107
  private static Intent buildStartIntent(String id, int hashCode) {
2108
+ return buildStartIntent(id, hashCode, "FGS test");
2109
+ }
2110
+
2111
+ private static Intent buildStartIntent(String id, int hashCode, String title) {
476
2112
  Intent intent = new Intent();
477
2113
  intent.setAction(ForegroundService.START_FOREGROUND_SERVICE_ACTION);
478
2114
  intent.putExtra("hashCode", hashCode);
479
- intent.putExtra("notification", buildNotification());
480
- intent.putExtra("notificationBundle", buildNotificationBundle(id));
2115
+ Notification notification =
2116
+ new NotificationCompat.Builder(RuntimeEnvironment.getApplication(), TEST_CHANNEL_ID)
2117
+ .setSmallIcon(android.R.drawable.ic_dialog_info)
2118
+ .setContentTitle(title)
2119
+ .setOnlyAlertOnce(true)
2120
+ .build();
2121
+ intent.putExtra("notification", notification);
2122
+ Bundle bundle = buildNotificationBundle(id);
2123
+ bundle.putString("title", title);
2124
+ intent.putExtra("notificationBundle", bundle);
2125
+ return intent;
2126
+ }
2127
+
2128
+ private static Intent buildStartIntent(
2129
+ String id, int hashCode, String title, int foregroundServiceType) {
2130
+ Intent intent = buildStartIntent(id, hashCode, title);
2131
+ Bundle bundle = intent.getBundleExtra("notificationBundle");
2132
+ Bundle android = bundle.getBundle("android");
2133
+ ArrayList<Integer> types = new ArrayList<>();
2134
+ types.add(foregroundServiceType);
2135
+ android.putIntegerArrayList("foregroundServiceTypes", types);
2136
+ intent.putExtra("notificationBundle", bundle);
481
2137
  return intent;
482
2138
  }
483
2139
 
@@ -538,6 +2194,67 @@ public class ForegroundServiceTest {
538
2194
  field.set(null, value);
539
2195
  }
540
2196
 
2197
+ private static Object getPrivateStatic(String name) throws Exception {
2198
+ Field field = ForegroundService.class.getDeclaredField(name);
2199
+ field.setAccessible(true);
2200
+ return field.get(null);
2201
+ }
2202
+
2203
+ public static class FgsRunnerCapture {
2204
+ final List<ForegroundServiceEvent> events = new ArrayList<>();
2205
+
2206
+ @Subscribe(threadMode = ThreadMode.POSTING)
2207
+ public void onForegroundServiceEvent(ForegroundServiceEvent event) {
2208
+ events.add(event);
2209
+ }
2210
+ }
2211
+
2212
+ @Implements(Service.class)
2213
+ public static class RejectBStartForegroundShadowService extends ShadowService {
2214
+ @Implementation
2215
+ protected void startForeground(int id, Notification notification) {
2216
+ if ("B".equals(notification.extras.getString(Notification.EXTRA_TITLE))) {
2217
+ throw new SecurityException("posting B rejected");
2218
+ }
2219
+ super.startForeground(id, notification);
2220
+ }
2221
+
2222
+ @Implementation
2223
+ protected void startForeground(int id, Notification notification, int foregroundServiceType) {
2224
+ if ("B".equals(notification.extras.getString(Notification.EXTRA_TITLE))) {
2225
+ throw new SecurityException("posting B rejected");
2226
+ }
2227
+ super.startForeground(id, notification, foregroundServiceType);
2228
+ }
2229
+ }
2230
+
2231
+ @Implements(NotificationManager.class)
2232
+ public static class RejectBNotificationShadowManager extends ShadowNotificationManager {
2233
+ @Implementation
2234
+ protected void notify(String tag, int id, Notification notification) {
2235
+ if ("B".equals(notification.extras.getString(Notification.EXTRA_TITLE))) {
2236
+ throw new SecurityException("posting B rejected");
2237
+ }
2238
+ super.notify(tag, id, notification);
2239
+ }
2240
+ }
2241
+
2242
+ @Implements(NotificationManager.class)
2243
+ public static class SilentlyRejectMissingChannelShadowManager extends ShadowNotificationManager {
2244
+ static int rejectedPosts;
2245
+ static Notification lastRejectedNotification;
2246
+
2247
+ @Implementation
2248
+ protected void notify(String tag, int id, Notification notification) {
2249
+ if ("missing-channel".equals(notification.getChannelId())) {
2250
+ rejectedPosts++;
2251
+ lastRejectedNotification = notification;
2252
+ return;
2253
+ }
2254
+ super.notify(tag, id, notification);
2255
+ }
2256
+ }
2257
+
541
2258
  /**
542
2259
  * greenrobot EventBus subscriber that records every {@link NotificationEvent} posted during a
543
2260
  * test. {@link ThreadMode#POSTING} fires the subscriber synchronously on the caller thread, so