react-native-notify-kit 10.7.2 → 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 +944 -93
- package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +4281 -57
- package/android/src/test/java/app/notifee/core/NotificationManagerActionRoutingTest.java +37 -0
- package/dist/fcm/parseFcmPayload.d.ts +1 -0
- package/dist/fcm/parseFcmPayload.js.map +1 -1
- package/dist/fcm/reconstructNotification.js +96 -8
- package/dist/fcm/reconstructNotification.js.map +1 -1
- 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/validators/validateAndroidPressAction.js +3 -0
- package/dist/validators/validateAndroidPressAction.js.map +1 -1
- package/dist/validators/validateIOSNotification.js +5 -2
- package/dist/validators/validateIOSNotification.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/ios/NotifeeCore/NotifeeCore.m +36 -3
- package/ios/NotifeeCore/NotifeeCoreExtensionHelper.m +82 -3
- package/ios/NotifeeCore/NotifeeCoreFcmIdentity.h +91 -0
- package/package.json +1 -1
- package/server/README.md +5 -3
- package/server/dist/buildPayload.d.ts.map +1 -1
- package/server/dist/buildPayload.js +2 -0
- package/server/dist/buildPayload.js.map +1 -1
- package/server/dist/ios.d.ts.map +1 -1
- package/server/dist/ios.js +8 -1
- package/server/dist/ios.js.map +1 -1
- package/server/dist/serialize.d.ts +2 -0
- package/server/dist/serialize.d.ts.map +1 -1
- package/server/dist/serialize.js +6 -0
- package/server/dist/serialize.js.map +1 -1
- package/server/dist/types.d.ts +1 -1
- package/server/dist/types.d.ts.map +1 -1
- package/server/dist/validation.d.ts.map +1 -1
- package/server/dist/validation.js +160 -6
- package/server/dist/validation.js.map +1 -1
- package/server/src/buildPayload.ts +2 -0
- package/server/src/ios.ts +8 -1
- package/server/src/serialize.ts +8 -0
- package/server/src/types.ts +1 -0
- package/server/src/validation.ts +187 -6
- package/src/fcm/parseFcmPayload.ts +1 -0
- package/src/fcm/reconstructNotification.ts +105 -10
- package/src/internal/fcmContract.d.ts +57 -12
- package/src/types/Notification.ts +27 -0
- package/src/validators/validateAndroidPressAction.ts +4 -0
- package/src/validators/validateIOSNotification.ts +7 -2
- package/src/version.ts +1 -1
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
package app.notifee.core;
|
|
2
2
|
|
|
3
3
|
import static org.junit.Assert.assertEquals;
|
|
4
|
+
import static org.junit.Assert.assertFalse;
|
|
4
5
|
import static org.junit.Assert.assertNotNull;
|
|
5
6
|
import static org.junit.Assert.assertNull;
|
|
7
|
+
import static org.junit.Assert.assertSame;
|
|
6
8
|
import static org.junit.Assert.assertThrows;
|
|
7
9
|
import static org.junit.Assert.assertTrue;
|
|
8
10
|
import static org.junit.Assert.fail;
|
|
9
11
|
|
|
12
|
+
import android.app.ForegroundServiceStartNotAllowedException;
|
|
13
|
+
import android.app.InvalidForegroundServiceTypeException;
|
|
14
|
+
import android.app.MissingForegroundServiceTypeException;
|
|
10
15
|
import android.app.Notification;
|
|
11
16
|
import android.app.NotificationChannel;
|
|
12
17
|
import android.app.NotificationManager;
|
|
13
18
|
import android.app.Service;
|
|
14
19
|
import android.content.ComponentName;
|
|
15
20
|
import android.content.Context;
|
|
21
|
+
import android.content.ContextWrapper;
|
|
16
22
|
import android.content.Intent;
|
|
17
23
|
import android.content.pm.PackageManager;
|
|
18
24
|
import android.content.pm.ServiceInfo;
|
|
25
|
+
import android.os.Build;
|
|
19
26
|
import android.os.Bundle;
|
|
27
|
+
import android.os.Looper;
|
|
20
28
|
import androidx.core.app.NotificationCompat;
|
|
21
29
|
import app.notifee.core.event.ForegroundServiceEvent;
|
|
22
30
|
import app.notifee.core.event.NotificationEvent;
|
|
@@ -24,6 +32,14 @@ import java.lang.reflect.Field;
|
|
|
24
32
|
import java.lang.reflect.Method;
|
|
25
33
|
import java.util.ArrayList;
|
|
26
34
|
import java.util.List;
|
|
35
|
+
import java.util.Map;
|
|
36
|
+
import java.util.UUID;
|
|
37
|
+
import java.util.concurrent.CountDownLatch;
|
|
38
|
+
import java.util.concurrent.ExecutorService;
|
|
39
|
+
import java.util.concurrent.Executors;
|
|
40
|
+
import java.util.concurrent.Future;
|
|
41
|
+
import java.util.concurrent.TimeUnit;
|
|
42
|
+
import java.util.concurrent.atomic.AtomicReference;
|
|
27
43
|
import org.greenrobot.eventbus.Subscribe;
|
|
28
44
|
import org.greenrobot.eventbus.ThreadMode;
|
|
29
45
|
import org.junit.After;
|
|
@@ -37,28 +53,4258 @@ import org.robolectric.android.controller.ServiceController;
|
|
|
37
53
|
import org.robolectric.annotation.Config;
|
|
38
54
|
import org.robolectric.annotation.Implementation;
|
|
39
55
|
import org.robolectric.annotation.Implements;
|
|
56
|
+
import org.robolectric.shadows.ShadowLog;
|
|
40
57
|
import org.robolectric.shadows.ShadowNotificationManager;
|
|
41
58
|
import org.robolectric.shadows.ShadowService;
|
|
42
59
|
|
|
43
60
|
@RunWith(RobolectricTestRunner.class)
|
|
44
61
|
public class ForegroundServiceTest {
|
|
45
62
|
|
|
63
|
+
private final List<ServiceController<ForegroundService>> controllers = new ArrayList<>();
|
|
64
|
+
|
|
46
65
|
@Before
|
|
47
66
|
public void setUp() {
|
|
48
67
|
// Initialize ContextHolder so the service can access the application context
|
|
49
68
|
ContextHolder.setApplicationContext(RuntimeEnvironment.getApplication());
|
|
50
69
|
}
|
|
51
70
|
|
|
52
|
-
@After
|
|
53
|
-
public void tearDown() throws Exception {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
@After
|
|
72
|
+
public void tearDown() throws Exception {
|
|
73
|
+
for (int i = controllers.size() - 1; i >= 0; i--) {
|
|
74
|
+
controllers.get(i).destroy();
|
|
75
|
+
}
|
|
76
|
+
org.robolectric.Shadows.shadowOf(Looper.getMainLooper()).idle();
|
|
77
|
+
ContextHolder.setApplicationContext(RuntimeEnvironment.getApplication());
|
|
78
|
+
// Reset caches and lifecycle registries only after runtime observations, so preserved
|
|
79
|
+
// UNKNOWN obligations and services created directly cannot leak into neighbouring tests.
|
|
80
|
+
ForegroundService.mCurrentNotificationId = null;
|
|
81
|
+
ForegroundService.mCurrentForegroundServiceType = -1;
|
|
82
|
+
setPrivateStatic("mCurrentNotificationBundle", null);
|
|
83
|
+
setPrivateStatic("mCurrentNotification", null);
|
|
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
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
@Test
|
|
3154
|
+
@Config(sdk = {24, 26, 30, 33, 34})
|
|
3155
|
+
public void stop_withoutInstance_doesNotStartServiceOrEmitRunner() {
|
|
3156
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3157
|
+
FgsRunnerCapture capture = new FgsRunnerCapture();
|
|
3158
|
+
EventBus.register(capture);
|
|
3159
|
+
try {
|
|
3160
|
+
ForegroundService.stop();
|
|
3161
|
+
idleMain();
|
|
3162
|
+
assertTrue(context.starts.isEmpty());
|
|
3163
|
+
assertEquals(0, context.stops);
|
|
3164
|
+
assertTrue(capture.events.isEmpty());
|
|
3165
|
+
assertNull(ForegroundService.mCurrentNotificationId);
|
|
3166
|
+
} finally {
|
|
3167
|
+
EventBus.unregister(capture);
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
|
|
3171
|
+
@Test
|
|
3172
|
+
@Config(sdk = 33)
|
|
3173
|
+
public void stop_startStopDestroySecondStop_doesNotRecreateService() {
|
|
3174
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3175
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
3176
|
+
ForegroundService service = controller.create().get();
|
|
3177
|
+
createChannel();
|
|
3178
|
+
service.onStartCommand(buildStartIntent("first", 101), 0, 1);
|
|
3179
|
+
|
|
3180
|
+
ForegroundService.stop();
|
|
3181
|
+
idleMain();
|
|
3182
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3183
|
+
controller.destroy();
|
|
3184
|
+
controllers.remove(controller);
|
|
3185
|
+
ForegroundService.stop();
|
|
3186
|
+
idleMain();
|
|
3187
|
+
assertTrue(context.starts.isEmpty());
|
|
3188
|
+
assertNull(ForegroundService.mCurrentNotificationId);
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
@Test
|
|
3192
|
+
@Config(sdk = 33)
|
|
3193
|
+
public void stop_nullNotificationCache_stillStopsLiveInstance() throws Exception {
|
|
3194
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3195
|
+
ForegroundService service = buildService().create().get();
|
|
3196
|
+
createChannel();
|
|
3197
|
+
service.onStartCommand(buildStartIntent("live", 102), 0, 1);
|
|
3198
|
+
ForegroundService.mCurrentNotificationId = null;
|
|
3199
|
+
ForegroundService.mCurrentForegroundServiceType = -1;
|
|
3200
|
+
setPrivateStatic("mCurrentNotificationBundle", null);
|
|
3201
|
+
setPrivateStatic("mCurrentNotification", null);
|
|
3202
|
+
setPrivateStatic("mCurrentHashCode", 0);
|
|
3203
|
+
|
|
3204
|
+
ForegroundService.stop();
|
|
3205
|
+
idleMain();
|
|
3206
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3207
|
+
assertTrue(context.starts.isEmpty());
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
@Test
|
|
3211
|
+
@Config(sdk = 33)
|
|
3212
|
+
public void stop_concurrentDestroy_doesNotDispatchNewGeneration() throws Exception {
|
|
3213
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3214
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
3215
|
+
ForegroundService service = controller.create().get();
|
|
3216
|
+
createChannel();
|
|
3217
|
+
service.onStartCommand(buildStartIntent("old", 103), 0, 1);
|
|
3218
|
+
CountDownLatch begin = new CountDownLatch(1);
|
|
3219
|
+
AtomicReference<Throwable> failure = new AtomicReference<>();
|
|
3220
|
+
Thread stop =
|
|
3221
|
+
new Thread(
|
|
3222
|
+
() -> {
|
|
3223
|
+
try {
|
|
3224
|
+
assertTrue(begin.await(5, TimeUnit.SECONDS));
|
|
3225
|
+
ForegroundService.stop();
|
|
3226
|
+
} catch (Throwable e) {
|
|
3227
|
+
failure.set(e);
|
|
3228
|
+
}
|
|
3229
|
+
});
|
|
3230
|
+
stop.start();
|
|
3231
|
+
begin.countDown();
|
|
3232
|
+
controller.destroy();
|
|
3233
|
+
controllers.remove(controller);
|
|
3234
|
+
stop.join(5000);
|
|
3235
|
+
assertFalse(stop.isAlive());
|
|
3236
|
+
assertNull(failure.get());
|
|
3237
|
+
idleMain();
|
|
3238
|
+
assertTrue(context.starts.isEmpty());
|
|
3239
|
+
}
|
|
3240
|
+
|
|
3241
|
+
@Test
|
|
3242
|
+
@Config(sdk = 33)
|
|
3243
|
+
public void onDestroy_oldInstance_preservesNewInstanceAndAllNotificationState() throws Exception {
|
|
3244
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3245
|
+
ServiceController<ForegroundService> oldController = buildService();
|
|
3246
|
+
ForegroundService old = oldController.create().get();
|
|
3247
|
+
createChannel();
|
|
3248
|
+
old.onStartCommand(buildStartIntent("old", 104), 0, 1);
|
|
3249
|
+
ForegroundService current = buildService().create().get();
|
|
3250
|
+
current.onStartCommand(buildStartIntent("new", 105, "new title"), 0, 2);
|
|
3251
|
+
oldController.destroy();
|
|
3252
|
+
controllers.remove(oldController);
|
|
3253
|
+
|
|
3254
|
+
assertCurrentState("new", 105, "new title");
|
|
3255
|
+
ForegroundService.stop();
|
|
3256
|
+
idleMain();
|
|
3257
|
+
assertTrue(org.robolectric.Shadows.shadowOf(current).isStoppedBySelf());
|
|
3258
|
+
assertTrue(context.starts.isEmpty());
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
@Test
|
|
3262
|
+
@Config(sdk = 33)
|
|
3263
|
+
public void runnerCompletion_oldInstance_doesNotDemoteOrClearNewInstance() throws Exception {
|
|
3264
|
+
useRecordingContext();
|
|
3265
|
+
FgsRunnerCapture capture = new FgsRunnerCapture();
|
|
3266
|
+
EventBus.register(capture);
|
|
3267
|
+
try {
|
|
3268
|
+
ForegroundService old = buildService().create().get();
|
|
3269
|
+
createChannel();
|
|
3270
|
+
old.onStartCommand(buildStartIntent("old", 106), 0, 1);
|
|
3271
|
+
ForegroundService current = buildService().create().get();
|
|
3272
|
+
current.onStartCommand(buildStartIntent("new", 107, "new title"), 0, 2);
|
|
3273
|
+
assertEquals(2, capture.events.size());
|
|
3274
|
+
|
|
3275
|
+
capture.events.get(0).setCompletionResult();
|
|
3276
|
+
idleMain();
|
|
3277
|
+
assertCurrentState("new", 107, "new title");
|
|
3278
|
+
assertFalse(org.robolectric.Shadows.shadowOf(current).isForegroundStopped());
|
|
3279
|
+
} finally {
|
|
3280
|
+
EventBus.unregister(capture);
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
@Test
|
|
3285
|
+
@Config(sdk = 33)
|
|
3286
|
+
public void runnerCompletion_oldRunOnSameInstance_preservesNewRun() throws Exception {
|
|
3287
|
+
useRecordingContext();
|
|
3288
|
+
FgsRunnerCapture capture = new FgsRunnerCapture();
|
|
3289
|
+
EventBus.register(capture);
|
|
3290
|
+
try {
|
|
3291
|
+
ForegroundService service = buildService().create().get();
|
|
3292
|
+
createChannel();
|
|
3293
|
+
service.onStartCommand(buildStartIntent("old", 108), 0, 1);
|
|
3294
|
+
service.onStartCommand(buildStopIntent(), 0, 2);
|
|
3295
|
+
service.onStartCommand(buildStartIntent("new", 109, "new title"), 0, 3);
|
|
3296
|
+
assertEquals(2, capture.events.size());
|
|
3297
|
+
|
|
3298
|
+
capture.events.get(0).setCompletionResult();
|
|
3299
|
+
idleMain();
|
|
3300
|
+
assertCurrentState("new", 109, "new title");
|
|
3301
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
|
|
3302
|
+
capture.events.get(1).setCompletionResult();
|
|
3303
|
+
idleMain();
|
|
3304
|
+
assertNull(ForegroundService.mCurrentNotificationId);
|
|
3305
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
|
|
3306
|
+
} finally {
|
|
3307
|
+
EventBus.unregister(capture);
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
@Test
|
|
3312
|
+
@Config(sdk = 33)
|
|
3313
|
+
public void stop_acceptedPendingStart_fulfillsRealContractBeforeStopping() {
|
|
3314
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3315
|
+
createChannel();
|
|
3316
|
+
ForegroundService.start(110, buildNotification(), buildNotificationBundle("pending"));
|
|
3317
|
+
ForegroundService.stop();
|
|
3318
|
+
idleMain();
|
|
3319
|
+
assertEquals(1, context.starts.size());
|
|
3320
|
+
ForegroundService service = buildService().create().get();
|
|
3321
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3322
|
+
service.onStartCommand(context.starts.get(0), 0, 1);
|
|
3323
|
+
idleMain();
|
|
3324
|
+
assertEquals(110, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
|
|
3325
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3326
|
+
assertNull(ForegroundService.mCurrentNotificationId);
|
|
3327
|
+
assertEquals(1, context.starts.size());
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
@Test
|
|
3331
|
+
@Config(sdk = 33)
|
|
3332
|
+
public void stop_betweenCreateAndPendingDelivery_doesNotStopBeforeRealPromotion() {
|
|
3333
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3334
|
+
createChannel();
|
|
3335
|
+
ForegroundService.start(111, buildNotification(), buildNotificationBundle("pending"));
|
|
3336
|
+
ForegroundService service = buildService().create().get();
|
|
3337
|
+
ForegroundService.stop();
|
|
3338
|
+
idleMain();
|
|
3339
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3340
|
+
assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
|
|
3341
|
+
|
|
3342
|
+
service.onStartCommand(context.starts.get(0), 0, 1);
|
|
3343
|
+
idleMain();
|
|
3344
|
+
assertEquals(111, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
|
|
3345
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3346
|
+
assertEquals(1, context.starts.size());
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
@Test
|
|
3350
|
+
@Config(sdk = 33)
|
|
3351
|
+
public void stop_newStartAcceptedBeforeQueuedStop_doesNotCancelLaterStart() throws Exception {
|
|
3352
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3353
|
+
createChannel();
|
|
3354
|
+
ForegroundService service = buildService().create().get();
|
|
3355
|
+
service.onStartCommand(buildStartIntent("same", 112, "A"), 0, 1);
|
|
3356
|
+
ForegroundService.stop();
|
|
3357
|
+
Intent later = buildStartIntent("same", 112, "B");
|
|
3358
|
+
ForegroundService.start(
|
|
3359
|
+
112, later.getParcelableExtra("notification"), later.getBundleExtra("notificationBundle"));
|
|
3360
|
+
idleMain();
|
|
3361
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3362
|
+
service.onStartCommand(context.starts.get(0), 0, 2);
|
|
3363
|
+
idleMain();
|
|
3364
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3365
|
+
assertCurrentState("same", 112, "B");
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3368
|
+
@Test
|
|
3369
|
+
@Config(
|
|
3370
|
+
sdk = {28, 33, 34},
|
|
3371
|
+
shadows = {RejectBStartForegroundShadowService.class, RejectBNotificationShadowManager.class})
|
|
3372
|
+
public void stop_failedRestart_preservesPreviousSubmittedStateAndRunner() throws Exception {
|
|
3373
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3374
|
+
if (Build.VERSION.SDK_INT >= 34) {
|
|
3375
|
+
declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
|
3376
|
+
}
|
|
3377
|
+
createChannel();
|
|
3378
|
+
FgsRunnerCapture capture = new FgsRunnerCapture();
|
|
3379
|
+
EventBus.register(capture);
|
|
3380
|
+
try {
|
|
3381
|
+
ForegroundService service = buildService().create().get();
|
|
3382
|
+
Intent a = buildStartIntent("same", 177, "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
|
3383
|
+
ForegroundService.start(
|
|
3384
|
+
177, a.getParcelableExtra("notification"), a.getBundleExtra("notificationBundle"));
|
|
3385
|
+
service.onStartCommand(context.starts.get(0), 0, 1);
|
|
3386
|
+
Field tokenField = ForegroundService.class.getDeclaredField("mRunnerToken");
|
|
3387
|
+
tokenField.setAccessible(true);
|
|
3388
|
+
Object tokenA = tokenField.get(service);
|
|
3389
|
+
int typeA = ForegroundService.mCurrentForegroundServiceType;
|
|
3390
|
+
|
|
3391
|
+
ForegroundService.stop();
|
|
3392
|
+
Intent b = buildStartIntent("same", 177, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
|
3393
|
+
ForegroundService.start(
|
|
3394
|
+
177, b.getParcelableExtra("notification"), b.getBundleExtra("notificationBundle"));
|
|
3395
|
+
SecurityException failure =
|
|
3396
|
+
assertThrows(
|
|
3397
|
+
SecurityException.class, () -> service.onStartCommand(context.starts.get(1), 0, 2));
|
|
3398
|
+
|
|
3399
|
+
assertEquals("posting B rejected", failure.getMessage());
|
|
3400
|
+
assertEquals(
|
|
3401
|
+
"A",
|
|
3402
|
+
org.robolectric.Shadows.shadowOf(
|
|
3403
|
+
(NotificationManager)
|
|
3404
|
+
RuntimeEnvironment.getApplication()
|
|
3405
|
+
.getSystemService(Context.NOTIFICATION_SERVICE))
|
|
3406
|
+
.getNotification(177)
|
|
3407
|
+
.extras
|
|
3408
|
+
.getString(Notification.EXTRA_TITLE));
|
|
3409
|
+
assertCurrentState("same", 177, "A");
|
|
3410
|
+
assertEquals(typeA, ForegroundService.mCurrentForegroundServiceType);
|
|
3411
|
+
assertTrue(tokenA == tokenField.get(service));
|
|
3412
|
+
assertEquals(1, capture.events.size());
|
|
3413
|
+
assertTrue(ForegroundService.repostIfActive("same"));
|
|
3414
|
+
idleMain();
|
|
3415
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
|
|
3416
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3417
|
+
capture.events.get(0).setCompletionResult();
|
|
3418
|
+
idleMain();
|
|
3419
|
+
assertCurrentState("same", 177, "A");
|
|
3420
|
+
assertTrue(tokenA == tokenField.get(service));
|
|
3421
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
|
|
3422
|
+
|
|
3423
|
+
// A failed reservation must still be replaced by a fresh runner when C succeeds in the
|
|
3424
|
+
// reserved session, even if C uses a different notification ID.
|
|
3425
|
+
Intent c = buildStartIntent("new", 178, "C", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
|
3426
|
+
ForegroundService.start(
|
|
3427
|
+
178, c.getParcelableExtra("notification"), c.getBundleExtra("notificationBundle"));
|
|
3428
|
+
service.onStartCommand(context.starts.get(2), 0, 3);
|
|
3429
|
+
assertCurrentState("new", 178, "C");
|
|
3430
|
+
assertFalse(tokenA == tokenField.get(service));
|
|
3431
|
+
assertEquals(2, capture.events.size());
|
|
3432
|
+
capture.events.get(0).setCompletionResult();
|
|
3433
|
+
idleMain();
|
|
3434
|
+
assertCurrentState("new", 178, "C");
|
|
3435
|
+
capture.events.get(1).setCompletionResult();
|
|
3436
|
+
idleMain();
|
|
3437
|
+
assertNull(ForegroundService.mCurrentNotificationId);
|
|
3438
|
+
} finally {
|
|
3439
|
+
EventBus.unregister(capture);
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
@Test
|
|
3444
|
+
@Config(sdk = 33)
|
|
3445
|
+
public void onStartCommand_stopAndNullWithPendingStart_doNotSuppressRealStart() {
|
|
3446
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3447
|
+
createChannel();
|
|
3448
|
+
ForegroundService.start(113, buildNotification(), buildNotificationBundle("pending"));
|
|
3449
|
+
ForegroundService service = buildService().create().get();
|
|
3450
|
+
service.onStartCommand(buildStopIntent(), 0, 1);
|
|
3451
|
+
service.onStartCommand(null, 0, 2);
|
|
3452
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3453
|
+
assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
|
|
3454
|
+
|
|
3455
|
+
service.onStartCommand(context.starts.get(0), 0, 3);
|
|
3456
|
+
idleMain();
|
|
3457
|
+
assertEquals(113, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
|
|
3458
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3459
|
+
}
|
|
3460
|
+
|
|
3461
|
+
@Test
|
|
3462
|
+
@Config(
|
|
3463
|
+
sdk = {31, 33, 34, 35},
|
|
3464
|
+
shadows = DefensiveFailureShadowService.class)
|
|
3465
|
+
public void onStartCommand_stopAndNullDenied_areNonFatalAndDoNotClaimPromotion()
|
|
3466
|
+
throws Exception {
|
|
3467
|
+
if (Build.VERSION.SDK_INT >= 34) {
|
|
3468
|
+
declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
|
3469
|
+
}
|
|
3470
|
+
DefensiveFailureShadowService.failure = new ForegroundServiceStartNotAllowedException("denied");
|
|
3471
|
+
DefensiveFailureShadowService.attempts = 0;
|
|
3472
|
+
ShadowLog.clear();
|
|
3473
|
+
ForegroundService service = buildService().create().get();
|
|
3474
|
+
service.onStartCommand(buildStopIntent(), 0, 1);
|
|
3475
|
+
service.onStartCommand(null, 0, 2);
|
|
3476
|
+
|
|
3477
|
+
assertEquals(2, DefensiveFailureShadowService.attempts);
|
|
3478
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3479
|
+
assertTrue(
|
|
3480
|
+
ShadowLog.getLogsForTag("NOTIFEE").stream()
|
|
3481
|
+
.anyMatch(item -> item.throwable == DefensiveFailureShadowService.failure));
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
@Test
|
|
3485
|
+
@Config(sdk = 31, shadows = DefensiveFailureShadowService.class)
|
|
3486
|
+
public void onStartCommand_stopDeniedApi31_isNonFatal() {
|
|
3487
|
+
DefensiveFailureShadowService.failure = new ForegroundServiceStartNotAllowedException("denied");
|
|
3488
|
+
ForegroundService service = buildService().create().get();
|
|
3489
|
+
service.onStartCommand(buildStopIntent(), 0, 1);
|
|
3490
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3491
|
+
}
|
|
3492
|
+
|
|
3493
|
+
@Test
|
|
3494
|
+
@Config(
|
|
3495
|
+
sdk = {24, 30, 33},
|
|
3496
|
+
shadows = DefensiveFailureShadowService.class)
|
|
3497
|
+
public void onStartCommand_stopGenericIllegalState_remainsObservable() {
|
|
3498
|
+
assertTerminalFailureObservable(new IllegalStateException("unrelated ISE"));
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
@Test
|
|
3502
|
+
@Config(sdk = 34, shadows = DefensiveFailureShadowService.class)
|
|
3503
|
+
public void onStartCommand_stopMissingType_remainsObservable() throws Exception {
|
|
3504
|
+
declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
|
3505
|
+
assertTerminalFailureObservable(new MissingForegroundServiceTypeException("missing type"));
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
@Test
|
|
3509
|
+
@Config(sdk = 34, shadows = DefensiveFailureShadowService.class)
|
|
3510
|
+
public void onStartCommand_stopInvalidType_remainsObservable() throws Exception {
|
|
3511
|
+
declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
|
3512
|
+
assertTerminalFailureObservable(new InvalidForegroundServiceTypeException("invalid type"));
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
@Test
|
|
3516
|
+
@Config(sdk = 34, shadows = DefensiveFailureShadowService.class)
|
|
3517
|
+
public void onStartCommand_noneDenied_isNotSoftSuppressed() throws Exception {
|
|
3518
|
+
declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
|
3519
|
+
DefensiveFailureShadowService.failure =
|
|
3520
|
+
new ForegroundServiceStartNotAllowedException("NONE denied");
|
|
3521
|
+
ForegroundService service = buildService().create().get();
|
|
3522
|
+
Intent none = buildStartIntent("none", 114, "none", ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE);
|
|
3523
|
+
// The model normalizes NONE to MANIFEST on API 34. Removing only the application context
|
|
3524
|
+
// makes manifest resolution return NONE; the Service still has its declared defensive type.
|
|
3525
|
+
ContextHolder.setApplicationContext(null);
|
|
3526
|
+
RuntimeException thrown =
|
|
3527
|
+
assertThrows(RuntimeException.class, () -> service.onStartCommand(none, 0, 1));
|
|
3528
|
+
assertTrue(
|
|
3529
|
+
thrown == DefensiveFailureShadowService.failure
|
|
3530
|
+
|| thrown.getCause() == DefensiveFailureShadowService.failure);
|
|
3531
|
+
assertEquals(Integer.MAX_VALUE - 1, DefensiveFailureShadowService.lastId);
|
|
3532
|
+
assertEquals(
|
|
3533
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC, DefensiveFailureShadowService.lastType);
|
|
3534
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
@Test
|
|
3538
|
+
@Config(sdk = 33, shadows = SecurityDefensiveChannelShadowManager.class)
|
|
3539
|
+
public void onStartCommand_terminalChannelSecurityException_remainsObservable() {
|
|
3540
|
+
for (Intent terminal : new Intent[] {buildStopIntent(), null}) {
|
|
3541
|
+
ForegroundService service = buildService().create().get();
|
|
3542
|
+
SecurityException thrown =
|
|
3543
|
+
assertThrows(SecurityException.class, () -> service.onStartCommand(terminal, 0, 1));
|
|
3544
|
+
assertEquals("defensive channel failure", thrown.getMessage());
|
|
3545
|
+
assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
|
|
3546
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
@Test
|
|
3551
|
+
@Config(sdk = 33, shadows = SecurityStopForegroundShadowService.class)
|
|
3552
|
+
public void onStartCommand_terminalTeardownSecurityException_remainsObservable() {
|
|
3553
|
+
for (Intent terminal : new Intent[] {buildStopIntent(), null}) {
|
|
3554
|
+
ForegroundService service = buildService().create().get();
|
|
3555
|
+
SecurityException thrown =
|
|
3556
|
+
assertThrows(SecurityException.class, () -> service.onStartCommand(terminal, 0, 1));
|
|
3557
|
+
assertEquals("defensive teardown failure", thrown.getMessage());
|
|
3558
|
+
assertEquals(
|
|
3559
|
+
Integer.MAX_VALUE - 1,
|
|
3560
|
+
org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
|
|
3561
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3562
|
+
}
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
@Test
|
|
3566
|
+
@Config(sdk = 33, shadows = DefensiveFailureShadowService.class)
|
|
3567
|
+
public void onStartCommand_pendingStartDenied_isNotSoftSuppressedByStop() {
|
|
3568
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3569
|
+
ForegroundService.start(115, buildNotification(), buildNotificationBundle("pending"));
|
|
3570
|
+
ForegroundService.stop();
|
|
3571
|
+
DefensiveFailureShadowService.failure =
|
|
3572
|
+
new ForegroundServiceStartNotAllowedException("real START denied");
|
|
3573
|
+
ForegroundService service = buildService().create().get();
|
|
3574
|
+
assertThrows(
|
|
3575
|
+
ForegroundServiceStartNotAllowedException.class,
|
|
3576
|
+
() -> service.onStartCommand(context.starts.get(0), 0, 1));
|
|
3577
|
+
idleMain();
|
|
3578
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3579
|
+
}
|
|
3580
|
+
|
|
3581
|
+
private void assertTerminalFailureObservable(RuntimeException failure) {
|
|
3582
|
+
DefensiveFailureShadowService.failure = failure;
|
|
3583
|
+
ForegroundService service = buildService().create().get();
|
|
3584
|
+
RuntimeException thrown =
|
|
3585
|
+
assertThrows(RuntimeException.class, () -> service.onStartCommand(buildStopIntent(), 0, 1));
|
|
3586
|
+
assertTrue(thrown == failure || thrown.getCause() == failure);
|
|
3587
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
@Test
|
|
3591
|
+
@Config(sdk = 33)
|
|
3592
|
+
public void runnerCompletion_stopSupersededByNewStart_preservesLaterRun() throws Exception {
|
|
3593
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3594
|
+
FgsRunnerCapture capture = new FgsRunnerCapture();
|
|
3595
|
+
EventBus.register(capture);
|
|
3596
|
+
try {
|
|
3597
|
+
ForegroundService service = buildService().create().get();
|
|
3598
|
+
createChannel();
|
|
3599
|
+
service.onStartCommand(buildStartIntent("same", 116, "A"), 0, 1);
|
|
3600
|
+
ForegroundService.stop();
|
|
3601
|
+
Intent later = buildStartIntent("same", 116, "B");
|
|
3602
|
+
ForegroundService.start(
|
|
3603
|
+
116,
|
|
3604
|
+
later.getParcelableExtra("notification"),
|
|
3605
|
+
later.getBundleExtra("notificationBundle"));
|
|
3606
|
+
service.onStartCommand(context.starts.get(0), 0, 2);
|
|
3607
|
+
capture.events.get(0).setCompletionResult();
|
|
3608
|
+
idleMain();
|
|
3609
|
+
|
|
3610
|
+
assertCurrentState("same", 116, "B");
|
|
3611
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
|
|
3612
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3613
|
+
assertEquals(2, capture.events.size());
|
|
3614
|
+
capture.events.get(1).setCompletionResult();
|
|
3615
|
+
idleMain();
|
|
3616
|
+
assertNull(ForegroundService.mCurrentNotificationId);
|
|
3617
|
+
} finally {
|
|
3618
|
+
EventBus.unregister(capture);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
@Test
|
|
3623
|
+
@Config(sdk = 33)
|
|
3624
|
+
public void stop_createdButUnpublishedStart_waitsForRealStartCommand() {
|
|
3625
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3626
|
+
ForegroundService service = buildService().create().get();
|
|
3627
|
+
ForegroundService.stop();
|
|
3628
|
+
idleMain();
|
|
3629
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3630
|
+
assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
|
|
3631
|
+
createChannel();
|
|
3632
|
+
service.onStartCommand(buildStartIntent("unknown", 117), 0, 1);
|
|
3633
|
+
idleMain();
|
|
3634
|
+
assertEquals(117, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
|
|
3635
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3636
|
+
assertTrue(context.starts.isEmpty());
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
@Test
|
|
3640
|
+
@Config(sdk = 33)
|
|
3641
|
+
public void stop_multiplePendingStarts_waitsForAllRealCommands() {
|
|
3642
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3643
|
+
createChannel();
|
|
3644
|
+
ForegroundService.start(118, buildNotification(), buildNotificationBundle("pending"));
|
|
3645
|
+
ForegroundService.start(118, buildNotification(), buildNotificationBundle("pending"));
|
|
3646
|
+
ForegroundService.stop();
|
|
3647
|
+
ForegroundService service = buildService().create().get();
|
|
3648
|
+
service.onStartCommand(context.starts.get(0), 0, 1);
|
|
3649
|
+
idleMain();
|
|
3650
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3651
|
+
service.onStartCommand(context.starts.get(1), 0, 2);
|
|
3652
|
+
idleMain();
|
|
3653
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3654
|
+
assertEquals(2, org.robolectric.Shadows.shadowOf(service).getStopSelfResultId());
|
|
3655
|
+
assertEquals(2, context.starts.size());
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
@Test
|
|
3659
|
+
@Config(sdk = 33)
|
|
3660
|
+
public void onStartCommand_pendingWithoutPromotion_doesNotAcquireStopAuthority() {
|
|
3661
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3662
|
+
// Accepted START, but its delivery has no notification. STOP must not turn this into a
|
|
3663
|
+
// successful/absent START obligation, even though malformed START handling is out of scope.
|
|
3664
|
+
ForegroundService.start(119, null, buildNotificationBundle("pending"));
|
|
3665
|
+
ForegroundService.stop();
|
|
3666
|
+
ForegroundService service = buildService().create().get();
|
|
3667
|
+
service.onStartCommand(context.starts.get(0), 0, 1);
|
|
3668
|
+
idleMain();
|
|
3669
|
+
ForegroundService.stop();
|
|
3670
|
+
service.onStartCommand(null, 0, 2);
|
|
3671
|
+
idleMain();
|
|
3672
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3673
|
+
assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
|
|
3674
|
+
assertEquals(1, context.starts.size());
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
@Test
|
|
3678
|
+
@Config(sdk = 33)
|
|
3679
|
+
public void start_dispatchFailure_doesNotCancelEarlierStop() {
|
|
3680
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3681
|
+
createChannel();
|
|
3682
|
+
ForegroundService service = buildService().create().get();
|
|
3683
|
+
service.onStartCommand(buildStartIntent("live", 120), 0, 1);
|
|
3684
|
+
ForegroundService.stop();
|
|
3685
|
+
context.startFailure = new ForegroundServiceStartNotAllowedException("dispatch rejected");
|
|
3686
|
+
assertThrows(
|
|
3687
|
+
ForegroundServiceStartNotAllowedException.class,
|
|
3688
|
+
() -> ForegroundService.start(121, buildNotification(), buildNotificationBundle("failed")));
|
|
3689
|
+
idleMain();
|
|
3690
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3691
|
+
assertNull(ForegroundService.mCurrentNotificationId);
|
|
3692
|
+
assertEquals(1, context.starts.size());
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
@Test
|
|
3696
|
+
@Config(sdk = 33, shadows = RefuseStopSelfShadowService.class)
|
|
3697
|
+
public void stop_frameworkRefusesTokenStop_preservesCurrentState() throws Exception {
|
|
3698
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3699
|
+
ForegroundService service = buildService().create().get();
|
|
3700
|
+
createChannel();
|
|
3701
|
+
service.onStartCommand(buildStartIntent("live", 122, "live title"), 0, 1);
|
|
3702
|
+
ForegroundService.stop();
|
|
3703
|
+
idleMain();
|
|
3704
|
+
assertCurrentState("live", 122, "live title");
|
|
3705
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
|
|
3706
|
+
assertTrue(context.starts.isEmpty());
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3709
|
+
@Test
|
|
3710
|
+
@Config(sdk = 34)
|
|
3711
|
+
public void onTimeout_oldInstance_doesNotClearNewStateOrEmitNewInstanceEvent() throws Exception {
|
|
3712
|
+
declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
|
|
3713
|
+
createChannel();
|
|
3714
|
+
ForegroundService old = buildService().create().get();
|
|
3715
|
+
old.onStartCommand(
|
|
3716
|
+
buildStartIntent("old", 123, "old", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
|
|
3717
|
+
0,
|
|
3718
|
+
1);
|
|
3719
|
+
ForegroundService current = buildService().create().get();
|
|
3720
|
+
current.onStartCommand(
|
|
3721
|
+
buildStartIntent(
|
|
3722
|
+
"new", 124, "new title", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
|
|
3723
|
+
0,
|
|
3724
|
+
2);
|
|
3725
|
+
FgsEventCapture capture = new FgsEventCapture();
|
|
3726
|
+
EventBus.register(capture);
|
|
3727
|
+
try {
|
|
3728
|
+
old.onTimeout(1);
|
|
3729
|
+
assertCurrentState("new", 124, "new title");
|
|
3730
|
+
assertEquals(
|
|
3731
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
|
|
3732
|
+
ForegroundService.mCurrentForegroundServiceType);
|
|
3733
|
+
assertTrue(capture.events.isEmpty());
|
|
3734
|
+
} finally {
|
|
3735
|
+
EventBus.unregister(capture);
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
@Test
|
|
3740
|
+
@Config(sdk = 33, shadows = FailStopForegroundShadowService.class)
|
|
3741
|
+
public void onStartCommand_denialOutsidePromotion_isStillFatal() {
|
|
3742
|
+
ForegroundService service = buildService().create().get();
|
|
3743
|
+
RuntimeException thrown =
|
|
3744
|
+
assertThrows(RuntimeException.class, () -> service.onStartCommand(buildStopIntent(), 0, 1));
|
|
3745
|
+
assertTrue(
|
|
3746
|
+
thrown instanceof ForegroundServiceStartNotAllowedException
|
|
3747
|
+
|| thrown.getCause() instanceof ForegroundServiceStartNotAllowedException);
|
|
3748
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
@Test
|
|
3752
|
+
@Config(sdk = 33)
|
|
3753
|
+
public void stop_duringStartAcceptance_preservesAndThenStopsAcceptedStart() throws Exception {
|
|
3754
|
+
CountDownLatch dispatchEntered = new CountDownLatch(1);
|
|
3755
|
+
CountDownLatch releaseDispatch = new CountDownLatch(1);
|
|
3756
|
+
CountDownLatch stopAttempted = new CountDownLatch(1);
|
|
3757
|
+
RecordingServiceContext context =
|
|
3758
|
+
new RecordingServiceContext(RuntimeEnvironment.getApplication()) {
|
|
3759
|
+
@Override
|
|
3760
|
+
public ComponentName startForegroundService(Intent intent) {
|
|
3761
|
+
dispatchEntered.countDown();
|
|
3762
|
+
try {
|
|
3763
|
+
assertTrue(releaseDispatch.await(5, TimeUnit.SECONDS));
|
|
3764
|
+
} catch (InterruptedException e) {
|
|
3765
|
+
throw new AssertionError(e);
|
|
3766
|
+
}
|
|
3767
|
+
return super.startForegroundService(intent);
|
|
3768
|
+
}
|
|
3769
|
+
};
|
|
3770
|
+
ContextHolder.setApplicationContext(context);
|
|
3771
|
+
createChannel();
|
|
3772
|
+
Notification notification = buildNotification();
|
|
3773
|
+
Bundle bundle = buildNotificationBundle("pending");
|
|
3774
|
+
ExecutorService workers = Executors.newFixedThreadPool(2);
|
|
3775
|
+
try {
|
|
3776
|
+
Future<?> start = workers.submit(() -> ForegroundService.start(125, notification, bundle));
|
|
3777
|
+
assertTrue(dispatchEntered.await(5, TimeUnit.SECONDS));
|
|
3778
|
+
Future<?> stop =
|
|
3779
|
+
workers.submit(
|
|
3780
|
+
() -> {
|
|
3781
|
+
stopAttempted.countDown();
|
|
3782
|
+
ForegroundService.stop();
|
|
3783
|
+
});
|
|
3784
|
+
assertTrue(stopAttempted.await(5, TimeUnit.SECONDS));
|
|
3785
|
+
releaseDispatch.countDown();
|
|
3786
|
+
start.get(5, TimeUnit.SECONDS);
|
|
3787
|
+
stop.get(5, TimeUnit.SECONDS);
|
|
3788
|
+
|
|
3789
|
+
ForegroundService service = buildService().create().get();
|
|
3790
|
+
service.onStartCommand(context.starts.get(0), 0, 1);
|
|
3791
|
+
idleMain();
|
|
3792
|
+
assertEquals(
|
|
3793
|
+
125, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
|
|
3794
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3795
|
+
assertEquals(1, context.starts.size());
|
|
3796
|
+
} finally {
|
|
3797
|
+
releaseDispatch.countDown();
|
|
3798
|
+
workers.shutdownNow();
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
@Test
|
|
3803
|
+
@Config(sdk = 33)
|
|
3804
|
+
public void stop_queuedForDestroyedInstance_doesNotTouchLaterInstance() throws Exception {
|
|
3805
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3806
|
+
createChannel();
|
|
3807
|
+
ServiceController<ForegroundService> oldController = buildService();
|
|
3808
|
+
ForegroundService old = oldController.create().get();
|
|
3809
|
+
old.onStartCommand(buildStartIntent("old", 126), 0, 1);
|
|
3810
|
+
ForegroundService.stop();
|
|
3811
|
+
oldController.destroy();
|
|
3812
|
+
controllers.remove(oldController);
|
|
3813
|
+
ForegroundService current = buildService().create().get();
|
|
3814
|
+
current.onStartCommand(buildStartIntent("new", 127, "new title"), 0, 2);
|
|
3815
|
+
idleMain();
|
|
3816
|
+
assertCurrentState("new", 127, "new title");
|
|
3817
|
+
assertFalse(org.robolectric.Shadows.shadowOf(current).isStoppedBySelf());
|
|
3818
|
+
assertTrue(context.starts.isEmpty());
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3821
|
+
@Test
|
|
3822
|
+
@Config(sdk = 33)
|
|
3823
|
+
public void stop_liveInstanceWithUnavailableApplicationContext_stillReachesInstance() {
|
|
3824
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3825
|
+
createChannel();
|
|
3826
|
+
ForegroundService service = buildService().create().get();
|
|
3827
|
+
service.onStartCommand(buildStartIntent("live", 128), 0, 1);
|
|
3828
|
+
ContextHolder.setApplicationContext(null);
|
|
3829
|
+
ForegroundService.stop();
|
|
3830
|
+
idleMain();
|
|
3831
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3832
|
+
assertTrue(context.starts.isEmpty());
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
@Test
|
|
3836
|
+
@Config(sdk = 24)
|
|
3837
|
+
public void stop_pendingStartBeforeApi26_preservesPlainStartContract() {
|
|
3838
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3839
|
+
ForegroundService.start(129, buildNotification(), buildNotificationBundle("pending"));
|
|
3840
|
+
ForegroundService.stop();
|
|
3841
|
+
ForegroundService service = buildService().create().get();
|
|
3842
|
+
service.onStartCommand(context.starts.get(0), 0, 1);
|
|
3843
|
+
idleMain();
|
|
3844
|
+
assertEquals(129, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
|
|
3845
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3846
|
+
assertEquals(1, context.starts.size());
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
@Test
|
|
3850
|
+
@Config(sdk = 33)
|
|
3851
|
+
public void runnerCompletion_afterStopBeforeNewDelivery_doesNotDemotePendingRun() {
|
|
3852
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3853
|
+
createChannel();
|
|
3854
|
+
FgsRunnerCapture capture = new FgsRunnerCapture();
|
|
3855
|
+
EventBus.register(capture);
|
|
3856
|
+
try {
|
|
3857
|
+
ForegroundService service = buildService().create().get();
|
|
3858
|
+
service.onStartCommand(buildStartIntent("same", 130), 0, 1);
|
|
3859
|
+
ForegroundService.stop();
|
|
3860
|
+
ForegroundService.start(130, buildNotification(), buildNotificationBundle("same"));
|
|
3861
|
+
capture.events.get(0).setCompletionResult();
|
|
3862
|
+
idleMain();
|
|
3863
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
|
|
3864
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3865
|
+
service.onStartCommand(context.starts.get(0), 0, 2);
|
|
3866
|
+
assertEquals(2, capture.events.size());
|
|
3867
|
+
assertEquals("same", ForegroundService.mCurrentNotificationId);
|
|
3868
|
+
} finally {
|
|
3869
|
+
EventBus.unregister(capture);
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
@Test
|
|
3874
|
+
@Config(sdk = 33)
|
|
3875
|
+
public void stop_laterRealPromotion_settlesEarlierDeliveredUnknownContract() {
|
|
3876
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3877
|
+
ForegroundService.start(131, null, buildNotificationBundle("unknown"));
|
|
3878
|
+
ForegroundService.stop();
|
|
3879
|
+
ForegroundService service = buildService().create().get();
|
|
3880
|
+
service.onStartCommand(context.starts.get(0), 0, 1);
|
|
3881
|
+
idleMain();
|
|
3882
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3883
|
+
createChannel();
|
|
3884
|
+
ForegroundService.start(132, buildNotification(), buildNotificationBundle("valid"));
|
|
3885
|
+
service.onStartCommand(context.starts.get(1), 0, 2);
|
|
3886
|
+
ForegroundService.stop();
|
|
3887
|
+
idleMain();
|
|
3888
|
+
assertEquals(132, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
|
|
3889
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3890
|
+
assertNull(ForegroundService.mCurrentNotificationId);
|
|
3891
|
+
assertEquals(2, context.starts.size());
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
@Test
|
|
3895
|
+
@Config(sdk = 28, shadows = RejectBStartForegroundShadowService.class)
|
|
3896
|
+
public void stop_failedUpdateOnRealForegroundInstance_doesNotFenceStopForever() throws Exception {
|
|
3897
|
+
RecordingServiceContext context = useRecordingContext();
|
|
3898
|
+
createChannel();
|
|
3899
|
+
ForegroundService service = buildService().create().get();
|
|
3900
|
+
service.onStartCommand(buildStartIntent("same", 133, "A"), 0, 1);
|
|
3901
|
+
Intent update = buildStartIntent("same", 133, "B");
|
|
3902
|
+
ForegroundService.start(
|
|
3903
|
+
133,
|
|
3904
|
+
update.getParcelableExtra("notification"),
|
|
3905
|
+
update.getBundleExtra("notificationBundle"));
|
|
3906
|
+
assertThrows(
|
|
3907
|
+
SecurityException.class, () -> service.onStartCommand(context.starts.get(0), 0, 2));
|
|
3908
|
+
assertCurrentState("same", 133, "A");
|
|
3909
|
+
ForegroundService.stop();
|
|
3910
|
+
idleMain();
|
|
3911
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
3912
|
+
assertNull(ForegroundService.mCurrentNotificationId);
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
@Test
|
|
3916
|
+
@Config(sdk = 34)
|
|
3917
|
+
public void onTimeout_oldRunOnSameInstance_preservesNewRun() throws Exception {
|
|
3918
|
+
declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
|
|
3919
|
+
createChannel();
|
|
3920
|
+
ForegroundService service = buildService().create().get();
|
|
3921
|
+
service.onStartCommand(
|
|
3922
|
+
buildStartIntent("old", 134, "old", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
|
|
3923
|
+
0,
|
|
3924
|
+
1);
|
|
3925
|
+
service.onStartCommand(buildStopIntent(), 0, 2);
|
|
3926
|
+
service.onStartCommand(
|
|
3927
|
+
buildStartIntent(
|
|
3928
|
+
"new", 135, "new title", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
|
|
3929
|
+
0,
|
|
3930
|
+
3);
|
|
3931
|
+
FgsEventCapture capture = new FgsEventCapture();
|
|
3932
|
+
EventBus.register(capture);
|
|
3933
|
+
try {
|
|
3934
|
+
service.onTimeout(1);
|
|
3935
|
+
assertCurrentState("new", 135, "new title");
|
|
3936
|
+
assertEquals(
|
|
3937
|
+
ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE,
|
|
3938
|
+
ForegroundService.mCurrentForegroundServiceType);
|
|
3939
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
|
|
3940
|
+
assertTrue(capture.events.isEmpty());
|
|
3941
|
+
} finally {
|
|
3942
|
+
EventBus.unregister(capture);
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
@Test
|
|
3947
|
+
@Config(sdk = 34)
|
|
3948
|
+
public void onTimeout_sameRunEarlierStartId_preservesLatestUpdateTimeoutBehavior()
|
|
3949
|
+
throws Exception {
|
|
3950
|
+
declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
|
|
3951
|
+
createChannel();
|
|
3952
|
+
ForegroundService service = buildService().create().get();
|
|
3953
|
+
service.onStartCommand(
|
|
3954
|
+
buildStartIntent("same", 136, "A", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
|
|
3955
|
+
0,
|
|
3956
|
+
1);
|
|
3957
|
+
service.onStartCommand(
|
|
3958
|
+
buildStartIntent("same", 136, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE),
|
|
3959
|
+
0,
|
|
3960
|
+
2);
|
|
3961
|
+
FgsEventCapture capture = new FgsEventCapture();
|
|
3962
|
+
EventBus.register(capture);
|
|
3963
|
+
try {
|
|
3964
|
+
service.onTimeout(1);
|
|
3965
|
+
assertNull(ForegroundService.mCurrentNotificationId);
|
|
3966
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
|
|
3967
|
+
assertEquals(1, capture.events.size());
|
|
3968
|
+
assertEquals("B", capture.events.get(0).getNotification().toBundle().getString("title"));
|
|
3969
|
+
} finally {
|
|
3970
|
+
EventBus.unregister(capture);
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
|
|
3974
|
+
@Test
|
|
3975
|
+
@Config(sdk = 33)
|
|
3976
|
+
public void start_serviceDeliveryBeforeDispatchReturns_canFulfillRealContract() throws Exception {
|
|
3977
|
+
CountDownLatch dispatchEntered = new CountDownLatch(1);
|
|
3978
|
+
CountDownLatch promotionObserved = new CountDownLatch(1);
|
|
3979
|
+
AtomicReference<Boolean> blocked = new AtomicReference<>(false);
|
|
3980
|
+
RecordingServiceContext context =
|
|
3981
|
+
new RecordingServiceContext(RuntimeEnvironment.getApplication()) {
|
|
3982
|
+
@Override
|
|
3983
|
+
public ComponentName startForegroundService(Intent intent) {
|
|
3984
|
+
ComponentName result = super.startForegroundService(intent);
|
|
3985
|
+
dispatchEntered.countDown();
|
|
3986
|
+
try {
|
|
3987
|
+
// The timeout releases the simulated Binder reply even for the broken candidate.
|
|
3988
|
+
blocked.set(!promotionObserved.await(2, TimeUnit.SECONDS));
|
|
3989
|
+
} catch (InterruptedException e) {
|
|
3990
|
+
throw new AssertionError(e);
|
|
3991
|
+
}
|
|
3992
|
+
return result;
|
|
3993
|
+
}
|
|
3994
|
+
};
|
|
3995
|
+
ContextHolder.setApplicationContext(context);
|
|
3996
|
+
createChannel();
|
|
3997
|
+
Notification notification = buildNotification();
|
|
3998
|
+
Bundle bundle = buildNotificationBundle("pending");
|
|
3999
|
+
ExecutorService worker = Executors.newSingleThreadExecutor();
|
|
4000
|
+
try {
|
|
4001
|
+
Future<?> start = worker.submit(() -> ForegroundService.start(137, notification, bundle));
|
|
4002
|
+
assertTrue(dispatchEntered.await(5, TimeUnit.SECONDS));
|
|
4003
|
+
ForegroundService service = buildService().create().get();
|
|
4004
|
+
service.onStartCommand(context.starts.get(0), 0, 1);
|
|
4005
|
+
promotionObserved.countDown();
|
|
4006
|
+
start.get(5, TimeUnit.SECONDS);
|
|
4007
|
+
assertFalse("Service promotion waited for the dispatch caller", blocked.get());
|
|
4008
|
+
assertEquals(
|
|
4009
|
+
137, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
|
|
4010
|
+
} finally {
|
|
4011
|
+
promotionObserved.countDown();
|
|
4012
|
+
worker.shutdownNow();
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
@Test
|
|
4017
|
+
@Config(sdk = 33)
|
|
4018
|
+
public void start_ambiguousDispatchFailure_retainsRealObligationUntilDelivery() {
|
|
4019
|
+
RecordingServiceContext context = useRecordingContext();
|
|
4020
|
+
context.startFailure = new IllegalStateException("Binder outcome unknown");
|
|
4021
|
+
assertThrows(
|
|
4022
|
+
IllegalStateException.class,
|
|
4023
|
+
() ->
|
|
4024
|
+
ForegroundService.start(138, buildNotification(), buildNotificationBundle("pending")));
|
|
4025
|
+
ForegroundService.stop();
|
|
4026
|
+
ForegroundService service = buildService().create().get();
|
|
4027
|
+
service.onStartCommand(null, 0, 1);
|
|
4028
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
4029
|
+
assertNull(org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification());
|
|
4030
|
+
createChannel();
|
|
4031
|
+
service.onStartCommand(context.starts.get(0), 0, 2);
|
|
4032
|
+
idleMain();
|
|
4033
|
+
assertEquals(138, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
|
|
4034
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
@Test
|
|
4038
|
+
@Config(sdk = 33)
|
|
4039
|
+
public void onStartCommand_oldAmbiguousDelivery_preservesNewForegroundRun() throws Exception {
|
|
4040
|
+
RecordingServiceContext context = useRecordingContext();
|
|
4041
|
+
createChannel();
|
|
4042
|
+
context.startFailure = new IllegalStateException("old reply unknown");
|
|
4043
|
+
assertThrows(
|
|
4044
|
+
IllegalStateException.class,
|
|
4045
|
+
() -> ForegroundService.start(139, buildNotification(), buildNotificationBundle("old")));
|
|
4046
|
+
ForegroundService.stop();
|
|
4047
|
+
context.startFailure = null;
|
|
4048
|
+
Intent fresh = buildStartIntent("new", 140, "new title");
|
|
4049
|
+
ForegroundService.start(
|
|
4050
|
+
140, fresh.getParcelableExtra("notification"), fresh.getBundleExtra("notificationBundle"));
|
|
4051
|
+
FgsRunnerCapture capture = new FgsRunnerCapture();
|
|
4052
|
+
EventBus.register(capture);
|
|
4053
|
+
try {
|
|
4054
|
+
ForegroundService service = buildService().create().get();
|
|
4055
|
+
service.onStartCommand(context.starts.get(1), 0, 1);
|
|
4056
|
+
service.onStartCommand(context.starts.get(0), 0, 2);
|
|
4057
|
+
idleMain();
|
|
4058
|
+
assertCurrentState("new", 140, "new title");
|
|
4059
|
+
assertEquals(1, capture.events.size());
|
|
4060
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
4061
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
|
|
4062
|
+
capture.events.get(0).setCompletionResult();
|
|
4063
|
+
assertNull(ForegroundService.mCurrentNotificationId);
|
|
4064
|
+
} finally {
|
|
4065
|
+
EventBus.unregister(capture);
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
@Test
|
|
4070
|
+
@Config(sdk = 33)
|
|
4071
|
+
public void onStartCommand_oldAmbiguousDeliveryAfterNewRunnerEnds_fulfillsThenStopsOnlyOldWork() {
|
|
4072
|
+
RecordingServiceContext context = useRecordingContext();
|
|
4073
|
+
createChannel();
|
|
4074
|
+
context.startFailure = new IllegalStateException("old reply unknown");
|
|
4075
|
+
assertThrows(
|
|
4076
|
+
IllegalStateException.class,
|
|
4077
|
+
() -> ForegroundService.start(141, buildNotification(), buildNotificationBundle("old")));
|
|
4078
|
+
ForegroundService.stop();
|
|
4079
|
+
context.startFailure = null;
|
|
4080
|
+
ForegroundService.start(142, buildNotification(), buildNotificationBundle("new"));
|
|
4081
|
+
FgsRunnerCapture capture = new FgsRunnerCapture();
|
|
4082
|
+
EventBus.register(capture);
|
|
4083
|
+
try {
|
|
4084
|
+
ForegroundService service = buildService().create().get();
|
|
4085
|
+
service.onStartCommand(context.starts.get(1), 0, 1);
|
|
4086
|
+
capture.events.get(0).setCompletionResult();
|
|
4087
|
+
service.onStartCommand(context.starts.get(0), 0, 2);
|
|
4088
|
+
idleMain();
|
|
4089
|
+
assertEquals(
|
|
4090
|
+
141, org.robolectric.Shadows.shadowOf(service).getLastForegroundNotificationId());
|
|
4091
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
4092
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isForegroundStopped());
|
|
4093
|
+
assertNull(ForegroundService.mCurrentNotificationId);
|
|
4094
|
+
assertEquals(1, capture.events.size());
|
|
4095
|
+
} finally {
|
|
4096
|
+
EventBus.unregister(capture);
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
|
|
4100
|
+
@Test
|
|
4101
|
+
@Config(sdk = 33)
|
|
4102
|
+
public void start_lateDispatchReply_doesNotCancelStopAfterRealDelivery() throws Exception {
|
|
4103
|
+
CountDownLatch dispatchEntered = new CountDownLatch(1);
|
|
4104
|
+
CountDownLatch releaseReply = new CountDownLatch(1);
|
|
4105
|
+
RecordingServiceContext context =
|
|
4106
|
+
new RecordingServiceContext(RuntimeEnvironment.getApplication()) {
|
|
4107
|
+
@Override
|
|
4108
|
+
public ComponentName startForegroundService(Intent intent) {
|
|
4109
|
+
ComponentName result = super.startForegroundService(intent);
|
|
4110
|
+
dispatchEntered.countDown();
|
|
4111
|
+
try {
|
|
4112
|
+
assertTrue(releaseReply.await(5, TimeUnit.SECONDS));
|
|
4113
|
+
} catch (InterruptedException e) {
|
|
4114
|
+
throw new AssertionError(e);
|
|
4115
|
+
}
|
|
4116
|
+
return result;
|
|
4117
|
+
}
|
|
4118
|
+
};
|
|
4119
|
+
ContextHolder.setApplicationContext(context);
|
|
4120
|
+
createChannel();
|
|
4121
|
+
ExecutorService worker = Executors.newSingleThreadExecutor();
|
|
4122
|
+
Notification notification = buildNotification();
|
|
4123
|
+
Bundle bundle = buildNotificationBundle("pending");
|
|
4124
|
+
try {
|
|
4125
|
+
Future<?> start = worker.submit(() -> ForegroundService.start(143, notification, bundle));
|
|
4126
|
+
assertTrue(dispatchEntered.await(5, TimeUnit.SECONDS));
|
|
4127
|
+
ForegroundService service = buildService().create().get();
|
|
4128
|
+
service.onStartCommand(context.starts.get(0), 0, 1);
|
|
4129
|
+
ForegroundService.stop();
|
|
4130
|
+
releaseReply.countDown();
|
|
4131
|
+
start.get(5, TimeUnit.SECONDS);
|
|
4132
|
+
idleMain();
|
|
4133
|
+
assertTrue(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
4134
|
+
assertNull(ForegroundService.mCurrentNotificationId);
|
|
4135
|
+
} finally {
|
|
4136
|
+
releaseReply.countDown();
|
|
4137
|
+
worker.shutdownNow();
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
@Test
|
|
4142
|
+
@Config(
|
|
4143
|
+
sdk = {33, 34},
|
|
4144
|
+
shadows = DenyOldNotificationShadowService.class)
|
|
4145
|
+
public void onStartCommand_supersededStartDenial_remainsARealStartFailure() throws Exception {
|
|
4146
|
+
if (Build.VERSION.SDK_INT >= 34) {
|
|
4147
|
+
declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
|
4148
|
+
}
|
|
4149
|
+
RecordingServiceContext context = useRecordingContext();
|
|
4150
|
+
createChannel();
|
|
4151
|
+
Intent old = buildStartIntent("old", 144, "B", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
|
4152
|
+
context.startFailure = new IllegalStateException("old reply unknown");
|
|
4153
|
+
assertThrows(
|
|
4154
|
+
IllegalStateException.class,
|
|
4155
|
+
() ->
|
|
4156
|
+
ForegroundService.start(
|
|
4157
|
+
144,
|
|
4158
|
+
old.getParcelableExtra("notification"),
|
|
4159
|
+
old.getBundleExtra("notificationBundle")));
|
|
4160
|
+
ForegroundService.stop();
|
|
4161
|
+
context.startFailure = null;
|
|
4162
|
+
ForegroundService.start(145, buildNotification(), buildNotificationBundle("new"));
|
|
4163
|
+
FgsRunnerCapture capture = new FgsRunnerCapture();
|
|
4164
|
+
EventBus.register(capture);
|
|
4165
|
+
try {
|
|
4166
|
+
ForegroundService service = buildService().create().get();
|
|
4167
|
+
service.onStartCommand(context.starts.get(1), 0, 1);
|
|
4168
|
+
capture.events.get(0).setCompletionResult();
|
|
4169
|
+
assertThrows(
|
|
4170
|
+
ForegroundServiceStartNotAllowedException.class,
|
|
4171
|
+
() -> service.onStartCommand(context.starts.get(0), 0, 2));
|
|
4172
|
+
idleMain();
|
|
4173
|
+
assertFalse(org.robolectric.Shadows.shadowOf(service).isStoppedBySelf());
|
|
4174
|
+
assertNull(ForegroundService.mCurrentNotificationId);
|
|
4175
|
+
} finally {
|
|
4176
|
+
EventBus.unregister(capture);
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
|
|
4180
|
+
@Implements(Service.class)
|
|
4181
|
+
public static class DenyOldNotificationShadowService extends ShadowService {
|
|
4182
|
+
@Implementation
|
|
4183
|
+
protected void startForeground(int id, Notification notification) {
|
|
4184
|
+
if ("B".equals(notification.extras.getString(Notification.EXTRA_TITLE))) {
|
|
4185
|
+
throw new ForegroundServiceStartNotAllowedException("real old START denied");
|
|
4186
|
+
}
|
|
4187
|
+
super.startForeground(id, notification);
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
|
|
4191
|
+
@Implements(Service.class)
|
|
4192
|
+
public static class RefuseStopSelfShadowService extends ShadowService {
|
|
4193
|
+
@Implementation
|
|
4194
|
+
protected boolean stopSelfResult(int startId) {
|
|
4195
|
+
return false;
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
@Implements(Service.class)
|
|
4200
|
+
public static class FailStopForegroundShadowService extends ShadowService {
|
|
4201
|
+
@Implementation
|
|
4202
|
+
protected void stopForeground(int flags) {
|
|
4203
|
+
throw new ForegroundServiceStartNotAllowedException("not from startForeground");
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
|
|
4207
|
+
private static void assertCurrentState(String id, int hash, String title) throws Exception {
|
|
4208
|
+
assertEquals(id, ForegroundService.mCurrentNotificationId);
|
|
4209
|
+
assertEquals(hash, getPrivateStatic("mCurrentHashCode"));
|
|
4210
|
+
assertEquals(
|
|
4211
|
+
title, ((Bundle) getPrivateStatic("mCurrentNotificationBundle")).getString("title"));
|
|
4212
|
+
assertEquals(
|
|
4213
|
+
title,
|
|
4214
|
+
((Notification) getPrivateStatic("mCurrentNotification"))
|
|
4215
|
+
.extras.getString(Notification.EXTRA_TITLE));
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
private static void idleMain() {
|
|
4219
|
+
org.robolectric.Shadows.shadowOf(Looper.getMainLooper()).idle();
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4222
|
+
private RecordingServiceContext useRecordingContext() {
|
|
4223
|
+
RecordingServiceContext context =
|
|
4224
|
+
new RecordingServiceContext(RuntimeEnvironment.getApplication());
|
|
4225
|
+
ContextHolder.setApplicationContext(context);
|
|
4226
|
+
return context;
|
|
4227
|
+
}
|
|
4228
|
+
|
|
4229
|
+
private ServiceController<ForegroundService> buildService() {
|
|
4230
|
+
ServiceController<ForegroundService> controller =
|
|
4231
|
+
Robolectric.buildService(ForegroundService.class);
|
|
4232
|
+
controllers.add(controller);
|
|
4233
|
+
return controller;
|
|
4234
|
+
}
|
|
4235
|
+
|
|
4236
|
+
private static class RecordingServiceContext extends ContextWrapper {
|
|
4237
|
+
final List<Intent> starts = new ArrayList<>();
|
|
4238
|
+
int stops;
|
|
4239
|
+
RuntimeException startFailure;
|
|
4240
|
+
|
|
4241
|
+
RecordingServiceContext(Context base) {
|
|
4242
|
+
super(base);
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
@Override
|
|
4246
|
+
public ComponentName startService(Intent intent) {
|
|
4247
|
+
starts.add(new Intent(intent));
|
|
4248
|
+
return intent.getComponent();
|
|
4249
|
+
}
|
|
4250
|
+
|
|
4251
|
+
@Override
|
|
4252
|
+
public ComponentName startForegroundService(Intent intent) {
|
|
4253
|
+
starts.add(new Intent(intent));
|
|
4254
|
+
if (startFailure != null) {
|
|
4255
|
+
throw startFailure;
|
|
4256
|
+
}
|
|
4257
|
+
return intent.getComponent();
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
@Override
|
|
4261
|
+
public boolean stopService(Intent intent) {
|
|
4262
|
+
stops++;
|
|
4263
|
+
return true;
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
@Implements(Service.class)
|
|
4268
|
+
public static class DefensiveFailureShadowService extends ShadowService {
|
|
4269
|
+
static RuntimeException failure;
|
|
4270
|
+
static int attempts;
|
|
4271
|
+
static int lastId;
|
|
4272
|
+
static int lastType;
|
|
4273
|
+
|
|
4274
|
+
@Implementation
|
|
4275
|
+
protected void startForeground(int id, Notification notification) {
|
|
4276
|
+
attempts++;
|
|
4277
|
+
lastId = id;
|
|
4278
|
+
lastType = ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE;
|
|
4279
|
+
throw failure;
|
|
4280
|
+
}
|
|
4281
|
+
|
|
4282
|
+
@Implementation
|
|
4283
|
+
protected void startForeground(int id, Notification notification, int foregroundServiceType) {
|
|
4284
|
+
attempts++;
|
|
4285
|
+
lastId = id;
|
|
4286
|
+
lastType = foregroundServiceType;
|
|
4287
|
+
throw failure;
|
|
4288
|
+
}
|
|
4289
|
+
}
|
|
4290
|
+
|
|
4291
|
+
@Implements(NotificationManager.class)
|
|
4292
|
+
public static class SecurityDefensiveChannelShadowManager extends ShadowNotificationManager {
|
|
4293
|
+
@Implementation
|
|
4294
|
+
protected void createNotificationChannel(NotificationChannel channel) {
|
|
4295
|
+
if ("notifee_fg_default".equals(channel.getId())) {
|
|
4296
|
+
throw new SecurityException("defensive channel failure");
|
|
4297
|
+
}
|
|
4298
|
+
super.createNotificationChannel(channel);
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
|
|
4302
|
+
@Implements(Service.class)
|
|
4303
|
+
public static class SecurityStopForegroundShadowService extends ShadowService {
|
|
4304
|
+
@Implementation
|
|
4305
|
+
protected void stopForeground(int flags) {
|
|
4306
|
+
throw new SecurityException("defensive teardown failure");
|
|
4307
|
+
}
|
|
62
4308
|
}
|
|
63
4309
|
|
|
64
4310
|
/**
|
|
@@ -68,8 +4314,7 @@ public class ForegroundServiceTest {
|
|
|
68
4314
|
*/
|
|
69
4315
|
@Test
|
|
70
4316
|
public void onStartCommand_stopIntentBeforeStart_doesNotCrash() {
|
|
71
|
-
ServiceController<ForegroundService> controller =
|
|
72
|
-
Robolectric.buildService(ForegroundService.class);
|
|
4317
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
73
4318
|
ForegroundService service = controller.create().get();
|
|
74
4319
|
|
|
75
4320
|
Intent stopIntent = new Intent();
|
|
@@ -86,8 +4331,7 @@ public class ForegroundServiceTest {
|
|
|
86
4331
|
*/
|
|
87
4332
|
@Test
|
|
88
4333
|
public void onStartCommand_nullIntent_doesNotCrash() {
|
|
89
|
-
ServiceController<ForegroundService> controller =
|
|
90
|
-
Robolectric.buildService(ForegroundService.class);
|
|
4334
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
91
4335
|
ForegroundService service = controller.create().get();
|
|
92
4336
|
|
|
93
4337
|
// Null intent simulates service recreation after process kill
|
|
@@ -103,8 +4347,7 @@ public class ForegroundServiceTest {
|
|
|
103
4347
|
*/
|
|
104
4348
|
@Test
|
|
105
4349
|
public void onStartCommand_stopIntent_resetsPublicStaticState() {
|
|
106
|
-
ServiceController<ForegroundService> controller =
|
|
107
|
-
Robolectric.buildService(ForegroundService.class);
|
|
4350
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
108
4351
|
ForegroundService service = controller.create().get();
|
|
109
4352
|
|
|
110
4353
|
// Set some stale state to simulate a prior invocation
|
|
@@ -132,8 +4375,7 @@ public class ForegroundServiceTest {
|
|
|
132
4375
|
public void onStartCommand_stopIntentApi34NoManifestType_throwsRuntimeException() {
|
|
133
4376
|
// Robolectric's default shadow PackageManager returns FOREGROUND_SERVICE_TYPE_NONE (0)
|
|
134
4377
|
// for services without an explicit foregroundServiceType in the test manifest.
|
|
135
|
-
ServiceController<ForegroundService> controller =
|
|
136
|
-
Robolectric.buildService(ForegroundService.class);
|
|
4378
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
137
4379
|
ForegroundService service = controller.create().get();
|
|
138
4380
|
|
|
139
4381
|
Intent stopIntent = new Intent();
|
|
@@ -148,8 +4390,7 @@ public class ForegroundServiceTest {
|
|
|
148
4390
|
@Test
|
|
149
4391
|
@Config(sdk = 34)
|
|
150
4392
|
public void onStartCommand_stopIntentApi34NoManifestType_messageContainsDocUrl() {
|
|
151
|
-
ServiceController<ForegroundService> controller =
|
|
152
|
-
Robolectric.buildService(ForegroundService.class);
|
|
4393
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
153
4394
|
ForegroundService service = controller.create().get();
|
|
154
4395
|
|
|
155
4396
|
Intent stopIntent = new Intent();
|
|
@@ -171,8 +4412,7 @@ public class ForegroundServiceTest {
|
|
|
171
4412
|
@Test
|
|
172
4413
|
@Config(sdk = 33)
|
|
173
4414
|
public void onStartCommand_stopIntentApiBelow34_defensivePathRunsNormally() {
|
|
174
|
-
ServiceController<ForegroundService> controller =
|
|
175
|
-
Robolectric.buildService(ForegroundService.class);
|
|
4415
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
176
4416
|
ForegroundService service = controller.create().get();
|
|
177
4417
|
|
|
178
4418
|
Intent stopIntent = new Intent();
|
|
@@ -192,8 +4432,7 @@ public class ForegroundServiceTest {
|
|
|
192
4432
|
@Test
|
|
193
4433
|
@Config(sdk = 33)
|
|
194
4434
|
public void onStartCommand_stopIntentAfterSuccessfulStart_skipsDefensivePath() {
|
|
195
|
-
ServiceController<ForegroundService> controller =
|
|
196
|
-
Robolectric.buildService(ForegroundService.class);
|
|
4435
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
197
4436
|
ForegroundService service = controller.create().get();
|
|
198
4437
|
|
|
199
4438
|
Intent stopIntent = new Intent();
|
|
@@ -214,8 +4453,7 @@ public class ForegroundServiceTest {
|
|
|
214
4453
|
declareForegroundServiceTypes(
|
|
215
4454
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
|
216
4455
|
| ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
|
217
|
-
ServiceController<ForegroundService> controller =
|
|
218
|
-
Robolectric.buildService(ForegroundService.class);
|
|
4456
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
219
4457
|
ForegroundService service = controller.create().get();
|
|
220
4458
|
|
|
221
4459
|
int result = service.onStartCommand(buildStopIntent(), 0, 1);
|
|
@@ -231,8 +4469,7 @@ public class ForegroundServiceTest {
|
|
|
231
4469
|
throws Exception {
|
|
232
4470
|
declareForegroundServiceTypes(
|
|
233
4471
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA | ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
|
234
|
-
ServiceController<ForegroundService> controller =
|
|
235
|
-
Robolectric.buildService(ForegroundService.class);
|
|
4472
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
236
4473
|
ForegroundService service = controller.create().get();
|
|
237
4474
|
|
|
238
4475
|
int result = service.onStartCommand(buildStopIntent(), 0, 1);
|
|
@@ -250,8 +4487,7 @@ public class ForegroundServiceTest {
|
|
|
250
4487
|
declareForegroundServiceTypes(
|
|
251
4488
|
ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
|
|
252
4489
|
| ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
|
|
253
|
-
ServiceController<ForegroundService> controller =
|
|
254
|
-
Robolectric.buildService(ForegroundService.class);
|
|
4490
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
255
4491
|
ForegroundService service = controller.create().get();
|
|
256
4492
|
|
|
257
4493
|
int result = service.onStartCommand(buildStopIntent(), 0, 1);
|
|
@@ -266,8 +4502,7 @@ public class ForegroundServiceTest {
|
|
|
266
4502
|
public void onStartCommand_stopIntentApi34MicrophoneOnly_doesNotUseUndeclaredShortService()
|
|
267
4503
|
throws Exception {
|
|
268
4504
|
declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
|
|
269
|
-
ServiceController<ForegroundService> controller =
|
|
270
|
-
Robolectric.buildService(ForegroundService.class);
|
|
4505
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
271
4506
|
ForegroundService service = controller.create().get();
|
|
272
4507
|
|
|
273
4508
|
int result = service.onStartCommand(buildStopIntent(), 0, 1);
|
|
@@ -281,8 +4516,7 @@ public class ForegroundServiceTest {
|
|
|
281
4516
|
@Config(sdk = 34, shadows = ThrowingStartForegroundShadowService.class)
|
|
282
4517
|
public void onStartCommand_stopIntentApi34SecurityException_doesNotCrash() throws Exception {
|
|
283
4518
|
declareForegroundServiceTypes(ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
|
|
284
|
-
ServiceController<ForegroundService> controller =
|
|
285
|
-
Robolectric.buildService(ForegroundService.class);
|
|
4519
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
286
4520
|
ForegroundService service = controller.create().get();
|
|
287
4521
|
|
|
288
4522
|
int result = service.onStartCommand(buildStopIntent(), 0, 1);
|
|
@@ -308,8 +4542,7 @@ public class ForegroundServiceTest {
|
|
|
308
4542
|
public void onStartCommand_startIntent_setsCurrentNotificationIdAndCallsStartForeground()
|
|
309
4543
|
throws Exception {
|
|
310
4544
|
createChannel();
|
|
311
|
-
ServiceController<ForegroundService> controller =
|
|
312
|
-
Robolectric.buildService(ForegroundService.class);
|
|
4545
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
313
4546
|
ForegroundService service = controller.create().get();
|
|
314
4547
|
|
|
315
4548
|
String id = "fgs-start-happy";
|
|
@@ -814,12 +5047,12 @@ public class ForegroundServiceTest {
|
|
|
814
5047
|
@Test
|
|
815
5048
|
@Config(sdk = 34)
|
|
816
5049
|
public void onTimeout_api34_emitsFgTimeoutEventWithStartIdAndSentinelFgsType() throws Exception {
|
|
817
|
-
ServiceController<ForegroundService> controller =
|
|
818
|
-
Robolectric.buildService(ForegroundService.class);
|
|
5050
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
819
5051
|
ForegroundService service = controller.create().get();
|
|
820
5052
|
|
|
821
5053
|
String id = "fgs-timeout-api34";
|
|
822
|
-
seedActiveForegroundServiceState(
|
|
5054
|
+
seedActiveForegroundServiceState(
|
|
5055
|
+
service, id, 42, ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
|
|
823
5056
|
|
|
824
5057
|
FgsEventCapture capture = new FgsEventCapture();
|
|
825
5058
|
EventBus.register(capture);
|
|
@@ -857,12 +5090,11 @@ public class ForegroundServiceTest {
|
|
|
857
5090
|
@Test
|
|
858
5091
|
@Config(sdk = 35)
|
|
859
5092
|
public void onTimeout_api35_emitsFgTimeoutEventWithExplicitFgsType() throws Exception {
|
|
860
|
-
ServiceController<ForegroundService> controller =
|
|
861
|
-
Robolectric.buildService(ForegroundService.class);
|
|
5093
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
862
5094
|
ForegroundService service = controller.create().get();
|
|
863
5095
|
|
|
864
5096
|
String id = "fgs-timeout-api35";
|
|
865
|
-
seedActiveForegroundServiceState(id);
|
|
5097
|
+
seedActiveForegroundServiceState(service, id, 7, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
|
866
5098
|
|
|
867
5099
|
FgsEventCapture capture = new FgsEventCapture();
|
|
868
5100
|
EventBus.register(capture);
|
|
@@ -894,8 +5126,7 @@ public class ForegroundServiceTest {
|
|
|
894
5126
|
@Test
|
|
895
5127
|
@Config(sdk = 34)
|
|
896
5128
|
public void onTimeout_withNoActiveState_doesNotCrashOrEmitEvent() {
|
|
897
|
-
ServiceController<ForegroundService> controller =
|
|
898
|
-
Robolectric.buildService(ForegroundService.class);
|
|
5129
|
+
ServiceController<ForegroundService> controller = buildService();
|
|
899
5130
|
ForegroundService service = controller.create().get();
|
|
900
5131
|
|
|
901
5132
|
FgsEventCapture capture = new FgsEventCapture();
|
|
@@ -1011,19 +5242,12 @@ public class ForegroundServiceTest {
|
|
|
1011
5242
|
return (int) method.invoke(shadowService);
|
|
1012
5243
|
}
|
|
1013
5244
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
*/
|
|
1021
|
-
private static void seedActiveForegroundServiceState(String id) throws Exception {
|
|
1022
|
-
ForegroundService.mCurrentNotificationId = id;
|
|
1023
|
-
ForegroundService.mCurrentForegroundServiceType = 1;
|
|
1024
|
-
setPrivateStatic("mCurrentNotificationBundle", buildNotificationBundle(id));
|
|
1025
|
-
setPrivateStatic("mCurrentNotification", buildNotification());
|
|
1026
|
-
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);
|
|
1027
5251
|
}
|
|
1028
5252
|
|
|
1029
5253
|
private static void setPrivateStatic(String name, Object value) throws Exception {
|