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