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