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.
- package/README.md +2 -0
- package/android/src/main/java/app/notifee/core/ForegroundService.java +614 -60
- package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +3080 -18
- package/dist/types/Notification.d.ts +25 -1
- package/dist/types/Notification.js +10 -0
- package/dist/types/Notification.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/types/Notification.ts +27 -0
- package/src/version.ts +1 -1
|
@@ -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
|
|
76
|
-
//
|
|
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(
|
|
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
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
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 {
|