react-native-notify-kit 10.8.0 → 10.9.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/README.md +34 -17
- package/android/build.gradle +50 -4
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/app/notifee/core/ForegroundService.java +1002 -58
- package/android/src/main/java/app/notifee/core/ForegroundServiceRunLimitReceiver.java +15 -0
- package/android/src/main/java/app/notifee/core/ForegroundServiceRunLimitScheduler.java +461 -0
- package/android/src/main/java/app/notifee/core/NotificationManager.java +6 -0
- package/android/src/main/java/app/notifee/core/event/NotificationEvent.java +6 -0
- package/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java +29 -0
- package/android/src/main/kotlin/io/invertase/notifee/NotifeeEventSubscriber.kt +3 -0
- package/android/src/test/java/app/notifee/core/ForegroundServiceRunLimitTest.java +2357 -0
- package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +3093 -21
- package/android/src/test/java/app/notifee/core/ShadowServiceCompatibilityTest.java +170 -0
- package/android/src/test/resources/robolectric.properties +4 -0
- package/cli/dist/cli.js +0 -1
- package/cli/dist/commands/initNse.js +1 -2
- package/cli/dist/lib/detectProject.js +1 -2
- package/cli/dist/lib/initNseCore.js +0 -1
- package/cli/dist/lib/logger.js +0 -1
- package/cli/dist/lib/patchPodfile.js +0 -1
- package/cli/dist/lib/patchXcodeProject.js +22 -3
- package/cli/dist/lib/writeTemplates.js +0 -1
- package/cli/dist/lib/xcode.js +6 -0
- package/cli/dist/templates/NotificationService.swift.tmpl +74 -6
- package/cli/package.json +1 -0
- package/cli/vendor/xcode/AUTHORS +6 -0
- package/cli/vendor/xcode/LICENSE +202 -0
- package/cli/vendor/xcode/NOTICE +5 -0
- package/cli/vendor/xcode/README.md +32 -0
- package/cli/vendor/xcode/UPSTREAM-SHA256.json +12 -0
- package/cli/vendor/xcode/index.js +18 -0
- package/cli/vendor/xcode/lib/parseJob.js +32 -0
- package/cli/vendor/xcode/lib/parser/pbxproj.js +1904 -0
- package/cli/vendor/xcode/lib/parser/pbxproj.pegjs +280 -0
- package/cli/vendor/xcode/lib/pbxFile.js +233 -0
- package/cli/vendor/xcode/lib/pbxProject.js +2205 -0
- package/cli/vendor/xcode/lib/pbxWriter.js +309 -0
- package/dist/NotifeeNativeModule.web.d.ts +2 -2
- package/dist/types/Notification.d.ts +36 -1
- package/dist/types/Notification.js +13 -0
- package/dist/types/Notification.js.map +1 -1
- package/dist/types/NotificationAndroid.d.ts +9 -0
- package/dist/types/NotificationAndroid.js.map +1 -1
- package/dist/validators/validateAndroidNotification.js +26 -1
- package/dist/validators/validateAndroidNotification.js.map +1 -1
- package/dist/validators/validateAndroidStyle.js +6 -0
- package/dist/validators/validateAndroidStyle.js.map +1 -1
- package/dist/validators/validateIOSAttachment.js +3 -1
- package/dist/validators/validateIOSAttachment.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -5
- package/src/NotifeeNativeModule.web.ts +2 -2
- package/src/types/Notification.ts +40 -0
- package/src/types/NotificationAndroid.ts +10 -0
- package/src/validators/validateAndroidNotification.ts +31 -1
- package/src/validators/validateAndroidStyle.ts +10 -2
- package/src/validators/validateIOSAttachment.ts +5 -1
- package/src/version.ts +1 -1
- package/cli/dist/cli.bundle.js +0 -20834
- package/cli/dist/cli.d.ts +0 -2
- package/cli/dist/cli.js.map +0 -1
- package/cli/dist/commands/initNse.d.ts +0 -12
- package/cli/dist/commands/initNse.js.map +0 -1
- package/cli/dist/lib/detectProject.d.ts +0 -12
- package/cli/dist/lib/detectProject.js.map +0 -1
- package/cli/dist/lib/initNseCore.d.ts +0 -8
- package/cli/dist/lib/initNseCore.js.map +0 -1
- package/cli/dist/lib/logger.d.ts +0 -4
- package/cli/dist/lib/logger.js.map +0 -1
- package/cli/dist/lib/patchPodfile.d.ts +0 -23
- package/cli/dist/lib/patchPodfile.js.map +0 -1
- package/cli/dist/lib/patchXcodeProject.d.ts +0 -41
- package/cli/dist/lib/patchXcodeProject.js.map +0 -1
- package/cli/dist/lib/writeTemplates.d.ts +0 -11
- package/cli/dist/lib/writeTemplates.js.map +0 -1
- package/cli/dist/templates/templates/Info.plist.tmpl +0 -29
- package/cli/dist/templates/templates/NotificationService.swift.tmpl +0 -100
- package/cli/dist/templates/templates/NotifyKitNSE.entitlements.tmpl +0 -6
|
@@ -0,0 +1,2357 @@
|
|
|
1
|
+
package app.notifee.core;
|
|
2
|
+
|
|
3
|
+
import static org.junit.Assert.*;
|
|
4
|
+
import static org.robolectric.Shadows.shadowOf;
|
|
5
|
+
|
|
6
|
+
import android.app.*;
|
|
7
|
+
import android.content.*;
|
|
8
|
+
import android.content.pm.*;
|
|
9
|
+
import android.os.*;
|
|
10
|
+
import android.provider.Settings;
|
|
11
|
+
import androidx.core.app.NotificationCompat;
|
|
12
|
+
import java.lang.reflect.*;
|
|
13
|
+
import java.time.Duration;
|
|
14
|
+
import java.util.*;
|
|
15
|
+
import org.junit.*;
|
|
16
|
+
import org.junit.runner.RunWith;
|
|
17
|
+
import org.robolectric.*;
|
|
18
|
+
import org.robolectric.android.controller.ServiceController;
|
|
19
|
+
import org.robolectric.annotation.*;
|
|
20
|
+
import org.robolectric.shadows.*;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Causal host oracle: acceptance, delivery, promotion, alarm registration and stop are separate.
|
|
24
|
+
*/
|
|
25
|
+
@RunWith(RobolectricTestRunner.class)
|
|
26
|
+
@Config(
|
|
27
|
+
sdk = 35,
|
|
28
|
+
shadows = {
|
|
29
|
+
ForegroundServiceRunLimitTest.Platform.class,
|
|
30
|
+
ForegroundServiceRunLimitTest.Alarms.class
|
|
31
|
+
})
|
|
32
|
+
public class ForegroundServiceRunLimitTest {
|
|
33
|
+
static final int DATA = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC;
|
|
34
|
+
static final String RECEIVER = "app.notifee.core.ForegroundServiceRunLimitReceiver";
|
|
35
|
+
static final String ACTION = "app.notifee.core.FGS_RUN_LIMIT_EXPIRE";
|
|
36
|
+
Context app;
|
|
37
|
+
Sender sender;
|
|
38
|
+
final List<ServiceController<? extends ForegroundService>> controllers = new ArrayList<>();
|
|
39
|
+
|
|
40
|
+
@Before
|
|
41
|
+
public void setup() throws Exception {
|
|
42
|
+
app = RuntimeEnvironment.getApplication();
|
|
43
|
+
app.getApplicationInfo().flags |= ApplicationInfo.FLAG_DEBUGGABLE;
|
|
44
|
+
Settings.Global.putInt(app.getContentResolver(), Settings.Global.BOOT_COUNT, 42);
|
|
45
|
+
ServiceInfo info = new ServiceInfo();
|
|
46
|
+
info.packageName = app.getPackageName();
|
|
47
|
+
info.name = ForegroundService.class.getName();
|
|
48
|
+
info.processName = app.getPackageName();
|
|
49
|
+
Field type = ServiceInfo.class.getDeclaredField("mForegroundServiceType");
|
|
50
|
+
type.setAccessible(true);
|
|
51
|
+
type.setInt(info, DATA | ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
|
52
|
+
shadowOf(app.getPackageManager()).addOrUpdateService(info);
|
|
53
|
+
ActivityInfo receiver = new ActivityInfo();
|
|
54
|
+
receiver.name = RECEIVER;
|
|
55
|
+
receiver.packageName = app.getPackageName();
|
|
56
|
+
receiver.processName = app.getPackageName();
|
|
57
|
+
receiver.exported = false;
|
|
58
|
+
receiver.enabled = true;
|
|
59
|
+
shadowOf(app.getPackageManager()).addOrUpdateReceiver(receiver);
|
|
60
|
+
app.getSystemService(android.app.NotificationManager.class)
|
|
61
|
+
.createNotificationChannel(
|
|
62
|
+
new NotificationChannel(
|
|
63
|
+
"alarm-poc", "PoC", android.app.NotificationManager.IMPORTANCE_LOW));
|
|
64
|
+
sender = new Sender(app);
|
|
65
|
+
ContextHolder.setApplicationContext(sender);
|
|
66
|
+
Alarms.reset();
|
|
67
|
+
Platform.reset();
|
|
68
|
+
Preferences.resetFaults();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@After
|
|
72
|
+
public void cleanup() throws Exception {
|
|
73
|
+
for (int i = controllers.size() - 1; i >= 0; i--) controllers.get(i).destroy();
|
|
74
|
+
((Map<?, ?>) field(null, "sPendingStarts")).clear();
|
|
75
|
+
set(null, "sStopRequest", null);
|
|
76
|
+
set(null, "sRunGeneration", 0L);
|
|
77
|
+
set(null, "sInstance", null);
|
|
78
|
+
set(null, "sStateOwner", null);
|
|
79
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
80
|
+
ContextHolder.setApplicationContext(app);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static Object field(Object owner, String name) throws Exception {
|
|
84
|
+
Field f = ForegroundService.class.getDeclaredField(name);
|
|
85
|
+
f.setAccessible(true);
|
|
86
|
+
return f.get(owner);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
static void set(Object owner, String name, Object value) throws Exception {
|
|
90
|
+
Field f = ForegroundService.class.getDeclaredField(name);
|
|
91
|
+
f.setAccessible(true);
|
|
92
|
+
f.set(owner, value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
Bundle bundle(String id, int type) {
|
|
96
|
+
Bundle b = new Bundle();
|
|
97
|
+
b.putString("id", id);
|
|
98
|
+
b.putString("title", "latest-" + id);
|
|
99
|
+
Bundle a = new Bundle();
|
|
100
|
+
a.putString("channelId", "alarm-poc");
|
|
101
|
+
a.putIntegerArrayList("foregroundServiceTypes", new ArrayList<>(Arrays.asList(type)));
|
|
102
|
+
b.putBundle("android", a);
|
|
103
|
+
return b;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
Notification notification() {
|
|
107
|
+
return new NotificationCompat.Builder(app, "alarm-poc")
|
|
108
|
+
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
|
109
|
+
.setContentTitle("PoC")
|
|
110
|
+
.build();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
void request(String id, boolean opt) throws Exception {
|
|
114
|
+
if (!opt) {
|
|
115
|
+
ForegroundService.start(id.hashCode(), notification(), bundle(id, DATA));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// Baseline adapter intentionally runs the ordinary START when the experimental entrypoint
|
|
119
|
+
// is absent. RED is missing alarm/teardown behavior, never a compile error.
|
|
120
|
+
try {
|
|
121
|
+
Method m =
|
|
122
|
+
ForegroundService.class.getDeclaredMethod(
|
|
123
|
+
"startAlarmPoc",
|
|
124
|
+
int.class,
|
|
125
|
+
Notification.class,
|
|
126
|
+
Bundle.class,
|
|
127
|
+
String.class,
|
|
128
|
+
long.class);
|
|
129
|
+
m.setAccessible(true);
|
|
130
|
+
m.invoke(
|
|
131
|
+
null,
|
|
132
|
+
id.hashCode(),
|
|
133
|
+
notification(),
|
|
134
|
+
bundle(id, DATA),
|
|
135
|
+
UUID.randomUUID().toString(),
|
|
136
|
+
1000L);
|
|
137
|
+
} catch (NoSuchMethodException absent) {
|
|
138
|
+
ForegroundService.start(id.hashCode(), notification(), bundle(id, DATA));
|
|
139
|
+
} catch (InvocationTargetException e) {
|
|
140
|
+
throw (RuntimeException) e.getCause();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
ForegroundService create() {
|
|
145
|
+
ServiceController<? extends ForegroundService> c =
|
|
146
|
+
Preferences.enabled || Preferences.failCommit > 0
|
|
147
|
+
? Robolectric.buildService(StorageService.class)
|
|
148
|
+
: Robolectric.buildService(ForegroundService.class);
|
|
149
|
+
controllers.add(c);
|
|
150
|
+
return c.create().get();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
void deliver(ForegroundService s, Intent i, int id) {
|
|
154
|
+
Platform p = (Platform) shadowOf(s);
|
|
155
|
+
p.accepted = Math.max(p.accepted, id);
|
|
156
|
+
p.delivered = id;
|
|
157
|
+
s.onStartCommand(i, 0, id);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
ForegroundService armed(String id) throws Exception {
|
|
161
|
+
request(id, true);
|
|
162
|
+
ForegroundService s = create();
|
|
163
|
+
deliver(s, sender.starts.get(sender.starts.size() - 1), 1);
|
|
164
|
+
assertEquals(
|
|
165
|
+
"one actual AlarmManager registration: "
|
|
166
|
+
+ ShadowLog.getLogsForTag("ForegroundServiceRunLimit"),
|
|
167
|
+
1,
|
|
168
|
+
Alarms.scheduled.size());
|
|
169
|
+
return s;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
Intent alarm() {
|
|
173
|
+
assertFalse("alarm must have actually been scheduled", Alarms.scheduled.isEmpty());
|
|
174
|
+
return new Intent(shadowOf(Alarms.scheduled.get(Alarms.scheduled.size() - 1)).getSavedIntent());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
void receive(Intent i) throws Exception {
|
|
178
|
+
try {
|
|
179
|
+
((BroadcastReceiver) Class.forName(RECEIVER).getDeclaredConstructor().newInstance())
|
|
180
|
+
.onReceive(app, i);
|
|
181
|
+
} catch (ClassNotFoundException baseline) {
|
|
182
|
+
/* no baseline delivery path */
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
void expire() throws Exception {
|
|
187
|
+
SystemClock.sleep(1100);
|
|
188
|
+
receive(alarm());
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
Platform platform(ForegroundService s) {
|
|
192
|
+
return (Platform) shadowOf(s);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
void noTeardown(ForegroundService s) {
|
|
196
|
+
assertEquals(0, platform(s).demotions);
|
|
197
|
+
assertEquals(0, platform(s).stopCalls);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
boolean marker(String marker) {
|
|
201
|
+
return ShadowLog.getLogsForTag("ForegroundServiceRunLimit").stream()
|
|
202
|
+
.anyMatch(l -> l.msg.contains(marker));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@Test
|
|
206
|
+
public void T01_defaultDisabled() throws Exception {
|
|
207
|
+
request("a", false);
|
|
208
|
+
deliver(create(), sender.starts.get(0), 1);
|
|
209
|
+
assertEquals(0, Alarms.requested);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
@Test
|
|
213
|
+
public void T02_onlyAfterSuccessfulPromotion() throws Exception {
|
|
214
|
+
request("a", true);
|
|
215
|
+
assertEquals(0, Alarms.requested);
|
|
216
|
+
ForegroundService s = create();
|
|
217
|
+
Platform.beforePromotion = () -> assertEquals(0, Alarms.requested);
|
|
218
|
+
deliver(s, sender.starts.get(0), 1);
|
|
219
|
+
assertEquals(1, Alarms.requested);
|
|
220
|
+
assertEquals(1, Alarms.scheduled.size());
|
|
221
|
+
assertEquals(AlarmManager.ELAPSED_REALTIME_WAKEUP, Alarms.type);
|
|
222
|
+
assertTrue(marker("ALARM_ARMED"));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@Test
|
|
226
|
+
public void T03_deniedPromotionNeverArms() throws Exception {
|
|
227
|
+
request("a", true);
|
|
228
|
+
ForegroundService s = create();
|
|
229
|
+
Platform.promotionFailure = new ForegroundServiceStartNotAllowedException("denied");
|
|
230
|
+
deliver(s, sender.starts.get(0), 1);
|
|
231
|
+
assertEquals(0, Alarms.requested);
|
|
232
|
+
assertEquals(1, platform(s).stopCalls);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@Test
|
|
236
|
+
public void T04_elapsedClock() throws Exception {
|
|
237
|
+
ShadowSystemClock.advanceBy(Duration.ofSeconds(5));
|
|
238
|
+
long before = SystemClock.elapsedRealtime();
|
|
239
|
+
armed("a");
|
|
240
|
+
assertEquals(before + 1000, Alarms.deadline);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@Test
|
|
244
|
+
public void T05_deepSleepCounts() throws Exception {
|
|
245
|
+
ForegroundService s = armed("a");
|
|
246
|
+
long uptime = SystemClock.uptimeMillis();
|
|
247
|
+
ShadowSystemClock.simulateDeepSleep(Duration.ofMillis(1100));
|
|
248
|
+
assertEquals(uptime, SystemClock.uptimeMillis());
|
|
249
|
+
receive(alarm());
|
|
250
|
+
assertEquals(1, platform(s).demotions);
|
|
251
|
+
assertEquals(1, platform(s).stopCalls);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
@Test
|
|
255
|
+
public void T06_sameIdPreservesDeadlineAndLatestState() throws Exception {
|
|
256
|
+
ForegroundService s = armed("a");
|
|
257
|
+
long first = Alarms.deadline;
|
|
258
|
+
SystemClock.sleep(100);
|
|
259
|
+
request("a", false);
|
|
260
|
+
deliver(s, sender.starts.get(1), 2);
|
|
261
|
+
assertEquals(1, Alarms.requested);
|
|
262
|
+
assertEquals(first, Alarms.deadline);
|
|
263
|
+
assertEquals(
|
|
264
|
+
"latest-a", ((Bundle) field(null, "mCurrentNotificationBundle")).getString("title"));
|
|
265
|
+
expire();
|
|
266
|
+
assertEquals(2, platform(s).lastStopId);
|
|
267
|
+
assertFalse(ForegroundService.repostIfActive("a"));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@Test
|
|
271
|
+
public void T07_uniqueImmutableExplicitPendingIntent() throws Exception {
|
|
272
|
+
ForegroundService s = armed("a");
|
|
273
|
+
PendingIntent a = Alarms.scheduled.get(0);
|
|
274
|
+
Intent old = alarm();
|
|
275
|
+
ForegroundService.stop();
|
|
276
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
277
|
+
request("a", true);
|
|
278
|
+
deliver(s, sender.starts.get(1), 2);
|
|
279
|
+
PendingIntent b = Alarms.scheduled.get(1);
|
|
280
|
+
assertNotEquals(a, b);
|
|
281
|
+
assertTrue(a.isImmutable());
|
|
282
|
+
assertTrue(a.isBroadcast());
|
|
283
|
+
assertEquals(new ComponentName(app, RECEIVER), old.getComponent());
|
|
284
|
+
assertEquals(ACTION, old.getAction());
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
@Test
|
|
288
|
+
public void T08_hashCollisionCannotAliasAlarm() throws Exception {
|
|
289
|
+
assertEquals("Aa".hashCode(), "BB".hashCode());
|
|
290
|
+
ForegroundService s = armed("Aa");
|
|
291
|
+
PendingIntent a = Alarms.scheduled.get(0);
|
|
292
|
+
ForegroundService.stop();
|
|
293
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
294
|
+
request("BB", true);
|
|
295
|
+
deliver(s, sender.starts.get(1), 2);
|
|
296
|
+
assertNotEquals(a, Alarms.scheduled.get(1));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
@Test
|
|
300
|
+
public void T09_invalidEnvelope() throws Exception {
|
|
301
|
+
ForegroundService s = armed("a");
|
|
302
|
+
SystemClock.sleep(1100);
|
|
303
|
+
receive(new Intent(alarm()).setAction("bad"));
|
|
304
|
+
receive(new Intent(alarm()).putExtra("token", "bad"));
|
|
305
|
+
receive(new Intent(alarm()).putExtra("schema", 999));
|
|
306
|
+
receive(new Intent(alarm()).setData(android.net.Uri.parse("bad://token")));
|
|
307
|
+
noTeardown(s);
|
|
308
|
+
receive(alarm());
|
|
309
|
+
assertEquals(1, platform(s).demotions);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
@Test
|
|
313
|
+
public void T10_earlyDeliveryDoesNotConsume() throws Exception {
|
|
314
|
+
ForegroundService s = armed("a");
|
|
315
|
+
receive(alarm());
|
|
316
|
+
noTeardown(s);
|
|
317
|
+
assertTrue(marker("ALARM_EARLY"));
|
|
318
|
+
expire();
|
|
319
|
+
assertEquals(1, platform(s).demotions);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
@Test
|
|
323
|
+
public void T11_lateDeliveryUsesCurrentOwner() throws Exception {
|
|
324
|
+
ForegroundService s = armed("a");
|
|
325
|
+
SystemClock.sleep(300000);
|
|
326
|
+
receive(alarm());
|
|
327
|
+
assertEquals(Arrays.asList("demote", "stop:1"), platform(s).order);
|
|
328
|
+
assertTrue(marker("ALARM_LATE"));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
@Test
|
|
332
|
+
public void T12_duplicateDeliveryRetiresOnce() throws Exception {
|
|
333
|
+
ForegroundService s = armed("a");
|
|
334
|
+
Intent i = alarm();
|
|
335
|
+
SystemClock.sleep(1100);
|
|
336
|
+
receive(i);
|
|
337
|
+
receive(i);
|
|
338
|
+
assertEquals(1, platform(s).demotions);
|
|
339
|
+
assertEquals(1, platform(s).stopCalls);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
@Test
|
|
343
|
+
public void T13_oldAlarmCannotStopSuccessor() throws Exception {
|
|
344
|
+
ForegroundService s = armed("a");
|
|
345
|
+
Intent old = alarm();
|
|
346
|
+
ForegroundService.stop();
|
|
347
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
348
|
+
request("b", true);
|
|
349
|
+
deliver(s, sender.starts.get(1), 2);
|
|
350
|
+
int demotions = platform(s).demotions, stops = platform(s).stopCalls;
|
|
351
|
+
SystemClock.sleep(1100);
|
|
352
|
+
receive(old);
|
|
353
|
+
assertEquals(demotions, platform(s).demotions);
|
|
354
|
+
assertEquals(stops, platform(s).stopCalls);
|
|
355
|
+
assertEquals("b", ForegroundService.mCurrentNotificationId);
|
|
356
|
+
receive(alarm());
|
|
357
|
+
assertEquals(demotions + 1, platform(s).demotions);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
@Test
|
|
361
|
+
public void T14_explicitStopInvalidatesBeforePostedStop() throws Exception {
|
|
362
|
+
ForegroundService s = armed("a");
|
|
363
|
+
Intent old = alarm();
|
|
364
|
+
ForegroundService.stop();
|
|
365
|
+
SystemClock.sleep(1100);
|
|
366
|
+
receive(old);
|
|
367
|
+
noTeardown(s);
|
|
368
|
+
assertTrue(Alarms.cancelled > 0);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
@Test
|
|
372
|
+
public void T15_cancelFailureAndInFlightDelivery() throws Exception {
|
|
373
|
+
ForegroundService s = armed("a");
|
|
374
|
+
Intent old = alarm();
|
|
375
|
+
Alarms.cancelFailure = true;
|
|
376
|
+
ForegroundService.stop();
|
|
377
|
+
SystemClock.sleep(1100);
|
|
378
|
+
receive(old);
|
|
379
|
+
noTeardown(s);
|
|
380
|
+
assertTrue(marker("CANCEL_FAILED"));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
@Test
|
|
384
|
+
public void T16_frameworkTimeoutInvalidatesAlarm() throws Exception {
|
|
385
|
+
ForegroundService s = armed("a");
|
|
386
|
+
Intent old = alarm();
|
|
387
|
+
s.onTimeout(1, DATA);
|
|
388
|
+
int n = platform(s).demotions;
|
|
389
|
+
SystemClock.sleep(1100);
|
|
390
|
+
receive(old);
|
|
391
|
+
assertEquals(n, platform(s).demotions);
|
|
392
|
+
assertTrue(Alarms.cancelled > 0);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
@Test
|
|
396
|
+
public void T17_newInstanceSameIdRejectsOldToken() throws Exception {
|
|
397
|
+
ForegroundService a = armed("a");
|
|
398
|
+
Intent old = alarm();
|
|
399
|
+
controllers.get(0).destroy();
|
|
400
|
+
controllers.clear();
|
|
401
|
+
request("a", true);
|
|
402
|
+
ForegroundService b = create();
|
|
403
|
+
deliver(b, sender.starts.get(1), 1);
|
|
404
|
+
SystemClock.sleep(1100);
|
|
405
|
+
receive(old);
|
|
406
|
+
noTeardown(b);
|
|
407
|
+
receive(alarm());
|
|
408
|
+
assertEquals(1, platform(b).demotions);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
@Test
|
|
412
|
+
public void T18_coldReceiverHasNoOwner() throws Exception {
|
|
413
|
+
armed("a");
|
|
414
|
+
Intent i = alarm();
|
|
415
|
+
controllers.get(0).destroy();
|
|
416
|
+
controllers.clear();
|
|
417
|
+
SystemClock.sleep(1100);
|
|
418
|
+
int starts = sender.starts.size();
|
|
419
|
+
receive(i);
|
|
420
|
+
assertNull(field(null, "sInstance"));
|
|
421
|
+
assertEquals(starts, sender.starts.size());
|
|
422
|
+
assertTrue(marker("NO_MATCHING_LIVE_INSTANCE"));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
@Test
|
|
426
|
+
public void T19_unknownPendingStartPreserved() throws Exception {
|
|
427
|
+
ForegroundService s = armed("a");
|
|
428
|
+
sender.failure = new RuntimeException("unknown Binder reply");
|
|
429
|
+
assertThrows(RuntimeException.class, () -> request("pending", false));
|
|
430
|
+
expire();
|
|
431
|
+
assertEquals(1, platform(s).demotions);
|
|
432
|
+
assertEquals(0, platform(s).stopCalls);
|
|
433
|
+
assertEquals(1, ((Map<?, ?>) field(null, "sPendingStarts")).size());
|
|
434
|
+
assertTrue(marker("STOP_DEFERRED"));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
@Test
|
|
438
|
+
public void T20_acceptedUndeliveredStartPreserved() throws Exception {
|
|
439
|
+
ForegroundService s = armed("a");
|
|
440
|
+
request("pending", false);
|
|
441
|
+
platform(s).accepted = 2;
|
|
442
|
+
expire();
|
|
443
|
+
assertEquals(1, platform(s).demotions);
|
|
444
|
+
assertEquals(0, platform(s).stopCalls);
|
|
445
|
+
assertEquals(1, ((Map<?, ?>) field(null, "sPendingStarts")).size());
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
@Test
|
|
449
|
+
public void T21_runnerCompletionCannotRetireSuccessor() throws Exception {
|
|
450
|
+
ForegroundService s = armed("a");
|
|
451
|
+
Object runner = field(s, "mRunnerToken");
|
|
452
|
+
Platform.duringDemotion =
|
|
453
|
+
() -> {
|
|
454
|
+
try {
|
|
455
|
+
request("b", false);
|
|
456
|
+
deliver(s, sender.starts.get(1), 2);
|
|
457
|
+
} catch (Exception e) {
|
|
458
|
+
throw new AssertionError(e);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
expire();
|
|
462
|
+
assertEquals("b", ForegroundService.mCurrentNotificationId);
|
|
463
|
+
assertNotSame(runner, field(s, "mRunnerToken"));
|
|
464
|
+
assertEquals(0, platform(s).stopCalls);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
@Test
|
|
468
|
+
public void T22_typeReplacementInvalidatesWithoutRearm() throws Exception {
|
|
469
|
+
ForegroundService s = armed("a");
|
|
470
|
+
Intent old = alarm();
|
|
471
|
+
request("a", false);
|
|
472
|
+
Intent update = sender.starts.get(1);
|
|
473
|
+
update.putExtra(
|
|
474
|
+
"notificationBundle",
|
|
475
|
+
bundle("a", DATA | ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK));
|
|
476
|
+
deliver(s, update, 2);
|
|
477
|
+
SystemClock.sleep(1100);
|
|
478
|
+
receive(old);
|
|
479
|
+
noTeardown(s);
|
|
480
|
+
assertEquals(1, Alarms.requested);
|
|
481
|
+
assertTrue(Alarms.cancelled > 0);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
@Test
|
|
485
|
+
public void T23_schedulingFailureCannotAuthorizeDelivery() throws Exception {
|
|
486
|
+
Alarms.scheduleFailure = true;
|
|
487
|
+
request("a", true);
|
|
488
|
+
ForegroundService s = create();
|
|
489
|
+
deliver(s, sender.starts.get(0), 1);
|
|
490
|
+
assertEquals(1, Alarms.requested);
|
|
491
|
+
assertEquals(0, Alarms.scheduled.size());
|
|
492
|
+
assertTrue(marker("SCHEDULE_FAILED"));
|
|
493
|
+
SystemClock.sleep(1100);
|
|
494
|
+
receive(new Intent(shadowOf(Alarms.lastRequested).getSavedIntent()));
|
|
495
|
+
noTeardown(s);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
@Test
|
|
499
|
+
public void T24_corruptLedgerCannotAuthorizeDelivery() throws Exception {
|
|
500
|
+
ForegroundService s = armed("a");
|
|
501
|
+
Intent i = alarm();
|
|
502
|
+
app.getSharedPreferences("notifee-fgs-run-limit", 0)
|
|
503
|
+
.edit()
|
|
504
|
+
.putString(recordKey(ticket(s)), "corrupt")
|
|
505
|
+
.commit();
|
|
506
|
+
SystemClock.sleep(1100);
|
|
507
|
+
receive(i);
|
|
508
|
+
noTeardown(s);
|
|
509
|
+
assertTrue(marker("LEDGER_INVALID"));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
@Test
|
|
513
|
+
public void T25_demotionFailureStillAttemptsConditionalStopOnce() throws Exception {
|
|
514
|
+
ForegroundService s = armed("a");
|
|
515
|
+
Platform.demotionFailure = true;
|
|
516
|
+
platform(s).stopResult = false;
|
|
517
|
+
expire();
|
|
518
|
+
receive(alarm());
|
|
519
|
+
assertEquals(1, platform(s).demotions);
|
|
520
|
+
assertEquals(1, platform(s).stopCalls);
|
|
521
|
+
assertTrue(marker("DEMOTION_FAILED"));
|
|
522
|
+
assertTrue(marker("STOP_REFUSED"));
|
|
523
|
+
assertNotNull(field(s, "mRunnerToken"));
|
|
524
|
+
assertFalse(ForegroundService.repostIfActive("a"));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
@Test
|
|
528
|
+
public void T26_frameworkNewerStartRefusesStop() throws Exception {
|
|
529
|
+
ForegroundService s = armed("a");
|
|
530
|
+
platform(s).accepted = 2;
|
|
531
|
+
expire();
|
|
532
|
+
assertFalse(platform(s).stopped);
|
|
533
|
+
assertEquals(1, platform(s).stopCalls);
|
|
534
|
+
assertTrue(marker("STOP_REFUSED"));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
@Test
|
|
538
|
+
public void T27_stopThrowsOutcomeIsDistinct() throws Exception {
|
|
539
|
+
ForegroundService s = armed("a");
|
|
540
|
+
Platform.stopFailure = true;
|
|
541
|
+
expire();
|
|
542
|
+
assertEquals(1, platform(s).stopCalls);
|
|
543
|
+
assertTrue(marker("STOP_FAILED"));
|
|
544
|
+
assertTrue(marker("DEMOTION_RETURNED"));
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
@Test
|
|
548
|
+
public void T28_noReactContextOrContextHolderRequired() throws Exception {
|
|
549
|
+
ForegroundService s = armed("a");
|
|
550
|
+
ContextHolder.setApplicationContext(null);
|
|
551
|
+
expire();
|
|
552
|
+
assertEquals(1, platform(s).demotions);
|
|
553
|
+
assertTrue(platform(s).stopped);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
@Test
|
|
557
|
+
public void T29_receiverNeverColdStartsService() throws Exception {
|
|
558
|
+
armed("a");
|
|
559
|
+
Intent i = alarm();
|
|
560
|
+
set(null, "sInstance", null);
|
|
561
|
+
set(null, "sStateOwner", null);
|
|
562
|
+
int starts = sender.starts.size();
|
|
563
|
+
SystemClock.sleep(1100);
|
|
564
|
+
receive(i);
|
|
565
|
+
assertEquals(starts, sender.starts.size());
|
|
566
|
+
assertTrue(marker("NO_MATCHING_LIVE_INSTANCE"));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
@Test
|
|
570
|
+
public void T30_voluntaryExpiryDoesNotPublishFgTimeout() throws Exception {
|
|
571
|
+
ForegroundService s = armed("a");
|
|
572
|
+
List<Object> events = new ArrayList<>();
|
|
573
|
+
try (org.mockito.MockedStatic<EventBus> bus = org.mockito.Mockito.mockStatic(EventBus.class)) {
|
|
574
|
+
bus.when(() -> EventBus.post(org.mockito.ArgumentMatchers.any()))
|
|
575
|
+
.thenAnswer(
|
|
576
|
+
i -> {
|
|
577
|
+
events.add(i.getArgument(0));
|
|
578
|
+
return null;
|
|
579
|
+
});
|
|
580
|
+
expire();
|
|
581
|
+
assertTrue(events.isEmpty());
|
|
582
|
+
assertEquals(1, platform(s).demotions);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
@Test
|
|
587
|
+
public void auditBootChangeCannotDemote() throws Exception {
|
|
588
|
+
ForegroundService s = armed("a");
|
|
589
|
+
Settings.Global.putInt(app.getContentResolver(), Settings.Global.BOOT_COUNT, 43);
|
|
590
|
+
expire();
|
|
591
|
+
noTeardown(s);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
@Test
|
|
595
|
+
public void auditProcessIncarnationIsNotRestoredFromDisk() throws Exception {
|
|
596
|
+
ForegroundService s = armed("a");
|
|
597
|
+
Intent i = alarm();
|
|
598
|
+
SystemClock.sleep(1100);
|
|
599
|
+
String wire = i.getStringExtra("record");
|
|
600
|
+
String other = new org.json.JSONObject(wire).put("process", "previous-process").toString();
|
|
601
|
+
app.getSharedPreferences("notifee-fgs-run-limit", 0)
|
|
602
|
+
.edit()
|
|
603
|
+
.putString(recordKey(ticket(s)), other)
|
|
604
|
+
.commit();
|
|
605
|
+
receive(new Intent(i).putExtra("record", other));
|
|
606
|
+
noTeardown(s);
|
|
607
|
+
// Even a matching persisted prior-process record cannot create a native ticket.
|
|
608
|
+
set(s, "mAlarmPoc", null);
|
|
609
|
+
receive(new Intent(i).putExtra("record", other));
|
|
610
|
+
noTeardown(s);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
@Test
|
|
614
|
+
public void auditEpochFenceIndependentOfEnvelope() throws Exception {
|
|
615
|
+
ForegroundService s = armed("a");
|
|
616
|
+
Object ticket = field(s, "mAlarmPoc");
|
|
617
|
+
Intent update = new Intent(sender.starts.get(0));
|
|
618
|
+
update.removeExtra("notifee.foregroundServiceStartRequestId");
|
|
619
|
+
update.putExtra(
|
|
620
|
+
"notificationBundle",
|
|
621
|
+
bundle("a", DATA | ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK));
|
|
622
|
+
deliver(s, update, 2);
|
|
623
|
+
// Adversarial fixture presents an old authentic ticket after epoch replacement. The native
|
|
624
|
+
// epoch fence must still refuse it, even with valid token, boot, generation and ledger.
|
|
625
|
+
Field valid = ticket.getClass().getDeclaredField("valid");
|
|
626
|
+
valid.setAccessible(true);
|
|
627
|
+
valid.set(ticket, true);
|
|
628
|
+
Field wire = ticket.getClass().getDeclaredField("wire");
|
|
629
|
+
wire.setAccessible(true);
|
|
630
|
+
app.getSharedPreferences("notifee-fgs-run-limit", 0)
|
|
631
|
+
.edit()
|
|
632
|
+
.putString(
|
|
633
|
+
recordKey((ForegroundServiceRunLimitScheduler.Ticket) ticket),
|
|
634
|
+
(String) wire.get(ticket))
|
|
635
|
+
.putString(stateKey((ForegroundServiceRunLimitScheduler.Ticket) ticket), "ARMED")
|
|
636
|
+
.commit();
|
|
637
|
+
set(s, "mAlarmPoc", ticket);
|
|
638
|
+
// Preserve the exact dataSync state to isolate epoch identity from the type guard.
|
|
639
|
+
ForegroundService.mCurrentForegroundServiceType = DATA;
|
|
640
|
+
expire();
|
|
641
|
+
noTeardown(s);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
@Test
|
|
645
|
+
public void auditOptInCannotAttachToExistingRun() throws Exception {
|
|
646
|
+
request("a", false);
|
|
647
|
+
ForegroundService s = create();
|
|
648
|
+
deliver(s, sender.starts.get(0), 1);
|
|
649
|
+
assertThrows(IllegalStateException.class, () -> request("a", true));
|
|
650
|
+
assertEquals(0, Alarms.requested);
|
|
651
|
+
noTeardown(s);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
@Test
|
|
655
|
+
public void auditMixedInitialTypeRejected() throws Exception {
|
|
656
|
+
request("a", true);
|
|
657
|
+
Intent i = sender.starts.get(0);
|
|
658
|
+
i.putExtra(
|
|
659
|
+
"notificationBundle",
|
|
660
|
+
bundle("a", DATA | ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK));
|
|
661
|
+
deliver(create(), i, 1);
|
|
662
|
+
assertEquals(0, Alarms.requested);
|
|
663
|
+
assertTrue(marker("ARM_REJECTED"));
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
@Test
|
|
667
|
+
@Config(sdk = 34)
|
|
668
|
+
public void auditApi34CannotOptIn() throws Exception {
|
|
669
|
+
assertThrows(IllegalStateException.class, () -> request("a", true));
|
|
670
|
+
assertEquals(0, Alarms.requested);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
@Test
|
|
674
|
+
public void auditExportedOrOtherProcessReceiverCannotOptIn() throws Exception {
|
|
675
|
+
ActivityInfo receiver =
|
|
676
|
+
app.getPackageManager().getReceiverInfo(new ComponentName(app, RECEIVER), 0);
|
|
677
|
+
receiver.exported = true;
|
|
678
|
+
shadowOf(app.getPackageManager()).addOrUpdateReceiver(receiver);
|
|
679
|
+
assertThrows(IllegalStateException.class, () -> request("a", true));
|
|
680
|
+
receiver.exported = false;
|
|
681
|
+
receiver.processName = app.getPackageName() + ":other";
|
|
682
|
+
shadowOf(app.getPackageManager()).addOrUpdateReceiver(receiver);
|
|
683
|
+
assertThrows(IllegalStateException.class, () -> request("a", true));
|
|
684
|
+
assertEquals(0, Alarms.requested);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
@Test
|
|
688
|
+
public void auditNonDebugCannotOptIn() throws Exception {
|
|
689
|
+
app.getApplicationInfo().flags &= ~ApplicationInfo.FLAG_DEBUGGABLE;
|
|
690
|
+
assertThrows(IllegalStateException.class, () -> request("a", true));
|
|
691
|
+
assertEquals(0, Alarms.requested);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
@Test
|
|
695
|
+
public void auditIncompleteArmingLedgerIsNotAuthority() throws Exception {
|
|
696
|
+
ForegroundService s = armed("a");
|
|
697
|
+
app.getSharedPreferences("notifee-fgs-run-limit", 0)
|
|
698
|
+
.edit()
|
|
699
|
+
.putString(stateKey(ticket(s)), "ARMING")
|
|
700
|
+
.commit();
|
|
701
|
+
expire();
|
|
702
|
+
noTeardown(s);
|
|
703
|
+
assertTrue(marker("LEDGER_INVALID"));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
@Test
|
|
707
|
+
public void auditStopReplyCannotRetireReentrantSuccessor() throws Exception {
|
|
708
|
+
ForegroundService s = armed("a");
|
|
709
|
+
Platform.duringStop =
|
|
710
|
+
() -> {
|
|
711
|
+
try {
|
|
712
|
+
request("b", false);
|
|
713
|
+
deliver(s, sender.starts.get(1), 2);
|
|
714
|
+
} catch (Exception e) {
|
|
715
|
+
throw new AssertionError(e);
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
expire();
|
|
719
|
+
assertEquals("b", ForegroundService.mCurrentNotificationId);
|
|
720
|
+
assertFalse((boolean) field(s, "mStopInitiated"));
|
|
721
|
+
assertNotNull(field(s, "mRunnerToken"));
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
public static final class Runners {
|
|
725
|
+
final List<app.notifee.core.event.ForegroundServiceEvent> events = new ArrayList<>();
|
|
726
|
+
|
|
727
|
+
@org.greenrobot.eventbus.Subscribe
|
|
728
|
+
public void on(app.notifee.core.event.ForegroundServiceEvent event) {
|
|
729
|
+
events.add(event);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
@Test
|
|
734
|
+
public void auditRunnerCompletionDuringExpiryAndAfterSuccessor() throws Exception {
|
|
735
|
+
Runners runners = new Runners();
|
|
736
|
+
EventBus.register(runners);
|
|
737
|
+
try {
|
|
738
|
+
ForegroundService s = armed("a");
|
|
739
|
+
assertEquals(1, runners.events.size());
|
|
740
|
+
Platform.duringDemotion = () -> runners.events.get(0).setCompletionResult();
|
|
741
|
+
expire();
|
|
742
|
+
assertEquals(1, platform(s).demotions);
|
|
743
|
+
request("b", false);
|
|
744
|
+
deliver(s, sender.starts.get(1), 2);
|
|
745
|
+
Object successorRunner = field(s, "mRunnerToken");
|
|
746
|
+
runners.events.get(0).setCompletionResult();
|
|
747
|
+
assertSame(successorRunner, field(s, "mRunnerToken"));
|
|
748
|
+
assertEquals("b", ForegroundService.mCurrentNotificationId);
|
|
749
|
+
} finally {
|
|
750
|
+
EventBus.unregister(runners);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
@Test
|
|
755
|
+
public void auditRunnerCompletionBeforeExpiryInvalidatesAlarm() throws Exception {
|
|
756
|
+
Runners runners = new Runners();
|
|
757
|
+
EventBus.register(runners);
|
|
758
|
+
try {
|
|
759
|
+
ForegroundService s = armed("a");
|
|
760
|
+
Intent i = alarm();
|
|
761
|
+
runners.events.get(0).setCompletionResult();
|
|
762
|
+
int n = platform(s).demotions;
|
|
763
|
+
SystemClock.sleep(1100);
|
|
764
|
+
receive(i);
|
|
765
|
+
assertEquals(n, platform(s).demotions);
|
|
766
|
+
assertEquals(0, platform(s).stopCalls);
|
|
767
|
+
assertTrue(Alarms.cancelled > 0);
|
|
768
|
+
} finally {
|
|
769
|
+
EventBus.unregister(runners);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
@Test
|
|
774
|
+
public void auditReentrantDuplicateDeliveryDuringDemotion() throws Exception {
|
|
775
|
+
ForegroundService s = armed("a");
|
|
776
|
+
Intent i = alarm();
|
|
777
|
+
Platform.duringDemotion =
|
|
778
|
+
() -> {
|
|
779
|
+
try {
|
|
780
|
+
receive(i);
|
|
781
|
+
} catch (Exception e) {
|
|
782
|
+
throw new AssertionError(e);
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
expire();
|
|
786
|
+
assertEquals(1, platform(s).demotions);
|
|
787
|
+
assertEquals(1, platform(s).stopCalls);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
@Test
|
|
791
|
+
public void auditNewPendingStartDuringDemotion() throws Exception {
|
|
792
|
+
ForegroundService s = armed("a");
|
|
793
|
+
Platform.duringDemotion =
|
|
794
|
+
() -> {
|
|
795
|
+
try {
|
|
796
|
+
request("b", false);
|
|
797
|
+
} catch (Exception e) {
|
|
798
|
+
throw new AssertionError(e);
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
expire();
|
|
802
|
+
assertEquals(0, platform(s).stopCalls);
|
|
803
|
+
assertEquals(1, ((Map<?, ?>) field(null, "sPendingStarts")).size());
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
@Test
|
|
807
|
+
public void auditStorageFailureBeforeSchedule() throws Exception {
|
|
808
|
+
Preferences.failCommit = 1;
|
|
809
|
+
request("a", true);
|
|
810
|
+
ForegroundService s = create();
|
|
811
|
+
deliver(s, sender.starts.get(0), 1);
|
|
812
|
+
assertEquals(0, Alarms.requested);
|
|
813
|
+
assertTrue(marker("STORAGE_FAILED"));
|
|
814
|
+
noTeardown(s);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
@Test
|
|
818
|
+
public void auditStorageFailureAfterSchedule() throws Exception {
|
|
819
|
+
Preferences.failCommit = 2;
|
|
820
|
+
request("a", true);
|
|
821
|
+
ForegroundService s = create();
|
|
822
|
+
deliver(s, sender.starts.get(0), 1);
|
|
823
|
+
assertEquals(
|
|
824
|
+
ShadowLog.getLogsForTag("ForegroundServiceRunLimit").toString(),
|
|
825
|
+
1,
|
|
826
|
+
Alarms.scheduled.size());
|
|
827
|
+
assertTrue(Alarms.cancelled > 0);
|
|
828
|
+
SystemClock.sleep(1100);
|
|
829
|
+
receive(alarm());
|
|
830
|
+
noTeardown(s);
|
|
831
|
+
assertTrue(marker("STORAGE_FAILED"));
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
@Test
|
|
835
|
+
public void auditStorageReadThrows() throws Exception {
|
|
836
|
+
Preferences.enabled = true;
|
|
837
|
+
ForegroundService s = armed("a");
|
|
838
|
+
Preferences.readFailure = true;
|
|
839
|
+
expire();
|
|
840
|
+
noTeardown(s);
|
|
841
|
+
assertTrue(marker("STORAGE_FAILED"));
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
@Test
|
|
845
|
+
public void auditDeathDuringArmingCannotAuthorizeColdDelivery() throws Exception {
|
|
846
|
+
Alarms.afterSchedule =
|
|
847
|
+
() -> {
|
|
848
|
+
controllers.get(0).destroy();
|
|
849
|
+
controllers.clear();
|
|
850
|
+
};
|
|
851
|
+
request("a", true);
|
|
852
|
+
ForegroundService s = create();
|
|
853
|
+
deliver(s, sender.starts.get(0), 1);
|
|
854
|
+
assertEquals(
|
|
855
|
+
ShadowLog.getLogsForTag("ForegroundServiceRunLimit").toString(),
|
|
856
|
+
1,
|
|
857
|
+
Alarms.scheduled.size());
|
|
858
|
+
assertTrue(Alarms.cancelled > 0);
|
|
859
|
+
SystemClock.sleep(1100);
|
|
860
|
+
receive(alarm());
|
|
861
|
+
noTeardown(s);
|
|
862
|
+
assertTrue(marker("NO_MATCHING_LIVE_INSTANCE"));
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
ForegroundServiceRunLimitScheduler.Ticket ticket(ForegroundService s) throws Exception {
|
|
866
|
+
return (ForegroundServiceRunLimitScheduler.Ticket) field(s, "mAlarmPoc");
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Baseline adapter changes only the address, never the expected ownership behavior.
|
|
870
|
+
String recordKey(ForegroundServiceRunLimitScheduler.Ticket t) throws Exception {
|
|
871
|
+
try {
|
|
872
|
+
Field key = t.getClass().getDeclaredField("recordKey");
|
|
873
|
+
key.setAccessible(true);
|
|
874
|
+
return (String) key.get(t);
|
|
875
|
+
} catch (NoSuchFieldException baseline) {
|
|
876
|
+
return "record";
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
String stateKey(ForegroundServiceRunLimitScheduler.Ticket t) throws Exception {
|
|
881
|
+
String key = recordKey(t);
|
|
882
|
+
return key.equals("record") ? "state" : key + "/state";
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
SharedPreferences store() {
|
|
886
|
+
return app.getSharedPreferences(ForegroundServiceRunLimitScheduler.PREFS, 0);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Fixed-slot contract: absence on the preimage is a behavioral no-op, never a compile RED.
|
|
890
|
+
void collect(Context context) throws Exception {
|
|
891
|
+
try {
|
|
892
|
+
Method method =
|
|
893
|
+
ForegroundServiceRunLimitScheduler.class.getDeclaredMethod(
|
|
894
|
+
"collectOrphans", Context.class);
|
|
895
|
+
method.setAccessible(true);
|
|
896
|
+
method.invoke(null, context);
|
|
897
|
+
} catch (NoSuchMethodException preimage) {
|
|
898
|
+
// The preimage retains every orphan.
|
|
899
|
+
} catch (InvocationTargetException failure) {
|
|
900
|
+
throw new AssertionError(
|
|
901
|
+
"cleanup must report storage failure without throwing", failure.getCause());
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
String gcSeed(int slot, String process, int boot, String state) throws Exception {
|
|
906
|
+
String token = UUID.randomUUID().toString();
|
|
907
|
+
String key = "ticket/" + process + "/" + token;
|
|
908
|
+
String wire =
|
|
909
|
+
new org.json.JSONObject()
|
|
910
|
+
.put("schema", 1)
|
|
911
|
+
.put("boot", boot)
|
|
912
|
+
.put("process", process)
|
|
913
|
+
.put("instance", UUID.randomUUID().toString())
|
|
914
|
+
.put("run", "orphan")
|
|
915
|
+
.put("epoch", UUID.randomUUID().toString())
|
|
916
|
+
.put("generation", 0)
|
|
917
|
+
.put("runGeneration", 0)
|
|
918
|
+
.put("token", token)
|
|
919
|
+
.put("firstSuccessfulPromotionElapsed", 1)
|
|
920
|
+
.put("deadlineElapsed", 1001)
|
|
921
|
+
.put("type", DATA)
|
|
922
|
+
.toString();
|
|
923
|
+
store()
|
|
924
|
+
.edit()
|
|
925
|
+
.putString("gc/slot/" + slot, key)
|
|
926
|
+
.putString(key, wire)
|
|
927
|
+
.putString(key + "/state", state)
|
|
928
|
+
.commit();
|
|
929
|
+
return key;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
String gcOrphan(int slot) throws Exception {
|
|
933
|
+
return gcSeed(slot, UUID.randomUUID().toString(), 42, "ARMED");
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
void collectCycle(Context context) throws Exception {
|
|
937
|
+
for (int n = 0; n < 4; n++) collect(context);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
@Test
|
|
941
|
+
public void GC_T01_noOrphanNoEffect() throws Exception {
|
|
942
|
+
Map<String, ?> before = new HashMap<>(store().getAll());
|
|
943
|
+
collectCycle(app);
|
|
944
|
+
assertEquals(before, store().getAll());
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
@Test
|
|
948
|
+
public void GC_T02_provenOrphanRemoved() throws Exception {
|
|
949
|
+
String key = gcOrphan(0);
|
|
950
|
+
collectCycle(app);
|
|
951
|
+
assertFalse("proven orphan must be removed", store().contains(key));
|
|
952
|
+
assertFalse(store().contains(key + "/state"));
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
@Test
|
|
956
|
+
public void GC_T03_currentTicketPreserved() throws Exception {
|
|
957
|
+
ForegroundService s = armed("a");
|
|
958
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
959
|
+
collectCycle(app);
|
|
960
|
+
assertTrue(ForegroundServiceRunLimitScheduler.ledgerMatches(a));
|
|
961
|
+
assertFalse(shadowOf(a.pendingIntent).isCanceled());
|
|
962
|
+
expire();
|
|
963
|
+
assertEquals(1, platform(s).demotions);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
@Test
|
|
967
|
+
public void GC_T04_cleanupAPreservesB() throws Exception {
|
|
968
|
+
ForegroundService s = armed("a");
|
|
969
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
970
|
+
promoteSuccessor(s).run();
|
|
971
|
+
store()
|
|
972
|
+
.edit()
|
|
973
|
+
.putString("gc/slot/10", a.recordKey)
|
|
974
|
+
.putString(a.recordKey, a.wire)
|
|
975
|
+
.putString(a.stateKey, "ARMED")
|
|
976
|
+
.commit();
|
|
977
|
+
collectCycle(app);
|
|
978
|
+
assertFalse(store().contains(a.recordKey));
|
|
979
|
+
assertSuccessorExpires(s, a);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
@Test
|
|
983
|
+
public void GC_T05_reentrantBAtRemove() throws Exception {
|
|
984
|
+
Preferences.enabled = true;
|
|
985
|
+
ForegroundService s = armed("a");
|
|
986
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
987
|
+
String key = gcOrphan(1);
|
|
988
|
+
Preferences.beforeRemove = promoteSuccessor(s);
|
|
989
|
+
collectCycle(s);
|
|
990
|
+
assertNull("removal witness must execute", Preferences.beforeRemove);
|
|
991
|
+
assertFalse(store().contains(key));
|
|
992
|
+
assertSuccessorExpires(s, a);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
@Test
|
|
996
|
+
public void GC_T06_ambiguousRetained() throws Exception {
|
|
997
|
+
String key = gcSeed(0, UUID.randomUUID().toString(), 42, "UNKNOWN");
|
|
998
|
+
String wire = store().getString(key, null);
|
|
999
|
+
collectCycle(app);
|
|
1000
|
+
assertEquals(wire, store().getString(key, null));
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
@Test
|
|
1004
|
+
public void GC_T07_corruptHasNoAuthority() throws Exception {
|
|
1005
|
+
String key = gcOrphan(0);
|
|
1006
|
+
store().edit().putString(key, "{corrupt").commit();
|
|
1007
|
+
collectCycle(app);
|
|
1008
|
+
assertEquals("{corrupt", store().getString(key, null));
|
|
1009
|
+
assertTrue(sender.starts.isEmpty());
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
@Test
|
|
1013
|
+
public void GC_T08_priorProcessRemoved() throws Exception {
|
|
1014
|
+
String key = gcSeed(0, UUID.randomUUID().toString(), 42, "ARMED");
|
|
1015
|
+
collectCycle(app);
|
|
1016
|
+
assertFalse(store().contains(key));
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
@Test
|
|
1020
|
+
public void GC_T09_priorBootRemoved() throws Exception {
|
|
1021
|
+
String key = gcSeed(0, UUID.randomUUID().toString(), 41, "ARMED");
|
|
1022
|
+
collectCycle(app);
|
|
1023
|
+
assertFalse(store().contains(key));
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
@Test
|
|
1027
|
+
public void GC_T10_armingCompleteRemovedIncompleteRetained() throws Exception {
|
|
1028
|
+
String full = gcSeed(0, UUID.randomUUID().toString(), 42, "ARMING");
|
|
1029
|
+
String partial = gcSeed(1, UUID.randomUUID().toString(), 42, "ARMING");
|
|
1030
|
+
store().edit().putString(partial, "{}").commit();
|
|
1031
|
+
collectCycle(app);
|
|
1032
|
+
assertFalse(store().contains(full));
|
|
1033
|
+
assertEquals("{}", store().getString(partial, null));
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
@Test
|
|
1037
|
+
public void GC_T11_failedScheduleResidueRemoved() throws Exception {
|
|
1038
|
+
Preferences.enabled = true;
|
|
1039
|
+
request("a", true);
|
|
1040
|
+
ForegroundService s = create();
|
|
1041
|
+
Alarms.afterSchedule =
|
|
1042
|
+
() -> {
|
|
1043
|
+
Alarms.afterSchedule = null;
|
|
1044
|
+
Preferences.commitThrows = true;
|
|
1045
|
+
};
|
|
1046
|
+
deliver(s, sender.starts.get(0), 1);
|
|
1047
|
+
String key = store().getString("gc/slot/0", null);
|
|
1048
|
+
assertNotNull("failed scheduling must remain indexed", key);
|
|
1049
|
+
Preferences.commitThrows = false;
|
|
1050
|
+
collectCycle(s);
|
|
1051
|
+
assertFalse(store().contains(key));
|
|
1052
|
+
receive(alarm());
|
|
1053
|
+
noTeardown(s);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
@Test
|
|
1057
|
+
public void GC_T12_duplicateHarmless() throws Exception {
|
|
1058
|
+
String key = gcOrphan(0);
|
|
1059
|
+
collectCycle(app);
|
|
1060
|
+
Map<String, ?> before = new HashMap<>(store().getAll());
|
|
1061
|
+
collectCycle(app);
|
|
1062
|
+
assertEquals(before, store().getAll());
|
|
1063
|
+
assertFalse(store().contains(key));
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
@Test
|
|
1067
|
+
public void GC_T13_readFailureRetains() throws Exception {
|
|
1068
|
+
Preferences.enabled = true;
|
|
1069
|
+
ForegroundService s = create();
|
|
1070
|
+
String key = gcOrphan(0);
|
|
1071
|
+
Preferences.readFailure = true;
|
|
1072
|
+
collectCycle(s);
|
|
1073
|
+
Preferences.readFailure = false;
|
|
1074
|
+
assertTrue(store().contains(key));
|
|
1075
|
+
assertTrue(marker("GC_RESULT"));
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
@Test
|
|
1079
|
+
public void GC_T14_commitFailureRetainsAndReports() throws Exception {
|
|
1080
|
+
Preferences.enabled = true;
|
|
1081
|
+
ForegroundService s = create();
|
|
1082
|
+
String key = gcOrphan(0);
|
|
1083
|
+
Preferences.commitThrows = true;
|
|
1084
|
+
collectCycle(s);
|
|
1085
|
+
Preferences.commitThrows = false;
|
|
1086
|
+
assertTrue(store().contains(key));
|
|
1087
|
+
assertTrue(marker("GC_RESULT"));
|
|
1088
|
+
collectCycle(s);
|
|
1089
|
+
assertFalse(store().contains(key));
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
@Test
|
|
1093
|
+
public void GC_T15_boundedEight() throws Exception {
|
|
1094
|
+
List<String> keys = new ArrayList<>();
|
|
1095
|
+
for (int n = 0; n < 32; n++) keys.add(gcOrphan(n));
|
|
1096
|
+
collect(app);
|
|
1097
|
+
long removed = keys.stream().filter(k -> !store().contains(k)).count();
|
|
1098
|
+
assertEquals("exactly eight slots per invocation", 8, removed);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
@Test
|
|
1102
|
+
public void GC_T16_moreThanBatchEventuallyRemoved() throws Exception {
|
|
1103
|
+
List<String> keys = new ArrayList<>();
|
|
1104
|
+
for (int n = 0; n < 32; n++) keys.add(gcOrphan(n));
|
|
1105
|
+
collectCycle(app);
|
|
1106
|
+
for (String key : keys) assertFalse(store().contains(key));
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
@Test
|
|
1110
|
+
public void GC_T17_noGcInTimeout() throws Exception {
|
|
1111
|
+
ForegroundService s = armed("a");
|
|
1112
|
+
String key = gcOrphan(10);
|
|
1113
|
+
ShadowLog.clear();
|
|
1114
|
+
s.onTimeout(1, DATA);
|
|
1115
|
+
assertTrue(store().contains(key));
|
|
1116
|
+
assertFalse(marker("GC_RESULT"));
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
@Test
|
|
1120
|
+
public void GC_T18_foreignAndLegacyKeysUntouched() throws Exception {
|
|
1121
|
+
store()
|
|
1122
|
+
.edit()
|
|
1123
|
+
.putString("foreign", "payload")
|
|
1124
|
+
.putString("record", "legacy")
|
|
1125
|
+
.putString("gc/slot/0", "foreign")
|
|
1126
|
+
.commit();
|
|
1127
|
+
collectCycle(app);
|
|
1128
|
+
assertEquals("payload", store().getString("foreign", null));
|
|
1129
|
+
assertEquals("legacy", store().getString("record", null));
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
@Test
|
|
1133
|
+
public void GC_T19_noPendingIntentCancellation() throws Exception {
|
|
1134
|
+
ForegroundService s = armed("a");
|
|
1135
|
+
gcOrphan(10);
|
|
1136
|
+
int cancelled = Alarms.cancelled;
|
|
1137
|
+
collectCycle(app);
|
|
1138
|
+
assertEquals(cancelled, Alarms.cancelled);
|
|
1139
|
+
assertFalse(shadowOf(ticket(s).pendingIntent).isCanceled());
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
@Test
|
|
1143
|
+
public void GC_T20_successorFullExpiry() throws Exception {
|
|
1144
|
+
ForegroundService s = armed("a");
|
|
1145
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1146
|
+
promoteSuccessor(s).run();
|
|
1147
|
+
String key = gcOrphan(10);
|
|
1148
|
+
collectCycle(app);
|
|
1149
|
+
assertFalse(store().contains(key));
|
|
1150
|
+
assertSuccessorExpires(s, a);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
@Test
|
|
1154
|
+
public void GC_auditFullAmbiguousIndexRefusesOnlyNewProtection() throws Exception {
|
|
1155
|
+
for (int n = 0; n < 32; n++) gcSeed(n, UUID.randomUUID().toString(), 42, "UNKNOWN");
|
|
1156
|
+
Map<String, ?> before = new HashMap<>(store().getAll());
|
|
1157
|
+
request("a", true);
|
|
1158
|
+
ForegroundService s = create();
|
|
1159
|
+
deliver(s, sender.starts.get(0), 1);
|
|
1160
|
+
assertEquals(before, store().getAll());
|
|
1161
|
+
assertEquals(0, Alarms.requested);
|
|
1162
|
+
assertNull(ticket(s));
|
|
1163
|
+
assertTrue(platform(s).foreground);
|
|
1164
|
+
noTeardown(s);
|
|
1165
|
+
assertTrue(marker("GC_CAPACITY"));
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
@Test
|
|
1169
|
+
public void GC_auditReentrantBAtCommitKeepsIndexAndExpiry() throws Exception {
|
|
1170
|
+
Preferences.enabled = true;
|
|
1171
|
+
ForegroundService s = armed("a");
|
|
1172
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1173
|
+
String key = gcOrphan(10);
|
|
1174
|
+
Preferences.beforeCommit = promoteSuccessor(s);
|
|
1175
|
+
collectCycle(s);
|
|
1176
|
+
assertNull(Preferences.beforeCommit);
|
|
1177
|
+
assertFalse(store().contains(key));
|
|
1178
|
+
assertTrue(store().getAll().containsValue(ticket(s).recordKey));
|
|
1179
|
+
assertSuccessorExpires(s, a);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
@Test
|
|
1183
|
+
public void GC_auditWrongTypeIndexAndUnindexedRetained() throws Exception {
|
|
1184
|
+
String unindexed = gcOrphan(0);
|
|
1185
|
+
store().edit().putInt("gc/slot/0", 7).commit();
|
|
1186
|
+
collectCycle(app);
|
|
1187
|
+
assertTrue(store().contains(unindexed));
|
|
1188
|
+
assertEquals(7, store().getInt("gc/slot/0", 0));
|
|
1189
|
+
assertTrue(sender.starts.isEmpty());
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
@Test
|
|
1193
|
+
public void GC_auditFractionalAndNoncanonicalRecordsAreAmbiguous() throws Exception {
|
|
1194
|
+
String[] fields = {
|
|
1195
|
+
"boot", "generation", "runGeneration", "deadlineElapsed", "instance", "epoch"
|
|
1196
|
+
};
|
|
1197
|
+
for (int slot = 0; slot < fields.length; slot++) {
|
|
1198
|
+
String key = gcOrphan(slot);
|
|
1199
|
+
org.json.JSONObject wire = new org.json.JSONObject(store().getString(key, null));
|
|
1200
|
+
wire.put(fields[slot], slot < 4 ? 42.5 : "------------------------------------");
|
|
1201
|
+
store().edit().putString(key, wire.toString()).commit();
|
|
1202
|
+
}
|
|
1203
|
+
Map<String, ?> before = new HashMap<>(store().getAll());
|
|
1204
|
+
collectCycle(app);
|
|
1205
|
+
assertEquals("ambiguous numbers and UUIDs must be retained", before, store().getAll());
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
@Test
|
|
1209
|
+
public void GC_auditAnotherConfiguredServiceProcessIsNotPreviousIncarnation() throws Exception {
|
|
1210
|
+
String key = gcOrphan(0);
|
|
1211
|
+
ServiceInfo service =
|
|
1212
|
+
app.getPackageManager().getServiceInfo(new ComponentName(app, ForegroundService.class), 0);
|
|
1213
|
+
service.processName = app.getPackageName() + ":remote";
|
|
1214
|
+
shadowOf(app.getPackageManager()).addOrUpdateService(service);
|
|
1215
|
+
collectCycle(app);
|
|
1216
|
+
assertTrue(
|
|
1217
|
+
"another potentially live service process is not a proven orphan", store().contains(key));
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
@Test
|
|
1221
|
+
public void GC_auditActiveTicketSurvivesChangedBootAndExpiredAge() throws Exception {
|
|
1222
|
+
ForegroundService s = armed("a");
|
|
1223
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1224
|
+
SystemClock.sleep(10000000);
|
|
1225
|
+
Settings.Global.putInt(app.getContentResolver(), Settings.Global.BOOT_COUNT, 43);
|
|
1226
|
+
collectCycle(app);
|
|
1227
|
+
assertTrue(ForegroundServiceRunLimitScheduler.ledgerMatches(a));
|
|
1228
|
+
assertFalse(shadowOf(a.pendingIntent).isCanceled());
|
|
1229
|
+
noTeardown(s);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
@Test
|
|
1233
|
+
public void GC_auditRepeatedCancelledRunsReuseFixedSlots() throws Exception {
|
|
1234
|
+
ForegroundService s = create();
|
|
1235
|
+
for (int n = 0; n < 65; n++) {
|
|
1236
|
+
request("a", true);
|
|
1237
|
+
deliver(s, sender.starts.get(sender.starts.size() - 1), n + 1);
|
|
1238
|
+
assertNotNull(ticket(s));
|
|
1239
|
+
ForegroundService.stop();
|
|
1240
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
1241
|
+
}
|
|
1242
|
+
assertEquals(65, Alarms.requested);
|
|
1243
|
+
assertEquals(1, store().getAll().size()); // reusable index slot only; no orphan growth.
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
@Test
|
|
1247
|
+
public void auditOptedSameIdCannotRearmOrExtend() throws Exception {
|
|
1248
|
+
ForegroundService s = armed("a");
|
|
1249
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1250
|
+
SystemClock.sleep(100);
|
|
1251
|
+
try {
|
|
1252
|
+
request("a", true);
|
|
1253
|
+
deliver(s, sender.starts.get(sender.starts.size() - 1), 2);
|
|
1254
|
+
} catch (IllegalStateException expectedAdmissionRejection) {
|
|
1255
|
+
// Ordinary updates remain allowed; this explicit new diagnostic arm is rejected.
|
|
1256
|
+
}
|
|
1257
|
+
assertEquals("same-ID opt-in must not schedule a second alarm", 1, Alarms.requested);
|
|
1258
|
+
assertSame(a, ticket(s));
|
|
1259
|
+
assertEquals(a.deadline, ticket(s).deadline);
|
|
1260
|
+
expire();
|
|
1261
|
+
assertEquals(1, platform(s).demotions);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
Runnable promoteSuccessor(ForegroundService s) {
|
|
1265
|
+
return () -> {
|
|
1266
|
+
try {
|
|
1267
|
+
ForegroundService.stop();
|
|
1268
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
1269
|
+
request("b", true);
|
|
1270
|
+
deliver(s, sender.starts.get(sender.starts.size() - 1), 2);
|
|
1271
|
+
} catch (Exception e) {
|
|
1272
|
+
throw new AssertionError(e);
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
void assertSuccessorExpires(ForegroundService s, ForegroundServiceRunLimitScheduler.Ticket a)
|
|
1278
|
+
throws Exception {
|
|
1279
|
+
ForegroundServiceRunLimitScheduler.Ticket b = ticket(s);
|
|
1280
|
+
assertNotNull("B must retain live native authority", b);
|
|
1281
|
+
assertNotSame(a, b);
|
|
1282
|
+
assertNotSame(a.epoch, b.epoch);
|
|
1283
|
+
assertNotEquals(a.token, b.token);
|
|
1284
|
+
assertNotEquals(a.runGeneration, b.runGeneration);
|
|
1285
|
+
assertTrue(b.valid);
|
|
1286
|
+
assertTrue(b.scheduled);
|
|
1287
|
+
assertTrue(platform(s).foreground);
|
|
1288
|
+
assertEquals("b", ForegroundService.mCurrentNotificationId);
|
|
1289
|
+
assertEquals(b.wire, store().getString(recordKey(b), null));
|
|
1290
|
+
assertEquals("ARMED", store().getString(stateKey(b), null));
|
|
1291
|
+
assertTrue(ForegroundServiceRunLimitScheduler.matches(b, alarm()));
|
|
1292
|
+
assertFalse(shadowOf(b.pendingIntent).isCanceled());
|
|
1293
|
+
assertFalse(Alarms.cancellations.contains(b.pendingIntent));
|
|
1294
|
+
long deadline = b.deadline;
|
|
1295
|
+
int requested = Alarms.requested;
|
|
1296
|
+
int demotions = platform(s).demotions, stops = platform(s).stopCalls;
|
|
1297
|
+
SystemClock.sleep(1100);
|
|
1298
|
+
receive(a.intent);
|
|
1299
|
+
assertEquals("A cannot demote B", demotions, platform(s).demotions);
|
|
1300
|
+
assertEquals(stops, platform(s).stopCalls);
|
|
1301
|
+
try (org.mockito.MockedStatic<EventBus> bus = org.mockito.Mockito.mockStatic(EventBus.class)) {
|
|
1302
|
+
receive(b.intent);
|
|
1303
|
+
bus.verifyNoInteractions();
|
|
1304
|
+
}
|
|
1305
|
+
assertEquals(demotions + 1, platform(s).demotions);
|
|
1306
|
+
assertEquals(stops + 1, platform(s).stopCalls);
|
|
1307
|
+
assertEquals(2, platform(s).lastStopId);
|
|
1308
|
+
assertTrue(platform(s).stopped);
|
|
1309
|
+
assertFalse(platform(s).foreground);
|
|
1310
|
+
assertFalse(b.valid);
|
|
1311
|
+
assertNull(ticket(s));
|
|
1312
|
+
assertNull(store().getString(recordKey(b), null));
|
|
1313
|
+
assertEquals(deadline, b.deadline);
|
|
1314
|
+
assertEquals(requested, Alarms.requested);
|
|
1315
|
+
assertTrue(marker("DEMOTION_ATTEMPTED"));
|
|
1316
|
+
assertTrue(marker("STOP_ACCEPTED"));
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
@Test
|
|
1320
|
+
public void B82_W1_reviewerCleanupPreservesSuccessorLedger() throws Exception {
|
|
1321
|
+
Preferences.enabled = true;
|
|
1322
|
+
ForegroundService s = armed("a");
|
|
1323
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1324
|
+
Preferences.afterRead = promoteSuccessor(s);
|
|
1325
|
+
s.onTimeout(1, DATA);
|
|
1326
|
+
assertNull("read interleaving must execute", Preferences.afterRead);
|
|
1327
|
+
assertEquals(
|
|
1328
|
+
"old cleanup must preserve successor record",
|
|
1329
|
+
ticket(s).wire,
|
|
1330
|
+
store().getString(recordKey(ticket(s)), null));
|
|
1331
|
+
assertSuccessorExpires(s, a);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
@Test
|
|
1335
|
+
public void B82_W1_reviewerCleanupMustNotDisableSuccessorExpiry() throws Exception {
|
|
1336
|
+
Preferences.enabled = true;
|
|
1337
|
+
ForegroundService s = armed("a");
|
|
1338
|
+
Preferences.afterRead = promoteSuccessor(s);
|
|
1339
|
+
s.onTimeout(1, DATA);
|
|
1340
|
+
assertNull(Preferences.afterRead);
|
|
1341
|
+
assertEquals("b", ForegroundService.mCurrentNotificationId);
|
|
1342
|
+
int before = platform(s).demotions;
|
|
1343
|
+
SystemClock.sleep(1100);
|
|
1344
|
+
receive(alarm());
|
|
1345
|
+
assertEquals("B must retain its expiry", before + 1, platform(s).demotions);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
@Test
|
|
1349
|
+
public void B82_W2_replacementBeforeRemoval() throws Exception {
|
|
1350
|
+
Preferences.enabled = true;
|
|
1351
|
+
ForegroundService s = armed("a");
|
|
1352
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1353
|
+
Preferences.beforeRemove = promoteSuccessor(s);
|
|
1354
|
+
s.onTimeout(1, DATA);
|
|
1355
|
+
assertNull(Preferences.beforeRemove);
|
|
1356
|
+
assertSuccessorExpires(s, a);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
@Test
|
|
1360
|
+
public void B82_W2_replacementDuringCommit() throws Exception {
|
|
1361
|
+
Preferences.enabled = true;
|
|
1362
|
+
ForegroundService s = armed("a");
|
|
1363
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1364
|
+
Preferences.beforeCommit = promoteSuccessor(s);
|
|
1365
|
+
s.onTimeout(1, DATA);
|
|
1366
|
+
assertNull(Preferences.beforeCommit);
|
|
1367
|
+
assertSuccessorExpires(s, a);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
@Test
|
|
1371
|
+
public void B82_W3_legitimateAndAbsentCleanup() throws Exception {
|
|
1372
|
+
ForegroundService s = armed("a");
|
|
1373
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1374
|
+
assertEquals(a.wire, store().getString(recordKey(a), null));
|
|
1375
|
+
ForegroundService.stop();
|
|
1376
|
+
assertNull(store().getString(recordKey(a), null));
|
|
1377
|
+
assertNull(store().getString(stateKey(a), null));
|
|
1378
|
+
ForegroundServiceRunLimitScheduler.cancel(a);
|
|
1379
|
+
assertNull(store().getString(recordKey(a), null));
|
|
1380
|
+
assertFalse(a.valid);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
@Test
|
|
1384
|
+
public void B82_W4_cleanupAfterSuccessor() throws Exception {
|
|
1385
|
+
ForegroundService s = armed("a");
|
|
1386
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1387
|
+
promoteSuccessor(s).run();
|
|
1388
|
+
ForegroundServiceRunLimitScheduler.cancel(a);
|
|
1389
|
+
assertSuccessorExpires(s, a);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
@Test
|
|
1393
|
+
public void B82_W5_duplicateCleanupWithSameRunName() throws Exception {
|
|
1394
|
+
ForegroundService s = armed("b");
|
|
1395
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1396
|
+
ForegroundService.stop();
|
|
1397
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
1398
|
+
ForegroundServiceRunLimitScheduler.cancel(a);
|
|
1399
|
+
request("b", true);
|
|
1400
|
+
deliver(s, sender.starts.get(1), 2);
|
|
1401
|
+
ForegroundServiceRunLimitScheduler.cancel(a);
|
|
1402
|
+
ForegroundServiceRunLimitScheduler.cancel(a);
|
|
1403
|
+
assertSuccessorExpires(s, a);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
@Test
|
|
1407
|
+
public void B82_W6_lateSchedulingFailure() throws Exception {
|
|
1408
|
+
ForegroundService s = create();
|
|
1409
|
+
ForegroundServiceRunLimitScheduler.Ticket[] a =
|
|
1410
|
+
new ForegroundServiceRunLimitScheduler.Ticket[1];
|
|
1411
|
+
Alarms.afterSchedule =
|
|
1412
|
+
() -> {
|
|
1413
|
+
Alarms.afterSchedule = null;
|
|
1414
|
+
try {
|
|
1415
|
+
a[0] = ticket(s);
|
|
1416
|
+
} catch (Exception e) {
|
|
1417
|
+
throw new AssertionError(e);
|
|
1418
|
+
}
|
|
1419
|
+
promoteSuccessor(s).run();
|
|
1420
|
+
throw new IllegalStateException("late A scheduling failure");
|
|
1421
|
+
};
|
|
1422
|
+
request("a", true);
|
|
1423
|
+
deliver(s, sender.starts.get(0), 1);
|
|
1424
|
+
assertTrue(marker("SCHEDULE_FAILED"));
|
|
1425
|
+
assertFalse(a[0].valid);
|
|
1426
|
+
assertSuccessorExpires(s, a[0]);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
@Test
|
|
1430
|
+
public void B82_W7_cancelFailureDuringReplacement() throws Exception {
|
|
1431
|
+
ForegroundService s = armed("a");
|
|
1432
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1433
|
+
Alarms.duringCancel = promoteSuccessor(s);
|
|
1434
|
+
Alarms.cancelFailure = true;
|
|
1435
|
+
s.onTimeout(1, DATA);
|
|
1436
|
+
assertTrue(marker("CANCEL_FAILED"));
|
|
1437
|
+
Alarms.cancelFailure = false;
|
|
1438
|
+
assertSuccessorExpires(s, a);
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
@Test
|
|
1442
|
+
public void B82_W8_storageReadFailureDuringOldCleanup() throws Exception {
|
|
1443
|
+
Preferences.enabled = true;
|
|
1444
|
+
ForegroundService s = armed("a");
|
|
1445
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1446
|
+
promoteSuccessor(s).run();
|
|
1447
|
+
Preferences.readFailure = true;
|
|
1448
|
+
ForegroundServiceRunLimitScheduler.cancel(a);
|
|
1449
|
+
assertFalse(a.valid);
|
|
1450
|
+
assertTrue(marker("STORAGE_FAILED"));
|
|
1451
|
+
Preferences.readFailure = false;
|
|
1452
|
+
assertSuccessorExpires(s, a);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
@Test
|
|
1456
|
+
public void B82_W8_storageWriteFailureDuringReplacement() throws Exception {
|
|
1457
|
+
Preferences.enabled = true;
|
|
1458
|
+
ForegroundService s = armed("a");
|
|
1459
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1460
|
+
Preferences.beforeCommit =
|
|
1461
|
+
() -> {
|
|
1462
|
+
promoteSuccessor(s).run();
|
|
1463
|
+
Preferences.commitThrows = true;
|
|
1464
|
+
};
|
|
1465
|
+
s.onTimeout(1, DATA);
|
|
1466
|
+
assertTrue(marker("STORAGE_FAILED"));
|
|
1467
|
+
Preferences.commitThrows = false;
|
|
1468
|
+
assertSuccessorExpires(s, a);
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
@Test
|
|
1472
|
+
public void B82_W9_corruptRecordCannotBeDeletedAsOwned() throws Exception {
|
|
1473
|
+
ForegroundService s = armed("a");
|
|
1474
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1475
|
+
store().edit().putString(recordKey(a), "corrupt").commit();
|
|
1476
|
+
ForegroundServiceRunLimitScheduler.cancel(a);
|
|
1477
|
+
assertEquals("corrupt", store().getString(recordKey(a), null));
|
|
1478
|
+
promoteSuccessor(s).run();
|
|
1479
|
+
ForegroundServiceRunLimitScheduler.cancel(a);
|
|
1480
|
+
assertSuccessorExpires(s, a);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
@Test
|
|
1484
|
+
public void B82_W10_fullExpiryAfterLateCleanup() throws Exception {
|
|
1485
|
+
ForegroundService s = armed("a");
|
|
1486
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1487
|
+
s.onTimeout(1, DATA);
|
|
1488
|
+
promoteSuccessor(s).run();
|
|
1489
|
+
ForegroundServiceRunLimitScheduler.cancel(a);
|
|
1490
|
+
assertSuccessorExpires(s, a);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
@Test
|
|
1494
|
+
public void B82_auditLateArmingCommitCannotOverwriteSuccessor() throws Exception {
|
|
1495
|
+
Preferences.enabled = true;
|
|
1496
|
+
ForegroundService s = create();
|
|
1497
|
+
ForegroundServiceRunLimitScheduler.Ticket[] a =
|
|
1498
|
+
new ForegroundServiceRunLimitScheduler.Ticket[1];
|
|
1499
|
+
Preferences.beforeCommit =
|
|
1500
|
+
() -> {
|
|
1501
|
+
try {
|
|
1502
|
+
a[0] = ticket(s);
|
|
1503
|
+
} catch (Exception e) {
|
|
1504
|
+
throw new AssertionError(e);
|
|
1505
|
+
}
|
|
1506
|
+
promoteSuccessor(s).run();
|
|
1507
|
+
};
|
|
1508
|
+
request("a", true);
|
|
1509
|
+
deliver(s, sender.starts.get(0), 1);
|
|
1510
|
+
assertNull(Preferences.beforeCommit);
|
|
1511
|
+
assertFalse(a[0].valid);
|
|
1512
|
+
assertNull(store().getString(recordKey(a[0]), null));
|
|
1513
|
+
assertSuccessorExpires(s, a[0]);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
@Test
|
|
1517
|
+
public void B82_auditLateArmedCommitCannotOverwriteSuccessor() throws Exception {
|
|
1518
|
+
Preferences.enabled = true;
|
|
1519
|
+
ForegroundService s = create();
|
|
1520
|
+
ForegroundServiceRunLimitScheduler.Ticket[] a =
|
|
1521
|
+
new ForegroundServiceRunLimitScheduler.Ticket[1];
|
|
1522
|
+
Alarms.afterSchedule =
|
|
1523
|
+
() -> {
|
|
1524
|
+
Alarms.afterSchedule = null;
|
|
1525
|
+
try {
|
|
1526
|
+
a[0] = ticket(s);
|
|
1527
|
+
} catch (Exception e) {
|
|
1528
|
+
throw new AssertionError(e);
|
|
1529
|
+
}
|
|
1530
|
+
Preferences.beforeCommit = promoteSuccessor(s);
|
|
1531
|
+
};
|
|
1532
|
+
request("a", true);
|
|
1533
|
+
deliver(s, sender.starts.get(0), 1);
|
|
1534
|
+
assertNull(Preferences.beforeCommit);
|
|
1535
|
+
assertFalse(a[0].valid);
|
|
1536
|
+
assertNull(store().getString(recordKey(a[0]), null));
|
|
1537
|
+
assertSuccessorExpires(s, a[0]);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
@Test
|
|
1541
|
+
public void B82_auditOwnedOrphanCleanupPreservesForeignPayload() throws Exception {
|
|
1542
|
+
ForegroundService s = armed("a");
|
|
1543
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1544
|
+
promoteSuccessor(s).run();
|
|
1545
|
+
ForegroundServiceRunLimitScheduler.Ticket b = ticket(s);
|
|
1546
|
+
// A foreign wire at A's address is corruption, not authority to remove that payload.
|
|
1547
|
+
store().edit().putString(recordKey(a), b.wire).putString(stateKey(a), "ARMED").commit();
|
|
1548
|
+
ForegroundServiceRunLimitScheduler.cancel(a);
|
|
1549
|
+
assertEquals(b.wire, store().getString(recordKey(a), null));
|
|
1550
|
+
// An actual own orphan may still be cleaned without touching the successor.
|
|
1551
|
+
store().edit().putString(recordKey(a), a.wire).commit();
|
|
1552
|
+
ForegroundServiceRunLimitScheduler.cancel(a);
|
|
1553
|
+
assertNull(store().getString(recordKey(a), null));
|
|
1554
|
+
assertNull(store().getString(stateKey(a), null));
|
|
1555
|
+
assertSuccessorExpires(s, a);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
@Test
|
|
1559
|
+
public void B82_auditWrongStorageTypeFailsClosed() throws Exception {
|
|
1560
|
+
ForegroundService s = armed("a");
|
|
1561
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1562
|
+
promoteSuccessor(s).run();
|
|
1563
|
+
store().edit().putInt(recordKey(a), 42).commit();
|
|
1564
|
+
ForegroundServiceRunLimitScheduler.cancel(a);
|
|
1565
|
+
assertEquals(42, store().getInt(recordKey(a), 0));
|
|
1566
|
+
assertTrue(marker("STORAGE_FAILED"));
|
|
1567
|
+
assertSuccessorExpires(s, a);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
@Test
|
|
1571
|
+
public void B82_auditRefusedCleanupCanRetryOnlyOwnOrphan() throws Exception {
|
|
1572
|
+
Preferences.enabled = true;
|
|
1573
|
+
ForegroundService s = armed("a");
|
|
1574
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1575
|
+
Preferences.failCommit = Preferences.commits + 1;
|
|
1576
|
+
s.onTimeout(1, DATA);
|
|
1577
|
+
assertEquals(a.wire, store().getString(recordKey(a), null));
|
|
1578
|
+
assertFalse(a.valid);
|
|
1579
|
+
assertTrue(marker("STORAGE_FAILED"));
|
|
1580
|
+
Preferences.failCommit = 0;
|
|
1581
|
+
promoteSuccessor(s).run();
|
|
1582
|
+
ForegroundServiceRunLimitScheduler.cancel(a);
|
|
1583
|
+
assertNull(store().getString(recordKey(a), null));
|
|
1584
|
+
assertSuccessorExpires(s, a);
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
@Test
|
|
1588
|
+
public void B82_auditLegacyAndPriorProcessOrphansNeverGrantAuthority() throws Exception {
|
|
1589
|
+
ForegroundService s = armed("a");
|
|
1590
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1591
|
+
String legacy = a.wire;
|
|
1592
|
+
ForegroundService.stop();
|
|
1593
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
1594
|
+
String oldKey = "ticket/previous-process/old-token";
|
|
1595
|
+
store()
|
|
1596
|
+
.edit()
|
|
1597
|
+
.putString("record", legacy)
|
|
1598
|
+
.putString("state", "ARMED")
|
|
1599
|
+
.putString(oldKey, legacy)
|
|
1600
|
+
.putString(oldKey + "/state", "corrupt")
|
|
1601
|
+
.commit();
|
|
1602
|
+
set(null, "sInstance", null);
|
|
1603
|
+
set(null, "sStateOwner", null);
|
|
1604
|
+
Map<String, ?> snapshot = new HashMap<>(store().getAll());
|
|
1605
|
+
int starts = sender.starts.size();
|
|
1606
|
+
SystemClock.sleep(1100);
|
|
1607
|
+
receive(a.intent);
|
|
1608
|
+
assertEquals(starts, sender.starts.size());
|
|
1609
|
+
assertNull(field(null, "sInstance"));
|
|
1610
|
+
assertEquals(snapshot, store().getAll());
|
|
1611
|
+
// A fresh service needs an explicit new promoted ticket; no disk restoration occurs.
|
|
1612
|
+
request("b", true);
|
|
1613
|
+
ForegroundService b = create();
|
|
1614
|
+
deliver(b, sender.starts.get(1), 2);
|
|
1615
|
+
assertEquals(legacy, store().getString("record", null));
|
|
1616
|
+
assertEquals(legacy, store().getString(oldKey, null));
|
|
1617
|
+
ForegroundServiceRunLimitScheduler.cancel(a);
|
|
1618
|
+
assertEquals(legacy, store().getString("record", null));
|
|
1619
|
+
assertSuccessorExpires(b, a);
|
|
1620
|
+
assertEquals(legacy, store().getString(oldKey, null));
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
public static class StorageService extends ForegroundService {
|
|
1624
|
+
@Override
|
|
1625
|
+
public SharedPreferences getSharedPreferences(String name, int mode) {
|
|
1626
|
+
return Preferences.wrap(super.getSharedPreferences(name, mode), name);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
public static class Preferences {
|
|
1631
|
+
static int failCommit, commits;
|
|
1632
|
+
static boolean readFailure, enabled;
|
|
1633
|
+
static Runnable afterRead, beforeRemove, beforeCommit;
|
|
1634
|
+
static boolean commitThrows;
|
|
1635
|
+
|
|
1636
|
+
static void resetFaults() {
|
|
1637
|
+
failCommit = commits = 0;
|
|
1638
|
+
readFailure = enabled = false;
|
|
1639
|
+
afterRead = beforeRemove = beforeCommit = null;
|
|
1640
|
+
commitThrows = false;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
static SharedPreferences wrap(SharedPreferences prefs, String name) {
|
|
1644
|
+
if (!"notifee-fgs-run-limit".equals(name)
|
|
1645
|
+
|| (failCommit == 0
|
|
1646
|
+
&& !readFailure
|
|
1647
|
+
&& afterRead == null
|
|
1648
|
+
&& beforeRemove == null
|
|
1649
|
+
&& beforeCommit == null
|
|
1650
|
+
&& !commitThrows)) return prefs;
|
|
1651
|
+
SharedPreferences proxy =
|
|
1652
|
+
org.mockito.Mockito.mock(
|
|
1653
|
+
SharedPreferences.class,
|
|
1654
|
+
org.mockito.Mockito.withSettings()
|
|
1655
|
+
.defaultAnswer(
|
|
1656
|
+
i -> {
|
|
1657
|
+
if ("getString".equals(i.getMethod().getName()) && readFailure)
|
|
1658
|
+
throw new IllegalStateException("storage unreadable");
|
|
1659
|
+
if ("edit".equals(i.getMethod().getName())) {
|
|
1660
|
+
SharedPreferences.Editor real = prefs.edit();
|
|
1661
|
+
return (SharedPreferences.Editor)
|
|
1662
|
+
java.lang.reflect.Proxy.newProxyInstance(
|
|
1663
|
+
SharedPreferences.Editor.class.getClassLoader(),
|
|
1664
|
+
new Class<?>[] {SharedPreferences.Editor.class},
|
|
1665
|
+
(editorProxy, method, args) -> {
|
|
1666
|
+
if ("remove".equals(method.getName()) && beforeRemove != null) {
|
|
1667
|
+
Runnable hook = beforeRemove;
|
|
1668
|
+
beforeRemove = null;
|
|
1669
|
+
hook.run();
|
|
1670
|
+
}
|
|
1671
|
+
if ("commit".equals(method.getName())) {
|
|
1672
|
+
Runnable hook = beforeCommit;
|
|
1673
|
+
beforeCommit = null;
|
|
1674
|
+
if (hook != null) hook.run();
|
|
1675
|
+
if (commitThrows)
|
|
1676
|
+
throw new IllegalStateException("commit failed");
|
|
1677
|
+
commits++;
|
|
1678
|
+
if (commits == failCommit) return false;
|
|
1679
|
+
}
|
|
1680
|
+
Object result = method.invoke(real, args);
|
|
1681
|
+
return result instanceof SharedPreferences.Editor
|
|
1682
|
+
? editorProxy
|
|
1683
|
+
: result;
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
Object result = i.getMethod().invoke(prefs, i.getArguments());
|
|
1687
|
+
if ("getString".equals(i.getMethod().getName()) && afterRead != null) {
|
|
1688
|
+
Runnable hook = afterRead;
|
|
1689
|
+
afterRead = null;
|
|
1690
|
+
hook.run();
|
|
1691
|
+
}
|
|
1692
|
+
return result;
|
|
1693
|
+
}));
|
|
1694
|
+
return proxy;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
@Test
|
|
1699
|
+
public void auditLedgerIpcCannotTransferExpiryToSuccessor() throws Exception {
|
|
1700
|
+
Preferences.enabled = true;
|
|
1701
|
+
ForegroundService s = armed("a");
|
|
1702
|
+
Intent old = alarm();
|
|
1703
|
+
Preferences.afterRead =
|
|
1704
|
+
() -> {
|
|
1705
|
+
try {
|
|
1706
|
+
ForegroundService.stop();
|
|
1707
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
1708
|
+
request("b", true);
|
|
1709
|
+
deliver(s, sender.starts.get(1), 2);
|
|
1710
|
+
} catch (Exception error) {
|
|
1711
|
+
throw new AssertionError(error);
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
SystemClock.sleep(1100);
|
|
1715
|
+
receive(old);
|
|
1716
|
+
assertEquals("b", ForegroundService.mCurrentNotificationId);
|
|
1717
|
+
assertTrue(platform(s).foreground);
|
|
1718
|
+
assertNotNull(field(s, "mAlarmPoc"));
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
@Test
|
|
1722
|
+
public void auditReceiverOwnerRevalidatedAfterLastLedgerRead() throws Exception {
|
|
1723
|
+
Preferences.enabled = true;
|
|
1724
|
+
ForegroundService s = armed("a");
|
|
1725
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
1726
|
+
int[] successorDemotions = {-1};
|
|
1727
|
+
// First read returns the authentic A wire. The last read returns authentic ARMED A,
|
|
1728
|
+
// but only after B has been created. This reaches the post-ledger ownership fence.
|
|
1729
|
+
Preferences.afterRead =
|
|
1730
|
+
() ->
|
|
1731
|
+
Preferences.afterRead =
|
|
1732
|
+
() -> {
|
|
1733
|
+
promoteSuccessor(s).run();
|
|
1734
|
+
successorDemotions[0] = platform(s).demotions;
|
|
1735
|
+
};
|
|
1736
|
+
SystemClock.sleep(1100);
|
|
1737
|
+
receive(a.intent);
|
|
1738
|
+
assertNull(Preferences.afterRead);
|
|
1739
|
+
assertTrue("the final-read interleaving must execute", successorDemotions[0] >= 0);
|
|
1740
|
+
assertEquals(
|
|
1741
|
+
"old expiry cannot demote B after a matching ledger snapshot",
|
|
1742
|
+
successorDemotions[0],
|
|
1743
|
+
platform(s).demotions);
|
|
1744
|
+
assertSuccessorExpires(s, a);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
@Test
|
|
1748
|
+
public void auditManifestWithoutDataSyncCannotDispatch() throws Exception {
|
|
1749
|
+
ServiceInfo service =
|
|
1750
|
+
app.getPackageManager().getServiceInfo(new ComponentName(app, ForegroundService.class), 0);
|
|
1751
|
+
Field type = ServiceInfo.class.getDeclaredField("mForegroundServiceType");
|
|
1752
|
+
type.setAccessible(true);
|
|
1753
|
+
type.setInt(service, ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE);
|
|
1754
|
+
shadowOf(app.getPackageManager()).addOrUpdateService(service);
|
|
1755
|
+
assertThrows(IllegalStateException.class, () -> request("a", true));
|
|
1756
|
+
assertTrue(sender.starts.isEmpty());
|
|
1757
|
+
assertEquals(0, Alarms.requested);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
@Test
|
|
1761
|
+
public void auditPendingIntentIdentityIsUniqueWithoutCancellation() throws Exception {
|
|
1762
|
+
ForegroundService s = create();
|
|
1763
|
+
ForegroundServiceRunLimitScheduler.Request a =
|
|
1764
|
+
new ForegroundServiceRunLimitScheduler.Request(app, "Aa", 1000);
|
|
1765
|
+
ForegroundServiceRunLimitScheduler.Request b =
|
|
1766
|
+
new ForegroundServiceRunLimitScheduler.Request(app, "BB", 1000);
|
|
1767
|
+
ForegroundServiceRunLimitScheduler.Ticket first =
|
|
1768
|
+
new ForegroundServiceRunLimitScheduler.Ticket(
|
|
1769
|
+
s, new Object(), 0, 0, a, SystemClock.elapsedRealtime());
|
|
1770
|
+
ForegroundServiceRunLimitScheduler.Ticket second =
|
|
1771
|
+
new ForegroundServiceRunLimitScheduler.Ticket(
|
|
1772
|
+
s, new Object(), 0, 0, b, SystemClock.elapsedRealtime());
|
|
1773
|
+
try {
|
|
1774
|
+
assertTrue(ForegroundServiceRunLimitScheduler.arm(first));
|
|
1775
|
+
assertTrue(ForegroundServiceRunLimitScheduler.arm(second));
|
|
1776
|
+
assertNotEquals(first.pendingIntent, second.pendingIntent);
|
|
1777
|
+
assertNotEquals(first.intent.getData(), second.intent.getData());
|
|
1778
|
+
} finally {
|
|
1779
|
+
ForegroundServiceRunLimitScheduler.cancel(first);
|
|
1780
|
+
ForegroundServiceRunLimitScheduler.cancel(second);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
Bundle publicBundle(String id, Object duration) {
|
|
1785
|
+
Bundle b = bundle(id, DATA);
|
|
1786
|
+
Bundle a = b.getBundle("android");
|
|
1787
|
+
a.putBoolean("asForegroundService", true);
|
|
1788
|
+
if (duration instanceof Double)
|
|
1789
|
+
a.putDouble("foregroundServiceBestEffortStopAfterMs", (Double) duration);
|
|
1790
|
+
else if (duration instanceof Long)
|
|
1791
|
+
a.putLong("foregroundServiceBestEffortStopAfterMs", (Long) duration);
|
|
1792
|
+
else if (duration instanceof String)
|
|
1793
|
+
a.putString("foregroundServiceBestEffortStopAfterMs", (String) duration);
|
|
1794
|
+
return b;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
void publicStart(Bundle b) {
|
|
1798
|
+
ForegroundService.start(b.getString("id").hashCode(), notification(), b);
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
ForegroundService publicArmed(String id) {
|
|
1802
|
+
publicStart(publicBundle(id, 60000L));
|
|
1803
|
+
assertEquals("no registration before real promotion", 0, Alarms.requested);
|
|
1804
|
+
ForegroundService s = create();
|
|
1805
|
+
deliver(s, sender.starts.get(sender.starts.size() - 1), 1);
|
|
1806
|
+
assertEquals("public option must reach AlarmManager", 1, Alarms.scheduled.size());
|
|
1807
|
+
return s;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
@Test
|
|
1811
|
+
public void PUB_validBridgeDoubleAndPostPromotion() throws Exception {
|
|
1812
|
+
publicStart(publicBundle("public", 60000.0));
|
|
1813
|
+
assertEquals(0, Alarms.requested);
|
|
1814
|
+
ForegroundService s = create();
|
|
1815
|
+
deliver(s, sender.starts.get(0), 1);
|
|
1816
|
+
assertEquals("public JS double must arm", 1, Alarms.scheduled.size());
|
|
1817
|
+
assertEquals(SystemClock.elapsedRealtime() + 60000, Alarms.deadline);
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
@Test
|
|
1821
|
+
public void PUB_nativeInvalidNeverDispatchesStart() {
|
|
1822
|
+
for (Object value :
|
|
1823
|
+
new Object[] {
|
|
1824
|
+
Double.NaN,
|
|
1825
|
+
Double.POSITIVE_INFINITY,
|
|
1826
|
+
60000.5,
|
|
1827
|
+
0L,
|
|
1828
|
+
-1L,
|
|
1829
|
+
59999L,
|
|
1830
|
+
18000001L,
|
|
1831
|
+
Long.MAX_VALUE,
|
|
1832
|
+
"60000"
|
|
1833
|
+
}) {
|
|
1834
|
+
assertThrows(
|
|
1835
|
+
"invalid option: " + value,
|
|
1836
|
+
IllegalArgumentException.class,
|
|
1837
|
+
() -> publicStart(publicBundle("bad", value)));
|
|
1838
|
+
assertTrue(sender.starts.isEmpty());
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
@Test
|
|
1843
|
+
@Config(sdk = 34)
|
|
1844
|
+
public void PUB_unsupportedApiNeverDispatchesStart() {
|
|
1845
|
+
assertThrows(IllegalArgumentException.class, () -> publicStart(publicBundle("old", 60000L)));
|
|
1846
|
+
assertTrue(sender.starts.isEmpty());
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
@Test
|
|
1850
|
+
public void PUB_requiresAsForegroundService() {
|
|
1851
|
+
Bundle b = publicBundle("not-fgs", 60000L);
|
|
1852
|
+
b.getBundle("android").putBoolean("asForegroundService", false);
|
|
1853
|
+
assertThrows(IllegalArgumentException.class, () -> publicStart(b));
|
|
1854
|
+
assertTrue(sender.starts.isEmpty());
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
@Test
|
|
1858
|
+
public void PUB_mixedTypesNeverDispatchStart() {
|
|
1859
|
+
Bundle b = publicBundle("mixed", 60000L);
|
|
1860
|
+
b.getBundle("android")
|
|
1861
|
+
.putIntegerArrayList("foregroundServiceTypes", new ArrayList<>(Arrays.asList(DATA, 2)));
|
|
1862
|
+
assertThrows(IllegalArgumentException.class, () -> publicStart(b));
|
|
1863
|
+
assertTrue(sender.starts.isEmpty());
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
@Test
|
|
1867
|
+
public void PUB_sameIdRepeatAndOmissionKeepOriginalDeadline() throws Exception {
|
|
1868
|
+
ForegroundService s = publicArmed("same");
|
|
1869
|
+
Object first = field(s, "mAlarmPoc");
|
|
1870
|
+
long deadline = Alarms.deadline;
|
|
1871
|
+
publicStart(publicBundle("same", 60000L));
|
|
1872
|
+
deliver(s, sender.starts.get(1), 2);
|
|
1873
|
+
publicStart(publicBundle("same", null));
|
|
1874
|
+
deliver(s, sender.starts.get(2), 3);
|
|
1875
|
+
assertEquals(1, Alarms.requested);
|
|
1876
|
+
assertEquals(deadline, Alarms.deadline);
|
|
1877
|
+
assertSame(first, field(s, "mAlarmPoc"));
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
@Test
|
|
1881
|
+
public void PUB_changedDurationRejectedBeforeStart() throws Exception {
|
|
1882
|
+
publicArmed("same");
|
|
1883
|
+
assertThrows(IllegalArgumentException.class, () -> publicStart(publicBundle("same", 120000L)));
|
|
1884
|
+
assertEquals(1, sender.starts.size());
|
|
1885
|
+
assertEquals(1, Alarms.requested);
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
@Test
|
|
1889
|
+
public void PUB_cannotEnableMidRun() throws Exception {
|
|
1890
|
+
publicStart(publicBundle("same", null));
|
|
1891
|
+
ForegroundService s = create();
|
|
1892
|
+
deliver(s, sender.starts.get(0), 1);
|
|
1893
|
+
assertThrows(IllegalArgumentException.class, () -> publicStart(publicBundle("same", 60000L)));
|
|
1894
|
+
assertEquals(0, Alarms.requested);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
@Test
|
|
1898
|
+
public void PUB_deniedPromotionNeverArms() throws Exception {
|
|
1899
|
+
publicStart(publicBundle("denied", 60000L));
|
|
1900
|
+
ForegroundService s = create();
|
|
1901
|
+
Platform.promotionFailure = new ForegroundServiceStartNotAllowedException("denied");
|
|
1902
|
+
deliver(s, sender.starts.get(0), 1);
|
|
1903
|
+
assertEquals(0, Alarms.requested);
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
@Test
|
|
1907
|
+
public void PUB_gcDoesNotRunBeforePromotion() throws Exception {
|
|
1908
|
+
publicStart(publicBundle("latency", 60000L));
|
|
1909
|
+
assertTrue(
|
|
1910
|
+
ShadowLog.getLogsForTag("ForegroundServiceRunLimit").stream()
|
|
1911
|
+
.noneMatch(x -> x.msg.contains("GC_RESULT")));
|
|
1912
|
+
ForegroundService s = create();
|
|
1913
|
+
Platform.beforePromotion =
|
|
1914
|
+
() ->
|
|
1915
|
+
assertTrue(
|
|
1916
|
+
ShadowLog.getLogsForTag("ForegroundServiceRunLimit").stream()
|
|
1917
|
+
.noneMatch(x -> x.msg.contains("GC_RESULT")));
|
|
1918
|
+
deliver(s, sender.starts.get(0), 1);
|
|
1919
|
+
assertEquals(1, Alarms.scheduled.size());
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
@Test
|
|
1923
|
+
public void PUB_releaseAppArmsWithoutDebugReflection() throws Exception {
|
|
1924
|
+
app.getApplicationInfo().flags &= ~ApplicationInfo.FLAG_DEBUGGABLE;
|
|
1925
|
+
publicArmed("release");
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
@Test
|
|
1929
|
+
public void PUB_expiryEventUsesLatestNotificationAfterTeardownUnlocked() throws Exception {
|
|
1930
|
+
ForegroundService s = publicArmed("event");
|
|
1931
|
+
Bundle update = publicBundle("event", null);
|
|
1932
|
+
update.putString("title", "updated");
|
|
1933
|
+
publicStart(update);
|
|
1934
|
+
deliver(s, sender.starts.get(1), 2);
|
|
1935
|
+
List<app.notifee.core.event.NotificationEvent> events = new ArrayList<>();
|
|
1936
|
+
try (org.mockito.MockedStatic<EventBus> bus = org.mockito.Mockito.mockStatic(EventBus.class)) {
|
|
1937
|
+
bus.when(() -> EventBus.post(org.mockito.ArgumentMatchers.any()))
|
|
1938
|
+
.thenAnswer(
|
|
1939
|
+
i -> {
|
|
1940
|
+
Object event = i.getArgument(0);
|
|
1941
|
+
if (event instanceof app.notifee.core.event.NotificationEvent) {
|
|
1942
|
+
assertEquals(1, platform(s).demotions);
|
|
1943
|
+
assertEquals(1, platform(s).stopCalls);
|
|
1944
|
+
assertFalse(Thread.holdsLock(field(null, "sLifecycleLock")));
|
|
1945
|
+
assertFalse(Thread.holdsLock(field(null, "sLock")));
|
|
1946
|
+
events.add((app.notifee.core.event.NotificationEvent) event);
|
|
1947
|
+
}
|
|
1948
|
+
return null;
|
|
1949
|
+
});
|
|
1950
|
+
SystemClock.sleep(60001);
|
|
1951
|
+
receive(alarm());
|
|
1952
|
+
receive(alarm());
|
|
1953
|
+
assertEquals(1, events.size());
|
|
1954
|
+
assertEquals(10, events.get(0).getType());
|
|
1955
|
+
assertEquals("updated", events.get(0).getNotification().toBundle().getString("title"));
|
|
1956
|
+
Map<?, ?> detail = (Map<?, ?>) events.get(0).toMap().get("detail");
|
|
1957
|
+
Map<?, ?> meta = (Map<?, ?>) detail.get("foregroundServiceRunLimit");
|
|
1958
|
+
assertEquals(60000L, ((Number) meta.get("requestedDurationMs")).longValue());
|
|
1959
|
+
assertEquals("returned", meta.get("demotion"));
|
|
1960
|
+
assertEquals("accepted", meta.get("conditionalStop"));
|
|
1961
|
+
assertFalse(meta.containsKey("stopped"));
|
|
1962
|
+
assertFalse(meta.containsKey("amsState"));
|
|
1963
|
+
assertNull(field(s, "mRunnerToken"));
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
@Test
|
|
1968
|
+
public void PUB_eventFailureCannotPreventTeardown() throws Exception {
|
|
1969
|
+
ForegroundService s = publicArmed("bridge-dead");
|
|
1970
|
+
try (org.mockito.MockedStatic<EventBus> bus = org.mockito.Mockito.mockStatic(EventBus.class)) {
|
|
1971
|
+
bus.when(() -> EventBus.post(org.mockito.ArgumentMatchers.any()))
|
|
1972
|
+
.thenThrow(new IllegalStateException("React absent"));
|
|
1973
|
+
SystemClock.sleep(60001);
|
|
1974
|
+
receive(alarm());
|
|
1975
|
+
assertEquals(1, platform(s).demotions);
|
|
1976
|
+
assertEquals(1, platform(s).stopCalls);
|
|
1977
|
+
assertTrue(marker("EVENT_FAILED"));
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
@Test
|
|
1982
|
+
public void PUB_storageAndSchedulingFailureAreObservable() throws Exception {
|
|
1983
|
+
Alarms.scheduleFailure = true;
|
|
1984
|
+
publicStart(publicBundle("schedule-failed", 60000L));
|
|
1985
|
+
ForegroundService s = create();
|
|
1986
|
+
deliver(s, sender.starts.get(0), 1);
|
|
1987
|
+
assertTrue(marker("SCHEDULE_FAILED"));
|
|
1988
|
+
assertFalse(marker("ALARM_ARMED"));
|
|
1989
|
+
assertNull(field(s, "mAlarmPoc"));
|
|
1990
|
+
assertTrue(platform(s).foreground);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
@Test
|
|
1994
|
+
public void PUB_nativeDeadlineOverflowRejected() {
|
|
1995
|
+
try (org.mockito.MockedStatic<SystemClock> clock =
|
|
1996
|
+
org.mockito.Mockito.mockStatic(SystemClock.class)) {
|
|
1997
|
+
clock.when(SystemClock::elapsedRealtime).thenReturn(Long.MAX_VALUE - 100);
|
|
1998
|
+
assertThrows(
|
|
1999
|
+
IllegalArgumentException.class, () -> publicStart(publicBundle("overflow", 60000L)));
|
|
2000
|
+
assertTrue(sender.starts.isEmpty());
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
@Test
|
|
2005
|
+
public void PUB_gcRecognizesFullPublicRange() throws Exception {
|
|
2006
|
+
String key = gcOrphan(0);
|
|
2007
|
+
org.json.JSONObject wire = new org.json.JSONObject(store().getString(key, null));
|
|
2008
|
+
wire.put("deadlineElapsed", 18000001L);
|
|
2009
|
+
store().edit().putString(key, wire.toString()).commit();
|
|
2010
|
+
collectCycle(app);
|
|
2011
|
+
assertFalse("valid public five-hour orphan is collectible", store().contains(key));
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
@Test
|
|
2015
|
+
public void PUB_newRunAfterRunnerCompletionGetsNewAuthority() throws Exception {
|
|
2016
|
+
Runners runners = new Runners();
|
|
2017
|
+
EventBus.register(runners);
|
|
2018
|
+
try {
|
|
2019
|
+
ForegroundService s = publicArmed("completed");
|
|
2020
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
2021
|
+
runners.events.get(0).setCompletionResult();
|
|
2022
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
2023
|
+
publicStart(publicBundle("next", 60000L));
|
|
2024
|
+
deliver(s, sender.starts.get(1), 2);
|
|
2025
|
+
ForegroundServiceRunLimitScheduler.Ticket b = ticket(s);
|
|
2026
|
+
assertNotNull("new real promotion must arm its own ticket", b);
|
|
2027
|
+
assertNotSame(a, b);
|
|
2028
|
+
assertNotEquals(a.token, b.token);
|
|
2029
|
+
assertNotSame(a.epoch, b.epoch);
|
|
2030
|
+
SystemClock.sleep(60001);
|
|
2031
|
+
receive(a.intent);
|
|
2032
|
+
assertSame(b, ticket(s));
|
|
2033
|
+
} finally {
|
|
2034
|
+
EventBus.unregister(runners);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
@Test
|
|
2039
|
+
public void PUB_reentrantStartDuringStopCancellationSupersedesOldStop() throws Exception {
|
|
2040
|
+
ForegroundService s = publicArmed("stop-order");
|
|
2041
|
+
ForegroundServiceRunLimitScheduler.Ticket a = ticket(s);
|
|
2042
|
+
Alarms.duringCancel =
|
|
2043
|
+
() -> {
|
|
2044
|
+
publicStart(publicBundle("stop-order", null));
|
|
2045
|
+
deliver(s, sender.starts.get(1), 2);
|
|
2046
|
+
};
|
|
2047
|
+
ForegroundService.stop();
|
|
2048
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
2049
|
+
assertFalse("later accepted START must supersede an older STOP", platform(s).stopped);
|
|
2050
|
+
assertTrue(platform(s).foreground);
|
|
2051
|
+
assertEquals("stop-order", ForegroundService.mCurrentNotificationId);
|
|
2052
|
+
assertNotNull(field(s, "mRunnerToken"));
|
|
2053
|
+
assertNotSame(a.epoch, field(s, "mForegroundEpoch"));
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
@Test
|
|
2057
|
+
public void PUB_completionCancellationCannotClearSuccessorState() throws Exception {
|
|
2058
|
+
Runners runners = new Runners();
|
|
2059
|
+
EventBus.register(runners);
|
|
2060
|
+
try {
|
|
2061
|
+
ForegroundService s = publicArmed("completion-a");
|
|
2062
|
+
ForegroundServiceRunLimitScheduler.Ticket[] b = {null};
|
|
2063
|
+
Alarms.duringCancel =
|
|
2064
|
+
() -> {
|
|
2065
|
+
ForegroundService.stop();
|
|
2066
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
2067
|
+
publicStart(publicBundle("completion-b", 60000L));
|
|
2068
|
+
deliver(s, sender.starts.get(sender.starts.size() - 1), 2);
|
|
2069
|
+
try {
|
|
2070
|
+
b[0] = ticket(s);
|
|
2071
|
+
assertNotNull(b[0]);
|
|
2072
|
+
} catch (Exception failure) {
|
|
2073
|
+
throw new AssertionError(failure);
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
2076
|
+
runners.events.get(0).setCompletionResult();
|
|
2077
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
2078
|
+
assertSame(b[0], ticket(s));
|
|
2079
|
+
assertEquals(
|
|
2080
|
+
"old cleanup must preserve successor notification",
|
|
2081
|
+
"completion-b",
|
|
2082
|
+
ForegroundService.mCurrentNotificationId);
|
|
2083
|
+
assertNotNull(field(s, "mRunnerToken"));
|
|
2084
|
+
assertTrue(b[0].valid);
|
|
2085
|
+
SystemClock.sleep(60001);
|
|
2086
|
+
receive(b[0].intent);
|
|
2087
|
+
assertFalse("authentic successor expiry still works", platform(s).foreground);
|
|
2088
|
+
} finally {
|
|
2089
|
+
EventBus.unregister(runners);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
@Test
|
|
2094
|
+
public void PUB_typeCommitCannotOverwriteReentrantSuccessor() throws Exception {
|
|
2095
|
+
ForegroundService s = publicArmed("type-a");
|
|
2096
|
+
Object[] successor = new Object[2];
|
|
2097
|
+
Alarms.duringCancel =
|
|
2098
|
+
() -> {
|
|
2099
|
+
ForegroundService.stop();
|
|
2100
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
2101
|
+
publicStart(publicBundle("type-b", null));
|
|
2102
|
+
deliver(s, sender.starts.get(sender.starts.size() - 1), 3);
|
|
2103
|
+
try {
|
|
2104
|
+
successor[0] = field(s, "mForegroundEpoch");
|
|
2105
|
+
successor[1] = field(s, "mRunnerToken");
|
|
2106
|
+
} catch (Exception failure) {
|
|
2107
|
+
throw new AssertionError(failure);
|
|
2108
|
+
}
|
|
2109
|
+
};
|
|
2110
|
+
Bundle update = publicBundle("type-a", null);
|
|
2111
|
+
update
|
|
2112
|
+
.getBundle("android")
|
|
2113
|
+
.putIntegerArrayList("foregroundServiceTypes", new ArrayList<>(Arrays.asList(DATA, 2)));
|
|
2114
|
+
publicStart(update);
|
|
2115
|
+
deliver(s, sender.starts.get(1), 2);
|
|
2116
|
+
assertSame("old type commit must preserve B epoch", successor[0], field(s, "mForegroundEpoch"));
|
|
2117
|
+
assertSame(successor[1], field(s, "mRunnerToken"));
|
|
2118
|
+
assertEquals("type-b", ForegroundService.mCurrentNotificationId);
|
|
2119
|
+
assertEquals("type-b", ((Bundle) field(null, "mCurrentNotificationBundle")).getString("id"));
|
|
2120
|
+
assertEquals(DATA, ForegroundService.mCurrentForegroundServiceType);
|
|
2121
|
+
assertTrue(platform(s).foreground);
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
static Runnable publicBootHook;
|
|
2125
|
+
|
|
2126
|
+
public static class BootReadService extends ForegroundService {
|
|
2127
|
+
@Override
|
|
2128
|
+
public ContentResolver getContentResolver() {
|
|
2129
|
+
if (publicBootHook != null) {
|
|
2130
|
+
Runnable hook = publicBootHook;
|
|
2131
|
+
publicBootHook = null;
|
|
2132
|
+
hook.run();
|
|
2133
|
+
throw new IllegalStateException("old ticket boot provider failed");
|
|
2134
|
+
}
|
|
2135
|
+
return super.getContentResolver();
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
@Test
|
|
2140
|
+
public void PUB_oldConstructionFailureCannotCancelNewTicket() throws Exception {
|
|
2141
|
+
ServiceController<BootReadService> controller = Robolectric.buildService(BootReadService.class);
|
|
2142
|
+
controllers.add(controller);
|
|
2143
|
+
ForegroundService s = controller.create().get();
|
|
2144
|
+
ForegroundServiceRunLimitScheduler.Ticket[] b = {null};
|
|
2145
|
+
publicBootHook =
|
|
2146
|
+
() -> {
|
|
2147
|
+
ForegroundService.stop();
|
|
2148
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
2149
|
+
publicStart(publicBundle("boot-b", 60000L));
|
|
2150
|
+
deliver(s, sender.starts.get(sender.starts.size() - 1), 2);
|
|
2151
|
+
try {
|
|
2152
|
+
b[0] = ticket(s);
|
|
2153
|
+
assertNotNull(b[0]);
|
|
2154
|
+
} catch (Exception failure) {
|
|
2155
|
+
throw new AssertionError(failure);
|
|
2156
|
+
}
|
|
2157
|
+
};
|
|
2158
|
+
try {
|
|
2159
|
+
publicStart(publicBundle("boot-a", 60000L));
|
|
2160
|
+
deliver(s, sender.starts.get(0), 1);
|
|
2161
|
+
assertNotNull(b[0]);
|
|
2162
|
+
assertSame("A's catch cannot cancel B", b[0], ticket(s));
|
|
2163
|
+
assertTrue(b[0].valid);
|
|
2164
|
+
SystemClock.sleep(60001);
|
|
2165
|
+
receive(b[0].intent);
|
|
2166
|
+
assertFalse(platform(s).foreground);
|
|
2167
|
+
} finally {
|
|
2168
|
+
publicBootHook = null;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
@Test
|
|
2173
|
+
public void PUB_nativeFacadeReportsValidationThroughCallback() throws Exception {
|
|
2174
|
+
java.util.concurrent.CountDownLatch done = new java.util.concurrent.CountDownLatch(1);
|
|
2175
|
+
java.util.concurrent.atomic.AtomicReference<Exception> error =
|
|
2176
|
+
new java.util.concurrent.atomic.AtomicReference<>();
|
|
2177
|
+
try {
|
|
2178
|
+
Notifee.getInstance()
|
|
2179
|
+
.displayNotification(
|
|
2180
|
+
publicBundle("native-invalid", Double.NaN),
|
|
2181
|
+
(failure, result) -> {
|
|
2182
|
+
error.set(failure);
|
|
2183
|
+
done.countDown();
|
|
2184
|
+
});
|
|
2185
|
+
} catch (RuntimeException failure) {
|
|
2186
|
+
fail("native facade must report validation through its async callback: " + failure);
|
|
2187
|
+
}
|
|
2188
|
+
assertTrue(done.await(5, java.util.concurrent.TimeUnit.SECONDS));
|
|
2189
|
+
assertNotNull(error.get());
|
|
2190
|
+
assertTrue(sender.starts.isEmpty());
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
@Test
|
|
2194
|
+
public void PUB_terminalCancellationCannotStopSuccessor() throws Exception {
|
|
2195
|
+
ForegroundService s = publicArmed("terminal-a");
|
|
2196
|
+
ForegroundServiceRunLimitScheduler.Ticket[] b = {null};
|
|
2197
|
+
int[] priorStops = {0};
|
|
2198
|
+
Alarms.duringCancel =
|
|
2199
|
+
() -> {
|
|
2200
|
+
ForegroundService.stop();
|
|
2201
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
2202
|
+
publicStart(publicBundle("terminal-b", 60000L));
|
|
2203
|
+
deliver(s, sender.starts.get(sender.starts.size() - 1), 3);
|
|
2204
|
+
try {
|
|
2205
|
+
b[0] = ticket(s);
|
|
2206
|
+
assertNotNull(b[0]);
|
|
2207
|
+
priorStops[0] = platform(s).stopCalls;
|
|
2208
|
+
} catch (Exception failure) {
|
|
2209
|
+
throw new AssertionError(failure);
|
|
2210
|
+
}
|
|
2211
|
+
};
|
|
2212
|
+
deliver(s, null, 2);
|
|
2213
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
2214
|
+
assertSame("old terminal cannot revoke B", b[0], ticket(s));
|
|
2215
|
+
assertEquals(priorStops[0], platform(s).stopCalls);
|
|
2216
|
+
assertTrue(b[0].valid);
|
|
2217
|
+
assertEquals("terminal-b", ForegroundService.mCurrentNotificationId);
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
@Test
|
|
2221
|
+
public void PUB_terminalCancellationPreservesNewPendingStart() throws Exception {
|
|
2222
|
+
ForegroundService s = publicArmed("pending-a");
|
|
2223
|
+
Alarms.duringCancel = () -> publicStart(publicBundle("pending-b", null));
|
|
2224
|
+
deliver(s, null, 2);
|
|
2225
|
+
deliver(s, sender.starts.get(sender.starts.size() - 1), 3);
|
|
2226
|
+
shadowOf(Looper.getMainLooper()).idle();
|
|
2227
|
+
assertFalse("newer accepted pending START supersedes old terminal", platform(s).stopped);
|
|
2228
|
+
assertEquals("pending-b", ForegroundService.mCurrentNotificationId);
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
@Test
|
|
2232
|
+
public void PUB_frameworkTimeoutSurvivesFailedVoluntaryTeardown() throws Exception {
|
|
2233
|
+
ForegroundService s = publicArmed("fallback");
|
|
2234
|
+
Intent original = alarm();
|
|
2235
|
+
Platform.demotionFailure = true;
|
|
2236
|
+
Platform.stopFailure = true;
|
|
2237
|
+
SystemClock.sleep(60001);
|
|
2238
|
+
receive(original);
|
|
2239
|
+
assertTrue(platform(s).foreground);
|
|
2240
|
+
assertEquals(1, platform(s).demotions);
|
|
2241
|
+
Platform.demotionFailure = false;
|
|
2242
|
+
Platform.stopFailure = false;
|
|
2243
|
+
s.onTimeout(1, DATA);
|
|
2244
|
+
assertEquals(
|
|
2245
|
+
"framework timeout retains its independent teardown chance", 2, platform(s).demotions);
|
|
2246
|
+
assertFalse(platform(s).foreground);
|
|
2247
|
+
receive(original);
|
|
2248
|
+
assertEquals(2, platform(s).demotions);
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
static class Sender extends ContextWrapper {
|
|
2252
|
+
final List<Intent> starts = new ArrayList<>();
|
|
2253
|
+
RuntimeException failure;
|
|
2254
|
+
|
|
2255
|
+
Sender(Context c) {
|
|
2256
|
+
super(c);
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
@Override
|
|
2260
|
+
public ComponentName startForegroundService(Intent i) {
|
|
2261
|
+
starts.add(new Intent(i));
|
|
2262
|
+
if (failure != null) throw failure;
|
|
2263
|
+
return i.getComponent();
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
@Implements(AlarmManager.class)
|
|
2268
|
+
public static class Alarms extends ShadowAlarmManager {
|
|
2269
|
+
static int requested, type, cancelled;
|
|
2270
|
+
static long deadline;
|
|
2271
|
+
static boolean scheduleFailure, cancelFailure;
|
|
2272
|
+
static PendingIntent lastRequested;
|
|
2273
|
+
static Runnable afterSchedule, duringCancel;
|
|
2274
|
+
static List<PendingIntent> cancellations = new ArrayList<>();
|
|
2275
|
+
static List<PendingIntent> scheduled = new ArrayList<>();
|
|
2276
|
+
|
|
2277
|
+
public static void reset() {
|
|
2278
|
+
requested = cancelled = 0;
|
|
2279
|
+
scheduled.clear();
|
|
2280
|
+
scheduleFailure = cancelFailure = false;
|
|
2281
|
+
lastRequested = null;
|
|
2282
|
+
afterSchedule = duringCancel = null;
|
|
2283
|
+
cancellations.clear();
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
@Implementation
|
|
2287
|
+
protected void setAndAllowWhileIdle(int t, long d, PendingIntent p) {
|
|
2288
|
+
requested++;
|
|
2289
|
+
type = t;
|
|
2290
|
+
deadline = d;
|
|
2291
|
+
lastRequested = p;
|
|
2292
|
+
if (scheduleFailure) throw new IllegalStateException("alarm unavailable");
|
|
2293
|
+
scheduled.add(p);
|
|
2294
|
+
if (afterSchedule != null) afterSchedule.run();
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
@Implementation
|
|
2298
|
+
protected void cancel(PendingIntent p) {
|
|
2299
|
+
cancelled++;
|
|
2300
|
+
cancellations.add(p);
|
|
2301
|
+
Runnable hook = duringCancel;
|
|
2302
|
+
duringCancel = null;
|
|
2303
|
+
if (hook != null) hook.run();
|
|
2304
|
+
if (cancelFailure) throw new IllegalStateException("cancel lost reply");
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
@Implements(Service.class)
|
|
2309
|
+
public static class Platform extends ShadowService {
|
|
2310
|
+
|
|
2311
|
+
int accepted, delivered, demotions, stopCalls, lastStopId;
|
|
2312
|
+
boolean foreground, stopped, stopResult = true;
|
|
2313
|
+
final List<String> order = new ArrayList<>();
|
|
2314
|
+
static RuntimeException promotionFailure;
|
|
2315
|
+
static boolean demotionFailure, stopFailure;
|
|
2316
|
+
static Runnable beforePromotion, duringDemotion, duringStop;
|
|
2317
|
+
|
|
2318
|
+
static void reset() {
|
|
2319
|
+
promotionFailure = null;
|
|
2320
|
+
demotionFailure = stopFailure = false;
|
|
2321
|
+
beforePromotion = duringDemotion = duringStop = null;
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
@Implementation
|
|
2325
|
+
protected void startForeground(int id, Notification n, int type) {
|
|
2326
|
+
if (beforePromotion != null) beforePromotion.run();
|
|
2327
|
+
if (promotionFailure != null) throw promotionFailure;
|
|
2328
|
+
foreground = true;
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
@Implementation
|
|
2332
|
+
protected void stopForeground(int flags) {
|
|
2333
|
+
assertEquals(Service.STOP_FOREGROUND_REMOVE, flags);
|
|
2334
|
+
demotions++;
|
|
2335
|
+
order.add("demote");
|
|
2336
|
+
if (demotionFailure) throw new SecurityException("demotion denied");
|
|
2337
|
+
foreground = false;
|
|
2338
|
+
Runnable hook = duringDemotion;
|
|
2339
|
+
duringDemotion = null;
|
|
2340
|
+
if (hook != null) hook.run();
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
@Implementation
|
|
2344
|
+
protected boolean stopSelfResult(int id) {
|
|
2345
|
+
stopCalls++;
|
|
2346
|
+
lastStopId = id;
|
|
2347
|
+
order.add("stop:" + id);
|
|
2348
|
+
if (stopFailure) throw new IllegalStateException("stop failure");
|
|
2349
|
+
boolean result = stopResult && id == accepted;
|
|
2350
|
+
stopped = result;
|
|
2351
|
+
Runnable hook = duringStop;
|
|
2352
|
+
duringStop = null;
|
|
2353
|
+
if (hook != null) hook.run();
|
|
2354
|
+
return result;
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
}
|